Refactor/file size limit - #45
Conversation
…ules and create refactoring plan
…ollowing the 500-line limit
Reviewer's GuideThe PR refactors three large modules (API client, data processor, and report generator) into focused sub-packages under core/api_client, core/data_processor, and visualization/report_generator, adds package-level init.py files to preserve the original public API, updates imports accordingly, and introduces a refactoring plan in documentation. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Here's the code health analysis summary for commits Analysis Summary
|
There was a problem hiding this comment.
Summary of Changes
Hello @uelkerd, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request significantly enhances the maintainability and modularity of the Governance Token Distribution Analyzer by refactoring several large core components into smaller, more focused packages. This strategic decomposition aims to improve code organization, facilitate easier testing, and reduce cognitive load for developers, aligning the codebase with a 500-line file size limit and the Single Responsibility Principle.
Highlights
- Codebase Refactoring for File Size Limits: The core intent of this pull request is to refactor large files within the Governance Token Distribution Analyzer codebase to enforce a 500-line limit per file. This is a strategic move to improve maintainability, testability, and overall code quality by breaking down complex, monolithic files into smaller, more focused modules and packages, adhering to the Single Responsibility Principle (SRP).
- Modularization of API Client: The extensive
src/governance_token_analyzer/core/api_client.pyfile has been completely restructured. Its functionality is now distributed across a newapi_clientpackage, comprising specialized modules for base client operations, data fetching, Ethereum-specific interactions, The Graph API integration, protocol-specific logic, and standardized response parsing. The original file now serves as a lightweight compatibility layer, ensuring existing imports continue to function. - Modularization of Data Processor: Similarly, the
src/governance_token_analyzer/core/data_processor.pyfile has undergone a significant refactor. Its responsibilities are now encapsulated within a newdata_processorpackage, which includes modules for base processing, metrics calculation, report generation, and data standardization. A placeholder module for visualization logic has also been introduced, and the original file acts as a compatibility shim. - Modularization of Report Generator: The report generation functionality, previously concentrated in a single file (implied from the refactoring plan), has been modularized into a
report_generatorpackage. This new structure includes dedicated modules for base report generation, comprehensive reports, historical analysis reports, and HTML-specific report generation, enhancing organization and reusability. - Detailed Refactoring Plan Documentation: A comprehensive
docs/refactoring_plan.mddocument has been added. This plan meticulously outlines the project's goals, identifies remaining large files, details the refactoring strategies employed, specifies implementation phases, describes the testing approach, defines coding standards, and sets clear success criteria. It explicitly marks the refactoring of the API client, data processor, and report generator as completed phases.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.33 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.33 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| def _create_governance_visualization( | ||
| protocol: str, governance_data: List[Dict[str, Any]], viz_dir: str, timestamp: str | ||
| ) -> Optional[Dict[str, str]]: | ||
| """Create visualization for governance data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| governance_data: Governance data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Visualization metadata or None if failed | ||
| """ | ||
| try: | ||
| if not governance_data: | ||
| return None | ||
|
|
||
| # Create governance participation chart | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_governance_{timestamp}.png") | ||
|
|
||
| # Extract participation data | ||
| proposals_data = [] | ||
| for proposal in governance_data: | ||
| if "id" in proposal and "for_votes" in proposal and "against_votes" in proposal: | ||
| proposals_data.append( | ||
| { | ||
| "id": proposal["id"], | ||
| "for": float(proposal.get("for_votes", 0)), | ||
| "against": float(proposal.get("against_votes", 0)), | ||
| } | ||
| ) | ||
|
|
||
| if proposals_data: | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
|
|
||
| proposal_ids = [p["id"] for p in proposals_data] | ||
| for_votes = [p["for"] for p in proposals_data] | ||
| against_votes = [p["against"] for p in proposals_data] | ||
|
|
||
| # Width of the bars | ||
| width = 0.3 | ||
|
|
||
| # Position of bars on x-axis | ||
| ind = np.arange(len(proposal_ids)) | ||
|
|
||
| # Creating bars | ||
| ax.bar(ind - width / 2, for_votes, width, label="For") | ||
| ax.bar(ind + width / 2, against_votes, width, label="Against") | ||
|
|
||
| # Labels and title | ||
| ax.set_xlabel("Proposal ID") | ||
| ax.set_ylabel("Votes") | ||
| ax.set_title(f"{protocol.upper()} Governance Participation") | ||
| ax.set_xticks(ind) | ||
| ax.set_xticklabels(proposal_ids) | ||
| ax.legend() | ||
|
|
||
| # Save figure | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file) | ||
| plt.close(fig) | ||
|
|
||
| # Return visualization metadata | ||
| return { | ||
| "title": "Governance Participation", | ||
| "path": chart_file, | ||
| "description": "Voting participation across governance proposals.", | ||
| } | ||
| except Exception as e: | ||
| logger.error(f"Error generating governance visualization: {e}") | ||
|
|
||
| return None |
There was a problem hiding this comment.
❌ New issue: Code Duplication
The module contains 2 functions with similar structure: _create_governance_visualization,_create_votes_visualization
| def _create_votes_visualization( | ||
| protocol: str, votes_data: List[Dict[str, Any]], viz_dir: str, timestamp: str | ||
| ) -> Optional[Dict[str, str]]: | ||
| """Create visualization for votes data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| votes_data: Votes data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Visualization metadata or None if failed | ||
| """ | ||
| try: | ||
| if not votes_data: | ||
| return None | ||
|
|
||
| # Create votes distribution chart | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_votes_{timestamp}.png") | ||
|
|
||
| # Group votes by proposal | ||
| proposals = {} | ||
| for vote in votes_data: | ||
| if "proposal_id" in vote and "voter" in vote and "support" in vote: | ||
| proposal_id = vote["proposal_id"] | ||
| if proposal_id not in proposals: | ||
| proposals[proposal_id] = {"for": 0, "against": 0} | ||
|
|
||
| if vote["support"]: | ||
| proposals[proposal_id]["for"] += 1 | ||
| else: | ||
| proposals[proposal_id]["against"] += 1 | ||
|
|
||
| if proposals: | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
|
|
||
| proposal_ids = list(proposals.keys()) | ||
| for_votes = [proposals[p]["for"] for p in proposal_ids] | ||
| against_votes = [proposals[p]["against"] for p in proposal_ids] | ||
|
|
||
| # Width of the bars | ||
| width = 0.3 | ||
|
|
||
| # Position of bars on x-axis | ||
| ind = np.arange(len(proposal_ids)) | ||
|
|
||
| # Creating bars | ||
| ax.bar(ind - width / 2, for_votes, width, label="For") | ||
| ax.bar(ind + width / 2, against_votes, width, label="Against") | ||
|
|
||
| # Labels and title | ||
| ax.set_xlabel("Proposal ID") | ||
| ax.set_ylabel("Vote Count") | ||
| ax.set_title(f"{protocol.upper()} Vote Distribution") | ||
| ax.set_xticks(ind) | ||
| ax.set_xticklabels(proposal_ids) | ||
| ax.legend() | ||
|
|
||
| # Save figure | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file) | ||
| plt.close(fig) | ||
|
|
||
| # Return visualization metadata | ||
| return { | ||
| "title": "Vote Distribution", | ||
| "path": chart_file, | ||
| "description": "Distribution of votes across governance proposals.", | ||
| } | ||
| except Exception as e: | ||
| logger.error(f"Error generating votes visualization: {e}") | ||
|
|
||
| return None |
There was a problem hiding this comment.
❌ New issue: Complex Method
_create_votes_visualization has a cyclomatic complexity of 13, threshold = 9
| def _create_governance_visualization( | ||
| protocol: str, governance_data: List[Dict[str, Any]], viz_dir: str, timestamp: str | ||
| ) -> Optional[Dict[str, str]]: | ||
| """Create visualization for governance data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| governance_data: Governance data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Visualization metadata or None if failed | ||
| """ | ||
| try: | ||
| if not governance_data: | ||
| return None | ||
|
|
||
| # Create governance participation chart | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_governance_{timestamp}.png") | ||
|
|
||
| # Extract participation data | ||
| proposals_data = [] | ||
| for proposal in governance_data: | ||
| if "id" in proposal and "for_votes" in proposal and "against_votes" in proposal: | ||
| proposals_data.append( | ||
| { | ||
| "id": proposal["id"], | ||
| "for": float(proposal.get("for_votes", 0)), | ||
| "against": float(proposal.get("against_votes", 0)), | ||
| } | ||
| ) | ||
|
|
||
| if proposals_data: | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
|
|
||
| proposal_ids = [p["id"] for p in proposals_data] | ||
| for_votes = [p["for"] for p in proposals_data] | ||
| against_votes = [p["against"] for p in proposals_data] | ||
|
|
||
| # Width of the bars | ||
| width = 0.3 | ||
|
|
||
| # Position of bars on x-axis | ||
| ind = np.arange(len(proposal_ids)) | ||
|
|
||
| # Creating bars | ||
| ax.bar(ind - width / 2, for_votes, width, label="For") | ||
| ax.bar(ind + width / 2, against_votes, width, label="Against") | ||
|
|
||
| # Labels and title | ||
| ax.set_xlabel("Proposal ID") | ||
| ax.set_ylabel("Votes") | ||
| ax.set_title(f"{protocol.upper()} Governance Participation") | ||
| ax.set_xticks(ind) | ||
| ax.set_xticklabels(proposal_ids) | ||
| ax.legend() | ||
|
|
||
| # Save figure | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file) | ||
| plt.close(fig) | ||
|
|
||
| # Return visualization metadata | ||
| return { | ||
| "title": "Governance Participation", | ||
| "path": chart_file, | ||
| "description": "Voting participation across governance proposals.", | ||
| } | ||
| except Exception as e: | ||
| logger.error(f"Error generating governance visualization: {e}") | ||
|
|
||
| return None |
There was a problem hiding this comment.
❌ New issue: Complex Method
_create_governance_visualization has a cyclomatic complexity of 11, threshold = 9
| def _create_token_distribution_visualization( | ||
| protocol: str, data: Dict[str, Any], viz_dir: str, timestamp: str | ||
| ) -> Optional[Dict[str, str]]: | ||
| """Create visualization for token distribution. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| data: Protocol data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Visualization metadata or None if failed | ||
| """ | ||
| try: | ||
| if "token_holders" not in data or not data["token_holders"]: | ||
| return None | ||
|
|
||
| # Prepare data for visualization | ||
| token_holders = data["token_holders"] | ||
| holders_data = [] | ||
| for holder in token_holders[:20]: # Top 20 holders | ||
| if "TokenHolderAddress" in holder and "TokenHolderQuantity" in holder: | ||
| holders_data.append( | ||
| { | ||
| "address": holder["TokenHolderAddress"], | ||
| "balance": float(holder["TokenHolderQuantity"]), | ||
| } | ||
| ) | ||
|
|
||
| # Create distribution chart if we have data | ||
| if holders_data: | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_distribution_{timestamp}.png") | ||
|
|
||
| # Extract data for plotting | ||
| addresses = [h["address"] for h in holders_data] | ||
| balances = [h["balance"] for h in holders_data] | ||
|
|
||
| # Create the chart | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
| ax.bar(range(len(addresses)), balances) | ||
| ax.set_title(f"{protocol.upper()} Token Distribution") | ||
| ax.set_xlabel("Holder Rank") | ||
| ax.set_ylabel("Token Balance") | ||
| ax.set_xticks([]) # Hide x-axis labels as they would be too crowded | ||
|
|
||
| # Save the chart | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file) | ||
| plt.close(fig) | ||
|
|
||
| # Return visualization metadata | ||
| return { | ||
| "title": "Token Distribution", | ||
| "path": chart_file, | ||
| "description": "Distribution of tokens among top token holders.", | ||
| } | ||
| except Exception as e: | ||
| logger.error(f"Error generating token distribution visualization: {e}") | ||
|
|
||
| return None |
There was a problem hiding this comment.
❌ New issue: Complex Method
_create_token_distribution_visualization has a cyclomatic complexity of 10, threshold = 9
| def create_visualizations_section( | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| ) -> Tuple[str, List[Dict[str, str]]]: | ||
| """Create the visualizations section of the report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Tuple of (HTML string for the visualizations section, List of visualizations) | ||
| """ | ||
| visualizations = [] | ||
| html_content = """ | ||
| <div class="section"> | ||
| <h2>Visualizations</h2> | ||
| """ | ||
|
|
||
| # Create token distribution visualization | ||
| if current_data and "token_holders" in current_data: | ||
| distribution_viz = _create_token_distribution_visualization(protocol, current_data, viz_dir, timestamp) | ||
| if distribution_viz: | ||
| visualizations.append(distribution_viz) | ||
|
|
||
| # Create governance visualization | ||
| if governance_data: | ||
| governance_viz = _create_governance_visualization(protocol, governance_data, viz_dir, timestamp) | ||
| if governance_viz: | ||
| visualizations.append(governance_viz) | ||
|
|
||
| # Create votes visualization | ||
| if votes_data: | ||
| votes_viz = _create_votes_visualization(protocol, votes_data, viz_dir, timestamp) | ||
| if votes_viz: | ||
| visualizations.append(votes_viz) | ||
|
|
||
| # Add visualizations to HTML content | ||
| for viz in visualizations: | ||
| viz_path = os.path.basename(viz["path"]) | ||
| html_content += f""" | ||
| <div class="visualization"> | ||
| <h3>{viz["title"]}</h3> | ||
| <img src="{viz_path}" alt="{viz["title"]}"> | ||
| <p>{viz["description"]}</p> | ||
| </div> | ||
| """ | ||
|
|
||
| html_content += "</div>\n" | ||
| return html_content, visualizations |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_visualizations_section has a cyclomatic complexity of 9, threshold = 9
| # Group votes by proposal | ||
| proposals = {} | ||
| for vote in votes_data: | ||
| if "proposal_id" in vote and "voter" in vote and "support" in vote: |
There was a problem hiding this comment.
❌ New issue: Complex Conditional
_create_votes_visualization has 1 complex conditionals with 2 branches, threshold = 2
| def create_visualizations_section( | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| ) -> Tuple[str, List[Dict[str, str]]]: | ||
| """Create the visualizations section of the report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Tuple of (HTML string for the visualizations section, List of visualizations) | ||
| """ | ||
| visualizations = [] | ||
| html_content = """ | ||
| <div class="section"> | ||
| <h2>Visualizations</h2> | ||
| """ | ||
|
|
||
| # Create token distribution visualization | ||
| if current_data and "token_holders" in current_data: | ||
| distribution_viz = _create_token_distribution_visualization(protocol, current_data, viz_dir, timestamp) | ||
| if distribution_viz: | ||
| visualizations.append(distribution_viz) | ||
|
|
||
| # Create governance visualization | ||
| if governance_data: | ||
| governance_viz = _create_governance_visualization(protocol, governance_data, viz_dir, timestamp) | ||
| if governance_viz: | ||
| visualizations.append(governance_viz) | ||
|
|
||
| # Create votes visualization | ||
| if votes_data: | ||
| votes_viz = _create_votes_visualization(protocol, votes_data, viz_dir, timestamp) | ||
| if votes_viz: | ||
| visualizations.append(votes_viz) | ||
|
|
||
| # Add visualizations to HTML content | ||
| for viz in visualizations: | ||
| viz_path = os.path.basename(viz["path"]) | ||
| html_content += f""" | ||
| <div class="visualization"> | ||
| <h3>{viz["title"]}</h3> | ||
| <img src="{viz_path}" alt="{viz["title"]}"> | ||
| <p>{viz["description"]}</p> | ||
| </div> | ||
| """ | ||
|
|
||
| html_content += "</div>\n" | ||
| return html_content, visualizations |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
create_visualizations_section has 3 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def _generate_comprehensive_html_report( | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| historical_data: Optional[Dict[str, Any]], | ||
| output_path: str, | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| ) -> str: | ||
| """Generate a comprehensive HTML report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| historical_data: Historical data dictionary | ||
| output_path: Path to save the report | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Path to the generated report | ||
| """ | ||
| # Create HTML content | ||
| html_content = create_comprehensive_report_header(protocol) | ||
|
|
||
| # Extract metrics | ||
| metrics_data = _extract_metrics(current_data) if current_data else [] | ||
| html_content += _create_metrics_section(metrics_data) | ||
|
|
||
| # Create visualizations | ||
| visualizations_html, visualizations = create_visualizations_section( | ||
| protocol, current_data, governance_data, votes_data, viz_dir, timestamp | ||
| ) | ||
| html_content += visualizations_html | ||
|
|
||
| # Add historical section if available | ||
| if historical_data: | ||
| from .historical_report_generator import create_time_series_section, create_snapshots_section | ||
|
|
||
| if "time_series" in historical_data: | ||
| html_content += create_time_series_section(protocol, historical_data["time_series"], viz_dir, timestamp) | ||
|
|
||
| if "snapshots" in historical_data: | ||
| html_content += create_snapshots_section(protocol, historical_data["snapshots"]) | ||
|
|
||
| # Add governance section | ||
| html_content += _create_governance_section(governance_data, votes_data) | ||
|
|
||
| # Add conclusion | ||
| html_content += """ | ||
| <div class="section"> | ||
| <h2>Conclusion</h2> | ||
| <p>This comprehensive analysis provides insights into the governance token distribution and participation metrics. | ||
| The data shows patterns in token holder distribution and governance participation that can inform decision-making.</p> | ||
| </div> | ||
|
|
||
| <div class="footer"> | ||
| <p>Generated using Governance Token Distribution Analyzer</p> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| """ | ||
|
|
||
| # Write to file | ||
| with open(output_path, "w") as f: | ||
| f.write(html_content) | ||
|
|
||
| return output_path |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
_generate_comprehensive_html_report has 8 arguments, threshold = 4
| def create_visualizations_section( | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| ) -> Tuple[str, List[Dict[str, str]]]: | ||
| """Create the visualizations section of the report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Tuple of (HTML string for the visualizations section, List of visualizations) | ||
| """ | ||
| visualizations = [] | ||
| html_content = """ | ||
| <div class="section"> | ||
| <h2>Visualizations</h2> | ||
| """ | ||
|
|
||
| # Create token distribution visualization | ||
| if current_data and "token_holders" in current_data: | ||
| distribution_viz = _create_token_distribution_visualization(protocol, current_data, viz_dir, timestamp) | ||
| if distribution_viz: | ||
| visualizations.append(distribution_viz) | ||
|
|
||
| # Create governance visualization | ||
| if governance_data: | ||
| governance_viz = _create_governance_visualization(protocol, governance_data, viz_dir, timestamp) | ||
| if governance_viz: | ||
| visualizations.append(governance_viz) | ||
|
|
||
| # Create votes visualization | ||
| if votes_data: | ||
| votes_viz = _create_votes_visualization(protocol, votes_data, viz_dir, timestamp) | ||
| if votes_viz: | ||
| visualizations.append(votes_viz) | ||
|
|
||
| # Add visualizations to HTML content | ||
| for viz in visualizations: | ||
| viz_path = os.path.basename(viz["path"]) | ||
| html_content += f""" | ||
| <div class="visualization"> | ||
| <h3>{viz["title"]}</h3> | ||
| <img src="{viz_path}" alt="{viz["title"]}"> | ||
| <p>{viz["description"]}</p> | ||
| </div> | ||
| """ | ||
|
|
||
| html_content += "</div>\n" | ||
| return html_content, visualizations |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
create_visualizations_section has 6 arguments, threshold = 4
| @@ -0,0 +1,717 @@ | |||
| #!/usr/bin/env python | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.81 across 16 functions. The mean complexity threshold is 4
There was a problem hiding this comment.
Hey @uelkerd - I've reviewed your changes - here's some feedback:
- The new visualization_processor.py is only a docstring but is imported by DataProcessor—please implement the VisualizationProcessor class or remove the import to prevent runtime errors.
- The standardize*_holders functions in data_standardizer.py are identical; consider merging them into one generic standardizer to reduce almost-duplicate code.
- This PR refactors three large modules at once—consider splitting future file-size refactors into smaller, package-by-package PRs for easier review and isolation of issues.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new visualization_processor.py is only a docstring but is imported by DataProcessor—please implement the VisualizationProcessor class or remove the import to prevent runtime errors.
- The _standardize_*_holders functions in data_standardizer.py are identical; consider merging them into one generic standardizer to reduce almost-duplicate code.
- This PR refactors three large modules at once—consider splitting future file-size refactors into smaller, package-by-package PRs for easier review and isolation of issues.
## Individual Comments
### Comment 1
<location> `src/governance_token_analyzer/core/api_client.py:3` </location>
<code_context>
-This module provides a unified interface for fetching governance token data
</code_context>
<issue_to_address>
The file is now a thin compatibility wrapper, but the docstring should clarify deprecation or migration intent.
Please specify if this file is deprecated and provide a timeline for its removal to guide users in migrating.
</issue_to_address>
### Comment 2
<location> `src/governance_token_analyzer/core/data_processor.py:1` </location>
<code_context>
-"""Data Processor Module for standardizing data across different protocols.
</code_context>
<issue_to_address>
The new docstring is less descriptive than the original.
Please clarify if the reduced detail is intentional, as it may impact future maintainers' understanding.
</issue_to_address>
### Comment 3
<location> `src/governance_token_analyzer/visualization/report_generator/html_report_generator.py:13` </location>
<code_context>
+from datetime import datetime
+from typing import Any, Dict, List, Optional, Tuple
+
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
</code_context>
<issue_to_address>
Direct use of matplotlib for file output may cause issues in headless environments.
Explicitly set a non-interactive backend (e.g., 'Agg') at the top of the module to ensure compatibility in headless environments.
</issue_to_address>
### Comment 4
<location> `src/governance_token_analyzer/core/data_processor/metrics_processor.py:129` </location>
<code_context>
+ abstain_votes = int(proposal.get("abstainVotes", 0))
+ total_proposal_votes = for_votes + against_votes + abstain_votes
+
+ # Assuming total_supply is available in the proposal or a constant
+ # Here we use a placeholder value
+ total_supply = 1000000 # This should be replaced with actual total supply
+
+ participation_rate = (total_proposal_votes / total_supply) * 100
</code_context>
<issue_to_address>
Hardcoding total_supply to 1,000,000 in participation rate calculation may lead to inaccurate metrics.
Consider passing the actual total supply as an argument or retrieving it from the proposal or protocol context to ensure accurate participation rate calculations.
</issue_to_address>
### Comment 5
<location> `src/governance_token_analyzer/core/data_processor/metrics_processor.py:225` </location>
<code_context>
+ # Calculate cumulative sum
+ cumsum = np.cumsum(sorted_balances)
+
+ # Calculate Gini coefficient using the formula:
+ # G = 1 - (2/n) * sum_{i=1}^{n} ((n+1-i)/n) * (x_i/sum(x))
+ # where x_i are the sorted balances
+ total = sum(sorted_balances)
+ gini = 1 - 2 * sum((n + 1 - i) * balance for i, balance in enumerate(sorted_balances, 1)) / (n * total)
+
+ return gini
</code_context>
<issue_to_address>
The Gini coefficient formula used here is not the most numerically stable for large n.
Consider using a numpy-based or vectorized implementation for better numerical stability and performance with large datasets, or document any input size limitations.
</issue_to_address>
### Comment 6
<location> `src/governance_token_analyzer/core/api_client/graph_client.py:49` </location>
<code_context>
+ current_time = time.time()
+ time_since_last_request = current_time - self.last_request_time
+
+ if time_since_last_request < self.min_request_interval:
+ sleep_time = self.min_request_interval - time_since_last_request
+ time.sleep(sleep_time)
+
+ self.last_request_time = time.time()
</code_context>
<issue_to_address>
Rate limiting is implemented with time.sleep, which may block the main thread.
Consider using a non-blocking rate limiter or making the client async-compatible if it will be used in async or multi-threaded environments.
Suggested implementation:
```python
# Implement non-blocking, thread-safe rate limiting
with self._rate_limit_lock:
current_time = time.time()
time_since_last_request = current_time - self.last_request_time
if time_since_last_request < self.min_request_interval:
sleep_time = self.min_request_interval - time_since_last_request
rate_limit_event = getattr(self, "_rate_limit_event", None)
if rate_limit_event is None:
rate_limit_event = threading.Event()
self._rate_limit_event = rate_limit_event
rate_limit_event.clear()
# Release the lock while waiting
self._rate_limit_lock.release()
rate_limit_event.wait(timeout=sleep_time)
self._rate_limit_lock.acquire()
self.last_request_time = time.time()
```
- You must add `import threading` at the top of the file if it is not already present.
- You must initialize `self._rate_limit_lock = threading.Lock()` in your class's `__init__` method.
- This approach is thread-safe and non-blocking for other threads, but if you want true async compatibility, you should refactor the method to be async and use `await asyncio.sleep(...)` instead of threading primitives.
</issue_to_address>
### Comment 7
<location> `src/governance_token_analyzer/core/api_client/response_parser.py:47` </location>
<code_context>
+ "balance": float(holder.get("balance", 0)) / 10**18, # Convert from wei
+ "percentage": float(holder.get("share", 0)),
+ })
+ elif api_type == "alchemy":
+ # Alchemy doesn't currently have a token holders endpoint
+ # This is a placeholder for when Alchemy adds this feature
+ pass
</code_context>
<issue_to_address>
Alchemy support is stubbed but not implemented, which may cause silent failures.
Raise a NotImplementedError or log a clear warning when Alchemy support is triggered to prevent silent failures.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
elif api_type == "alchemy":
# Alchemy doesn't currently have a token holders endpoint
# This is a placeholder for when Alchemy adds this feature
pass
=======
elif api_type == "alchemy":
# Alchemy doesn't currently have a token holders endpoint
# This is a placeholder for when Alchemy adds this feature
logger.warning("Alchemy API type selected, but token holders endpoint is not implemented.")
raise NotImplementedError("Token holders parsing for Alchemy is not implemented yet.")
>>>>>>> REPLACE
</suggested_fix>
### Comment 8
<location> `src/governance_token_analyzer/core/api_client/data_fetcher.py:56` </location>
<code_context>
+ def normalize_holder_balances(self, holders: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
</code_context>
<issue_to_address>
normalize_holder_balances does not handle missing or malformed addresses.
Validate that the 'address' field is a non-empty string and matches the expected Ethereum address format to prevent downstream errors.
</issue_to_address>
### Comment 9
<location> `src/governance_token_analyzer/visualization/report_generator/historical_report_generator.py:96` </location>
<code_context>
+ # Create HTML content
+ html_content = create_historical_report_header(protocol)
+
+ # Process time series data
+ if "time_series" in historical_data:
+ html_content += create_time_series_section(protocol, historical_data["time_series"], viz_dir, timestamp)
+
</code_context>
<issue_to_address>
No handling for missing or malformed time_series data in historical_data.
Add checks to ensure 'time_series' is the expected type before processing.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
# Create HTML content
html_content = create_historical_report_header(protocol)
# Process time series data
if "time_series" in historical_data:
html_content += create_time_series_section(protocol, historical_data["time_series"], viz_dir, timestamp)
=======
# Create HTML content
html_content = create_historical_report_header(protocol)
# Process time series data with validation
time_series = historical_data.get("time_series")
if time_series is not None:
if isinstance(time_series, (list, dict)): # Adjust type as needed
html_content += create_time_series_section(protocol, time_series, viz_dir, timestamp)
else:
logging.warning("Expected 'time_series' to be a list or dict, got %s", type(time_series).__name__)
else:
logging.info("'time_series' key not found in historical_data; skipping time series section.")
>>>>>>> REPLACE
</suggested_fix>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| This module provides a unified interface for fetching governance token data | ||
| from various blockchain APIs including Etherscan, The Graph, and Alchemy. | ||
| """ |
There was a problem hiding this comment.
suggestion: The file is now a thin compatibility wrapper, but the docstring should clarify deprecation or migration intent.
Please specify if this file is deprecated and provide a timeline for its removal to guide users in migrating.
| @@ -1,133 +1,12 @@ | |||
| """Data Processor Module for standardizing data across different protocols. | |||
There was a problem hiding this comment.
question: The new docstring is less descriptive than the original.
Please clarify if the reduced detail is intentional, as it may impact future maintainers' understanding.
| from datetime import datetime | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
| import matplotlib.pyplot as plt |
There was a problem hiding this comment.
issue (bug_risk): Direct use of matplotlib for file output may cause issues in headless environments.
Explicitly set a non-interactive backend (e.g., 'Agg') at the top of the module to ensure compatibility in headless environments.
| # Assuming total_supply is available in the proposal or a constant | ||
| # Here we use a placeholder value | ||
| total_supply = 1000000 # This should be replaced with actual total supply |
There was a problem hiding this comment.
issue (bug_risk): Hardcoding total_supply to 1,000,000 in participation rate calculation may lead to inaccurate metrics.
Consider passing the actual total supply as an argument or retrieving it from the proposal or protocol context to ensure accurate participation rate calculations.
| # Calculate Gini coefficient using the formula: | ||
| # G = 1 - (2/n) * sum_{i=1}^{n} ((n+1-i)/n) * (x_i/sum(x)) | ||
| # where x_i are the sorted balances | ||
| total = sum(sorted_balances) | ||
| gini = 1 - 2 * sum((n + 1 - i) * balance for i, balance in enumerate(sorted_balances, 1)) / (n * total) |
There was a problem hiding this comment.
suggestion: The Gini coefficient formula used here is not the most numerically stable for large n.
Consider using a numpy-based or vectorized implementation for better numerical stability and performance with large datasets, or document any input size limitations.
| logger.error(f"Error fetching governance votes for {protocol} proposal {proposal_id}: {e}") | ||
| return self._generate_sample_vote_data(protocol, proposal_id) | ||
|
|
||
| def _generate_sample_proposal_data(self, protocol: str, count: int) -> List[Dict[str, Any]]: |
There was a problem hiding this comment.
issue (code-quality): Low code quality found in ProtocolClient._generate_sample_proposal_data - 7% (low-code-quality)
Explanation
The quality score for this function is below the quality threshold of 25%.This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines. - Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.
|
|
||
| # Generate votes | ||
| votes = [] | ||
| for i in range(vote_count): |
There was a problem hiding this comment.
issue (code-quality): We've found these issues:
- Replace unused for index with underscore (
for-index-underscore) - Inline variable that is immediately returned (
inline-immediately-returned-variable)
| holders = [] | ||
|
|
||
| try: | ||
| if api_type == "etherscan": |
There was a problem hiding this comment.
issue (code-quality): We've found these issues:
- Simplify conditional into switch-like form [×2] (
switch) - Replace a for append loop with list extend (
for-append-to-extend) - Remove redundant pass statement (
remove-redundant-pass)
| Token supply as an integer | ||
| """ | ||
| try: | ||
| if api_type == "etherscan": |
There was a problem hiding this comment.
issue (code-quality): Remove empty elif clause (remove-pass-elif)
There was a problem hiding this comment.
Code Review
The pull request refactors the api_client.py and data_processor.py files into packages to enforce a 500-line file size limit, improving maintainability. The changes include creating new modules for different functionalities and updating imports accordingly. The refactoring plan is documented in docs/refactoring_plan.md.
| This file is maintained for backward compatibility and imports from the api_client package. | ||
| """ |
There was a problem hiding this comment.
Consider adding a docstring to explain why this file is maintained and what the implications are for backward compatibility.
'''
API Client for Governance Token Distribution Analyzer.
This module provides a unified interface for fetching governance token data
from various blockchain APIs including Etherscan, The Graph, and Alchemy.
This file is maintained for backward compatibility and imports from the api_client package.
'''| self.etherscan_api_key = os.getenv("ETHERSCAN_API_KEY", DEFAULT_ETHERSCAN_API_KEY) | ||
| self.alchemy_api_key = os.getenv("ALCHEMY_API_KEY", DEFAULT_ALCHEMY_API_KEY) | ||
| self.graph_api_key = os.getenv("GRAPH_API_KEY", DEFAULT_GRAPH_API_KEY) | ||
| self.moralis_api_key = os.getenv("MORALIS_API_KEY", "") | ||
| self.ethplorer_api_key = os.getenv("ETHPLORER_API_KEY", DEFAULT_ETHPLORER_API_KEY) |
There was a problem hiding this comment.
Consider using a dictionary to store the API keys and iterate through them to log their availability. This would reduce code duplication and improve maintainability.
api_keys = {
"Etherscan": self.etherscan_api_key,
"Alchemy": self.alchemy_api_key,
"The Graph": self.graph_api_key,
"Moralis": self.moralis_api_key,
"Ethplorer": self.ethplorer_api_key,
}
logger.info("APIClient initialized with available API keys:")
for name, key in api_keys.items():
logger.info(f" {name}: {{'✓' if key else '✗'}}")| logger.warning(f"No token data provided for {protocol}") | ||
| return {} |
| logger.warning(f"No protocol data provided for {protocol}") | ||
| return {} |
There was a problem hiding this comment.
Hey @uelkerd - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `src/governance_token_analyzer/core/data_processor/visualization_processor.py:1` </location>
<code_context>
+"""Visualization Processor for Governance Token Distribution Analyzer."""
</code_context>
<issue_to_address>
VisualizationProcessor is only a stub and lacks implementation.
If this is meant as a placeholder, define an empty class to prevent import issues. Otherwise, implement the class or remove the file until it's ready.
</issue_to_address>
### Comment 2
<location> `src/governance_token_analyzer/core/data_processor/data_processor_base.py:19` </location>
<code_context>
+
+ def __init__(self):
+ """Initialize the data processor."""
+ self.metrics_processor = None
+ self.visualization_processor = None
+ self.report_processor = None
+
+ # Initialize processors lazily when needed
+ self._initialize_processors()
+
+ def _initialize_processors(self):
</code_context>
<issue_to_address>
Eager initialization of processors may be unnecessary if not all are used.
Initializing all sub-processors in the constructor may lead to unnecessary resource usage if some are never used. Lazy initialization would help optimize imports and resource consumption.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
def __init__(self):
"""Initialize the data processor."""
self.metrics_processor = None
self.visualization_processor = None
self.report_processor = None
# Initialize processors lazily when needed
self._initialize_processors()
def _initialize_processors(self):
"""Initialize the specialized processors."""
from .metrics_processor import MetricsProcessor
from .visualization_processor import VisualizationProcessor
from .report_processor import ReportProcessor
self.metrics_processor = MetricsProcessor()
self.visualization_processor = VisualizationProcessor()
self.report_processor = ReportProcessor()
=======
def __init__(self):
"""Initialize the data processor."""
self._metrics_processor = None
self._visualization_processor = None
self._report_processor = None
@property
def metrics_processor(self):
if self._metrics_processor is None:
from .metrics_processor import MetricsProcessor
self._metrics_processor = MetricsProcessor()
return self._metrics_processor
@property
def visualization_processor(self):
if self._visualization_processor is None:
from .visualization_processor import VisualizationProcessor
self._visualization_processor = VisualizationProcessor()
return self._visualization_processor
@property
def report_processor(self):
if self._report_processor is None:
from .report_processor import ReportProcessor
self._report_processor = ReportProcessor()
return self._report_processor
>>>>>>> REPLACE
</suggested_fix>
### Comment 3
<location> `src/governance_token_analyzer/core/data_processor/data_processor_base.py:128` </location>
<code_context>
+ # Process governance data
+ governance_result = self.process_governance_data(protocol, {"governance_proposals": proposals})
+
+ # Combine metrics
+ combined_metrics = {
+ **distribution_result.get("metrics", {}),
+ **governance_result.get("metrics", {}),
+ }
+
+ # Generate visualizations
+ visualizations = self.visualization_processor.generate_visualizations(
+ protocol, token_holders, proposals, combined_metrics
</code_context>
<issue_to_address>
Combining metrics with dictionary unpacking may silently overwrite keys.
If both metric dictionaries share keys, governance_result values will overwrite distribution_result values. Use explicit merging or namespacing if you want to avoid this.
Suggested implementation:
```python
# Combine metrics with namespacing to avoid key collisions
combined_metrics = {
"distribution_metrics": distribution_result.get("metrics", {}),
"governance_metrics": governance_result.get("metrics", {}),
}
```
```python
# Generate visualizations
visualizations = self.visualization_processor.generate_visualizations(
protocol, token_holders, proposals, combined_metrics
)
```
</issue_to_address>
### Comment 4
<location> `src/governance_token_analyzer/core/data_processor/data_processor_base.py:182` </location>
<code_context>
+ logger.warning("No protocol data provided for comparison")
+ return {}
+
+ # Extract metrics for each protocol
+ protocol_metrics = {}
+ for protocol, data in protocol_data.items():
+ protocol_metrics[protocol] = data.get("metrics", {})
+
+ # Calculate comparison metrics
+ comparison_metrics = self.metrics_processor.calculate_comparison_metrics(protocol_metrics)
+
</code_context>
<issue_to_address>
No check for empty or missing metrics in protocol_data.
If 'metrics' is missing, protocol_metrics[protocol] becomes an empty dict, which could affect downstream comparisons. Please add validation or set appropriate defaults.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
# Extract metrics for each protocol
protocol_metrics = {}
for protocol, data in protocol_data.items():
protocol_metrics[protocol] = data.get("metrics", {})
=======
# Extract metrics for each protocol with validation
protocol_metrics = {}
for protocol, data in protocol_data.items():
metrics = data.get("metrics")
if not metrics:
logger.warning(f"Missing or empty 'metrics' for protocol '{protocol}'. Skipping this protocol.")
continue
protocol_metrics[protocol] = metrics
>>>>>>> REPLACE
</suggested_fix>
### Comment 5
<location> `src/governance_token_analyzer/core/data_processor/metrics_processor.py:66` </location>
<code_context>
+ else (sorted_balances[holder_count // 2 - 1] + sorted_balances[holder_count // 2]) / 2
+ )
+
+ # Calculate Gini coefficient
+ gini = self._calculate_gini_coefficient(balances)
+
+ # Calculate top holder concentrations
</code_context>
<issue_to_address>
The Gini coefficient implementation may not handle negative or zero balances robustly.
Filter out negative balances and check that the sum is positive before calculating the Gini coefficient to avoid incorrect results or division by zero.
</issue_to_address>
### Comment 6
<location> `src/governance_token_analyzer/core/data_processor/metrics_processor.py:123` </location>
<code_context>
+ executed_count = sum(1 for p in proposals if p.get("executed", False))
+ success_rate = (executed_count / proposal_count) * 100 if proposal_count > 0 else 0
+
+ # Calculate average participation rate
+ participation_rates = []
+ for proposal in proposals:
+ for_votes = int(proposal.get("forVotes", 0))
+ against_votes = int(proposal.get("againstVotes", 0))
+ abstain_votes = int(proposal.get("abstainVotes", 0))
+ total_proposal_votes = for_votes + against_votes + abstain_votes
+
+ # Assuming total_supply is available in the proposal or a constant
+ # Here we use a placeholder value
+ total_supply = 1000000 # This should be replaced with actual total supply
+
+ participation_rate = (total_proposal_votes / total_supply) * 100
</code_context>
<issue_to_address>
Participation rate uses a hardcoded total supply value, which may not reflect the actual protocol supply.
Passing the actual total supply from protocol data or proposals will improve the accuracy of participation calculations.
</issue_to_address>
### Comment 7
<location> `src/governance_token_analyzer/core/api_client/graph_client.py:28` </location>
<code_context>
+ """
+ self.subgraph_url = subgraph_url
+ self.session = requests.Session()
+ self.session.headers.update(
+ {
+ "Content-Type": "application/json",
+ "User-Agent": "GovernanceTokenAnalyzer/1.0",
+ }
+ )
+ self.last_request_time = 0
</code_context>
<issue_to_address>
The Graph client sets a static User-Agent, which may be blocked or rate-limited by some endpoints.
Consider making the User-Agent configurable to help avoid rate-limiting or blocking by certain endpoints.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
def __init__(self, subgraph_url: str):
"""Initialize The Graph API client.
Args:
subgraph_url: URL of the subgraph to query
"""
self.subgraph_url = subgraph_url
self.session = requests.Session()
self.session.headers.update(
{
"Content-Type": "application/json",
"User-Agent": "GovernanceTokenAnalyzer/1.0",
}
)
self.last_request_time = 0
self.min_request_interval = 0.5 # 500ms between requests
=======
def __init__(self, subgraph_url: str, user_agent: str = "GovernanceTokenAnalyzer/1.0"):
"""Initialize The Graph API client.
Args:
subgraph_url: URL of the subgraph to query
user_agent: User-Agent string to use for requests (default: "GovernanceTokenAnalyzer/1.0")
"""
self.subgraph_url = subgraph_url
self.session = requests.Session()
self.session.headers.update(
{
"Content-Type": "application/json",
"User-Agent": user_agent,
}
)
self.last_request_time = 0
self.min_request_interval = 0.5 # 500ms between requests
>>>>>>> REPLACE
</suggested_fix>
### Comment 8
<location> `src/governance_token_analyzer/core/api_client/graph_client.py:34` </location>
<code_context>
- self.moralis_api_key = os.getenv("MORALIS_API_KEY") # New API key
-
- # Rate limiting
- self.last_request_time = 0
- self.min_request_interval = 0.2 # 200ms between requests
-
</code_context>
<issue_to_address>
The rate limiting is implemented per client instance, which may not be effective if multiple instances are created.
Multiple instances won't share rate limits, which could cause API overuse. Consider implementing a shared rate limiter if this scenario is likely.
Suggested implementation:
```python
import logging
import time
import threading
from typing import Any, Dict, Optional
import requests
```
```python
self.session = requests.Session()
self.session.headers.update(
{
"Content-Type": "application/json",
"User-Agent": "GovernanceTokenAnalyzer/1.0",
}
)
# Shared rate limiting variables
_last_request_time = 0
_min_request_interval = 0.2 # 200ms between requests
_rate_limit_lock = threading.Lock()
```
You will also need to update any method that performs a request to use the class-level rate limiting, for example:
```python
with self.__class__._rate_limit_lock:
now = time.time()
elapsed = now - self.__class__._last_request_time
if elapsed < self.__class__._min_request_interval:
time.sleep(self.__class__._min_request_interval - elapsed)
self.__class__._last_request_time = time.time()
```
This should be placed immediately before making an API request, replacing any previous instance-level rate limiting logic.
</issue_to_address>
### Comment 9
<location> `src/governance_token_analyzer/core/api_client/response_parser.py:51` </location>
<code_context>
+ "percentage": float(holder.get("share", 0)),
+ }
+ )
+ elif api_type == "alchemy":
+ # Alchemy doesn't currently have a token holders endpoint
+ # This is a placeholder for when Alchemy adds this feature
+ pass
+ else:
+ logger.warning(f"Unknown API type: {api_type}")
</code_context>
<issue_to_address>
Alchemy parsing is a placeholder and may cause silent failures if used.
Consider raising a NotImplementedError or logging a warning if this code path is executed to avoid silent failures.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
elif api_type == "alchemy":
# Alchemy doesn't currently have a token holders endpoint
# This is a placeholder for when Alchemy adds this feature
pass
=======
elif api_type == "alchemy":
# Alchemy doesn't currently have a token holders endpoint
logger.warning("Alchemy API type selected, but token holders endpoint is not implemented.")
raise NotImplementedError("Token holders parsing for Alchemy API is not implemented yet.")
>>>>>>> REPLACE
</suggested_fix>
### Comment 10
<location> `src/governance_token_analyzer/core/data_processor/data_standardizer.py:29` </location>
<code_context>
-
- """
- # Handle different protocol data structures
- if protocol_name.lower() == "compound":
- df = _standardize_compound_holders(holder_data)
- elif protocol_name.lower() == "uniswap":
- df = _standardize_uniswap_holders(holder_data)
- elif protocol_name.lower() == "aave":
- df = _standardize_aave_holders(holder_data)
- else:
- raise ValueError(f"Unsupported protocol: {protocol_name}")
-
- # Add protocol column
</code_context>
<issue_to_address>
Standardization functions for each protocol are nearly identical and could be unified.
These functions share the same logic; refactor them into a single, reusable function to eliminate duplication.
Suggested implementation:
```python
# Standardize holder data for supported protocols
df = _standardize_holders(holder_data, protocol_name)
```
```python
def _standardize_holders(holder_data: List[Dict[str, Any]], protocol_name: str) -> pd.DataFrame:
"""
Standardize token holder data for supported protocols.
Extracts required fields: address, balance, percentage.
"""
standardized = []
for holder in holder_data:
# Try to extract address, balance, and percentage using common keys
address = holder.get("address") or holder.get("holder") or holder.get("account")
balance = holder.get("balance") or holder.get("tokenBalance") or holder.get("amount")
percentage = holder.get("percentage") or holder.get("share") or holder.get("ownership")
standardized.append({
"address": address,
"balance": balance,
"percentage": percentage
})
df = pd.DataFrame(standardized)
return df
```
Remove the now-unused `_standardize_compound_holders`, `_standardize_uniswap_holders`, and `_standardize_aave_holders` functions from the file.
If there are protocol-specific quirks, you may need to adjust the key extraction logic in `_standardize_holders` to handle those cases.
Update any tests or other code that called the protocol-specific functions to use the new unified function.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| @@ -0,0 +1 @@ | |||
| """Visualization Processor for Governance Token Distribution Analyzer.""" | |||
There was a problem hiding this comment.
issue: VisualizationProcessor is only a stub and lacks implementation.
If this is meant as a placeholder, define an empty class to prevent import issues. Otherwise, implement the class or remove the file until it's ready.
| # Combine metrics | ||
| combined_metrics = { | ||
| **distribution_result.get("metrics", {}), | ||
| **governance_result.get("metrics", {}), | ||
| } | ||
|
|
||
| # Generate visualizations |
There was a problem hiding this comment.
suggestion (bug_risk): Combining metrics with dictionary unpacking may silently overwrite keys.
If both metric dictionaries share keys, governance_result values will overwrite distribution_result values. Use explicit merging or namespacing if you want to avoid this.
Suggested implementation:
# Combine metrics with namespacing to avoid key collisions
combined_metrics = {
"distribution_metrics": distribution_result.get("metrics", {}),
"governance_metrics": governance_result.get("metrics", {}),
} # Generate visualizations
visualizations = self.visualization_processor.generate_visualizations(
protocol, token_holders, proposals, combined_metrics
)| # Calculate Gini coefficient | ||
| gini = self._calculate_gini_coefficient(balances) |
There was a problem hiding this comment.
issue: The Gini coefficient implementation may not handle negative or zero balances robustly.
Filter out negative balances and check that the sum is positive before calculating the Gini coefficient to avoid incorrect results or division by zero.
| logger.error(f"Error fetching governance votes for {protocol} proposal {proposal_id}: {e}") | ||
| return self._generate_sample_vote_data(protocol, proposal_id) | ||
|
|
||
| def _generate_sample_proposal_data(self, protocol: str, count: int) -> List[Dict[str, Any]]: |
There was a problem hiding this comment.
issue (code-quality): Low code quality found in ProtocolClient._generate_sample_proposal_data - 7% (low-code-quality)
Explanation
The quality score for this function is below the quality threshold of 25%.This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines. - Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.
|
|
||
| # Generate votes | ||
| votes = [] | ||
| for i in range(vote_count): |
There was a problem hiding this comment.
issue (code-quality): We've found these issues:
- Replace unused for index with underscore (
for-index-underscore) - Inline variable that is immediately returned (
inline-immediately-returned-variable)
| holders = [] | ||
|
|
||
| try: | ||
| if api_type == "etherscan": |
There was a problem hiding this comment.
issue (code-quality): We've found these issues:
- Simplify conditional into switch-like form [×2] (
switch) - Replace a for append loop with list extend (
for-append-to-extend) - Remove redundant pass statement (
remove-redundant-pass)
left a comment
There was a problem hiding this comment.
Pull Request Overview
This PR refactors large monolithic modules by splitting them into focused packages to enforce a 500-line file size limit and improve modularity.
- Restructured report generator into multiple submodules (base, HTML, historical, comprehensive).
- Broke down core API client and data processor into dedicated packages with clear responsibilities.
- Added backward compatibility wrappers and updated imports accordingly.
Reviewed Changes
Copilot reviewed 13 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/governance_token_analyzer/visualization/report_generator/report_generator_base.py | Introduces ReportGenerator base class with Jinja2 templating and report generation logic |
| src/governance_token_analyzer/visualization/report_generator/init.py | Updated package exports for report generator submodules |
| src/governance_token_analyzer/core/data_processor/visualization_processor.py | Added visualization processor placeholder (docstring only) |
| src/governance_token_analyzer/core/data_processor/metrics_processor.py | Added metrics calculation logic for distribution, governance, comparison |
| src/governance_token_analyzer/core/data_processor/data_standardizer.py | Added functions to standardize holder data and combine/filter DataFrames |
| src/governance_token_analyzer/core/data_processor/data_processor_base.py | Added DataProcessor class wiring metrics, visualization, and report processors |
| src/governance_token_analyzer/core/data_processor/init.py | Updated package exports for data processor modules |
| src/governance_token_analyzer/core/data_processor.py | Kept for backward compatibility re-exporting DataProcessor |
| src/governance_token_analyzer/core/api_client/response_parser.py | Added ResponseParser with parsing methods for various API responses |
| src/governance_token_analyzer/core/api_client/graph_client.py | Added TheGraphAPI client with rate-limited request execution |
| src/governance_token_analyzer/core/api_client/data_fetcher.py | Added DataFetcher for retrieving token holder and governance data |
| src/governance_token_analyzer/core/api_client/init.py | Updated package exports for API client modules |
| docs/refactoring_plan.md | Documented file size limit refactoring plan and progress |
Comments suppressed due to low confidence (1)
src/governance_token_analyzer/core/data_processor/visualization_processor.py:1
- This module declares a VisualizationProcessor import but defines no class or functions. Add a VisualizationProcessor implementation or remove the import to prevent an ImportError.
"""Visualization Processor for Governance Token Distribution Analyzer."""
| # Avoid division by zero | ||
| if total_tokens == 0 or holder_count == 0: | ||
| logger.warning("No tokens or holders found") | ||
| return { |
There was a problem hiding this comment.
The zero-case return dictionary omits the "whale_count" key that is present in the non-zero return. Adding it will maintain a consistent schema and prevent potential KeyError downstream.
Resolved issues in the following files with DeepSource Autofix: 1. src/governance_token_analyzer/core/api_client/base_client.py 2. src/governance_token_analyzer/core/api_client/data_fetcher.py 3. src/governance_token_analyzer/core/api_client/ethereum_client.py 4. src/governance_token_analyzer/core/api_client/protocol_client.py 5. src/governance_token_analyzer/core/data_processor/metrics_processor.py 6. src/governance_token_analyzer/core/data_processor/report_processor.py 7. src/governance_token_analyzer/visualization/report_generator/comprehensive_report.py 8. src/governance_token_analyzer/visualization/report_generator/historical_report_generator.py 9. src/governance_token_analyzer/visualization/report_generator/report_generator_base.py
…or_base.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| def process_time_series( | ||
| protocol: str, | ||
| time_series: Any, | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| historical_visualizations: List[Dict[str, str]], | ||
| historical_metrics: Dict[str, Any], | ||
| ) -> None: | ||
| """Process time series data and create visualizations. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| time_series: Time series data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
| historical_visualizations: List to append visualizations to | ||
| historical_metrics: Dict to add metrics to | ||
| """ | ||
| from governance_token_analyzer.visualization.historical_charts import create_time_series_chart | ||
|
|
||
| # Process dictionary of DataFrames | ||
| if isinstance(time_series, dict): | ||
| for metric_name, metric_data in time_series.items(): | ||
| if not isinstance(metric_data, pd.DataFrame) or metric_data.empty: | ||
| continue | ||
|
|
||
| chart_file = os.path.join(viz_dir, f"{protocol}_{metric_name}_{timestamp}.png") | ||
|
|
||
| try: | ||
| create_time_series_chart( | ||
| time_series=metric_data, | ||
| output_path=chart_file, | ||
| metric=metric_name, | ||
| title=f"{protocol.upper()} {metric_name.replace('_', ' ').title()} Over Time", | ||
| ) | ||
|
|
||
| historical_visualizations.append( | ||
| { | ||
| "title": f"Historical {metric_name.replace('_', ' ').title()}", | ||
| "path": chart_file, | ||
| "description": f"Time series analysis of {metric_name.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Add to historical metrics | ||
| historical_metrics[metric_name] = ( | ||
| metric_data.iloc[-1][metric_name] if metric_name in metric_data.columns else "N/A" | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error creating time series chart for {metric_name}: {e}") | ||
|
|
||
| # Process single DataFrame | ||
| elif isinstance(time_series, pd.DataFrame) and not time_series.empty: | ||
| for column in time_series.columns: | ||
| if column in ["date", "timestamp"]: | ||
| continue | ||
|
|
||
| chart_file = os.path.join(viz_dir, f"{protocol}_{column}_{timestamp}.png") | ||
|
|
||
| try: | ||
| create_time_series_chart( | ||
| time_series=time_series, | ||
| output_path=chart_file, | ||
| metric=column, | ||
| title=f"{protocol.upper()} {column.replace('_', ' ').title()} Over Time", | ||
| ) | ||
|
|
||
| historical_visualizations.append( | ||
| { | ||
| "title": f"Historical {column.replace('_', ' ').title()}", | ||
| "path": chart_file, | ||
| "description": f"Time series analysis of {column.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Add to historical metrics | ||
| historical_metrics[column] = time_series.iloc[-1][column] if column in time_series.columns else "N/A" | ||
| except Exception as e: | ||
| logger.error(f"Error creating time series chart for {column}: {e}") |
There was a problem hiding this comment.
❌ New issue: Complex Method
process_time_series has a cyclomatic complexity of 11, threshold = 9
| def create_governance_visualization( | ||
| protocol: str, governance_data: List[Dict[str, Any]], viz_dir: str, timestamp: str | ||
| ) -> List[Dict[str, str]]: | ||
| """Create visualizations for governance data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| governance_data: Governance data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| List of visualization metadata | ||
| """ | ||
| visualizations = [] | ||
|
|
||
| if not governance_data: | ||
| return visualizations | ||
|
|
||
| try: | ||
| # Create governance participation chart | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_governance_{timestamp}.png") | ||
|
|
||
| # Extract participation data | ||
| proposals_data = [] | ||
| for proposal in governance_data: | ||
| if "id" in proposal and "for_votes" in proposal and "against_votes" in proposal: | ||
| proposals_data.append( | ||
| { | ||
| "id": proposal["id"], | ||
| "for": float(proposal.get("for_votes", 0)), | ||
| "against": float(proposal.get("against_votes", 0)), | ||
| } | ||
| ) | ||
|
|
||
| if proposals_data: | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
|
|
||
| proposal_ids = [p["id"] for p in proposals_data] | ||
| for_votes = [p["for"] for p in proposals_data] | ||
| against_votes = [p["against"] for p in proposals_data] | ||
|
|
||
| # Width of the bars | ||
| width = 0.3 | ||
|
|
||
| # Position of bars on x-axis | ||
| ind = np.arange(len(proposal_ids)) | ||
|
|
||
| # Creating bars | ||
| ax.bar(ind - width / 2, for_votes, width, label="For") | ||
| ax.bar(ind + width / 2, against_votes, width, label="Against") | ||
|
|
||
| # Labels and title | ||
| ax.set_xlabel("Proposal ID") | ||
| ax.set_ylabel("Votes") | ||
| ax.set_title(f"{protocol.upper()} Governance Participation") | ||
| ax.set_xticks(ind) | ||
| ax.set_xticklabels(proposal_ids) | ||
| ax.legend() | ||
|
|
||
| # Save figure | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file, dpi=300) | ||
| plt.close(fig) | ||
|
|
||
| # Add to visualizations | ||
| visualizations.append( | ||
| { | ||
| "title": "Governance Participation", | ||
| "path": chart_file, | ||
| "description": "Voting participation across governance proposals.", | ||
| } | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error generating governance visualizations: {e}") | ||
|
|
||
| return visualizations |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_governance_visualization has a cyclomatic complexity of 11, threshold = 9
| def create_token_distribution_visualization( | ||
| protocol: str, current_data: Dict[str, Any], viz_dir: str, timestamp: str | ||
| ) -> List[Dict[str, str]]: | ||
| """Create visualizations for token distribution. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| List of visualization metadata | ||
| """ | ||
| visualizations = [] | ||
|
|
||
| if not current_data or "token_holders" not in current_data or not current_data["token_holders"]: | ||
| return visualizations | ||
|
|
||
| try: | ||
| # Prepare data for visualization | ||
| token_holders = current_data["token_holders"] | ||
| holders_data = [] | ||
| for holder in token_holders[:20]: # Top 20 holders | ||
| if "TokenHolderAddress" in holder and "TokenHolderQuantity" in holder: | ||
| holders_data.append( | ||
| { | ||
| "address": holder["TokenHolderAddress"], | ||
| "balance": float(holder["TokenHolderQuantity"]), | ||
| } | ||
| ) | ||
|
|
||
| # Create distribution chart if we have data | ||
| if holders_data: | ||
| chart_file = os.path.join(viz_dir, f"{protocol}_distribution_{timestamp}.png") | ||
|
|
||
| # Extract data for plotting | ||
| addresses = [h["address"] for h in holders_data] | ||
| balances = [h["balance"] for h in holders_data] | ||
|
|
||
| # Create the chart | ||
| fig, ax = plt.subplots(figsize=(10, 6)) | ||
| ax.bar(range(len(addresses)), balances) | ||
| ax.set_title(f"{protocol.upper()} Token Distribution") | ||
| ax.set_xlabel("Holder Rank") | ||
| ax.set_ylabel("Token Balance") | ||
| ax.set_xticks([]) # Hide x-axis labels as they would be too crowded | ||
|
|
||
| # Save the chart | ||
| plt.tight_layout() | ||
| plt.savefig(chart_file) | ||
| plt.close(fig) | ||
|
|
||
| # Add to visualizations list | ||
| visualizations.append( | ||
| { | ||
| "title": "Token Distribution", | ||
| "path": chart_file, | ||
| "description": "Distribution of tokens among top token holders.", | ||
| } | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error generating token distribution visualizations: {e}") | ||
|
|
||
| return visualizations |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_token_distribution_visualization has a cyclomatic complexity of 11, threshold = 9
| def generate_comprehensive_html_report( | ||
| report_generator, | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| historical_data: Optional[Dict[str, Any]] = None, | ||
| output_path: str = None, | ||
| report_dir: str = None, | ||
| viz_dir: str = None, | ||
| timestamp: str = None, | ||
| ) -> str: | ||
| """Generate a comprehensive HTML report with all data components. | ||
|
|
||
| Args: | ||
| report_generator: The ReportGenerator instance | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| historical_data: Historical data dictionary | ||
| output_path: Path to save the report | ||
| report_dir: Directory for the report | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Path to the generated report | ||
| """ | ||
| # Initialize parameters with default values if not provided | ||
| timestamp = timestamp or datetime.now().strftime("%Y%m%d_%H%M%S") | ||
| report_dir = report_dir or report_generator.output_dir | ||
| viz_dir = viz_dir or os.path.join(report_dir, "visualizations") | ||
| os.makedirs(viz_dir, exist_ok=True) | ||
| output_path = output_path or os.path.join(report_dir, f"{protocol}_report_{timestamp}.html") | ||
|
|
||
| # Extract metrics and create visualizations | ||
| metrics_data = report_generator._extract_metrics(current_data) if current_data and "metrics" in current_data else [] | ||
|
|
||
| # Generate visualizations | ||
| distribution_visualizations = create_token_distribution_visualization(protocol, current_data, viz_dir, timestamp) | ||
|
|
||
| governance_visualizations = create_governance_visualization(protocol, governance_data, viz_dir, timestamp) | ||
|
|
||
| # Process historical data if available | ||
| historical_section = process_historical_data(protocol, historical_data, viz_dir, timestamp) | ||
|
|
||
| # Combine all visualizations | ||
| all_visualizations = distribution_visualizations + governance_visualizations | ||
| if historical_section and "visualizations" in historical_section: | ||
| all_visualizations.extend(historical_section["visualizations"]) | ||
|
|
||
| # Generate the HTML report using template | ||
| try: | ||
| return render_html_template( | ||
| report_generator, | ||
| protocol=protocol, | ||
| metrics_data=metrics_data, | ||
| all_visualizations=all_visualizations, | ||
| governance_data=governance_data[:10], # Limit to top 10 proposals | ||
| historical_section=historical_section, | ||
| output_path=output_path, | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error rendering HTML: {e}") | ||
|
|
||
| # Fallback to basic HTML if template rendering fails | ||
| return generate_basic_html_report( | ||
| protocol=protocol, | ||
| metrics=metrics_data, | ||
| visualizations=all_visualizations, | ||
| output_path=output_path, | ||
| historical_section=historical_section, | ||
| ) |
There was a problem hiding this comment.
❌ New issue: Complex Method
generate_comprehensive_html_report has a cyclomatic complexity of 9, threshold = 9
| """ | ||
| visualizations = [] | ||
|
|
||
| if not current_data or "token_holders" not in current_data or not current_data["token_holders"]: |
There was a problem hiding this comment.
❌ New issue: Complex Conditional
create_token_distribution_visualization has 1 complex conditionals with 2 branches, threshold = 2
| def parse_token_holders(response: Dict[str, Any], api_type: str) -> List[Dict[str, Any]]: | ||
| """Parse token holder response from various APIs. | ||
|
|
||
| Args: | ||
| response: API response dictionary | ||
| api_type: Type of API (etherscan, ethplorer, alchemy) | ||
|
|
||
| Returns: | ||
| Standardized list of token holder dictionaries | ||
| """ | ||
| holders = [] | ||
|
|
||
| try: | ||
| if api_type == "etherscan": | ||
| if "result" in response and isinstance(response["result"], list): | ||
| for holder in response["result"]: | ||
| holders.append( | ||
| { | ||
| "address": holder.get("address", ""), | ||
| "balance": float(holder.get("value", 0)) / 10**18, # Convert from wei | ||
| "percentage": float(holder.get("share", 0)), | ||
| } | ||
| ) | ||
| elif api_type == "ethplorer": | ||
| if "holders" in response and isinstance(response["holders"], list): | ||
| for holder in response["holders"]: | ||
| holders.append( | ||
| { | ||
| "address": holder.get("address", ""), | ||
| "balance": float(holder.get("balance", 0)) / 10**18, # Convert from wei | ||
| "percentage": float(holder.get("share", 0)), | ||
| } | ||
| ) | ||
| elif api_type == "alchemy": | ||
| # Alchemy doesn't currently have a token holders endpoint | ||
| # This is a placeholder for when Alchemy adds this feature | ||
| pass | ||
| else: | ||
| logger.warning(f"Unknown API type: {api_type}") | ||
| except Exception as e: | ||
| logger.error(f"Error parsing token holders from {api_type}: {e}") | ||
|
|
||
| return holders |
There was a problem hiding this comment.
❌ New issue: Complex Method
ResponseParser.parse_token_holders has a cyclomatic complexity of 12, threshold = 9
| def parse_token_balance(response: Dict[str, Any], api_type: str) -> float: | ||
| """Parse token balance response from various APIs. | ||
|
|
||
| Args: | ||
| response: API response dictionary | ||
| api_type: Type of API (etherscan, ethplorer, alchemy) | ||
|
|
||
| Returns: | ||
| Token balance as a float | ||
| """ | ||
| try: | ||
| if api_type == "etherscan": | ||
| if "result" in response: | ||
| return float(response["result"]) / 10**18 # Convert from wei | ||
| elif api_type == "ethplorer": | ||
| if "balance" in response: | ||
| return float(response["balance"]) / 10**18 # Convert from wei | ||
| elif api_type == "alchemy": | ||
| if "result" in response: | ||
| return float(response["result"]) / 10**18 # Convert from wei | ||
| else: | ||
| logger.warning(f"Unknown API type: {api_type}") | ||
| except Exception as e: | ||
| logger.error(f"Error parsing token balance from {api_type}: {e}") | ||
|
|
||
| return 0.0 |
There was a problem hiding this comment.
❌ New issue: Complex Method
ResponseParser.parse_token_balance has a cyclomatic complexity of 9, threshold = 9
| def parse_token_balance(response: Dict[str, Any], api_type: str) -> float: | ||
| """Parse token balance response from various APIs. | ||
|
|
||
| Args: | ||
| response: API response dictionary | ||
| api_type: Type of API (etherscan, ethplorer, alchemy) | ||
|
|
||
| Returns: | ||
| Token balance as a float | ||
| """ | ||
| try: | ||
| if api_type == "etherscan": | ||
| if "result" in response: | ||
| return float(response["result"]) / 10**18 # Convert from wei | ||
| elif api_type == "ethplorer": | ||
| if "balance" in response: | ||
| return float(response["balance"]) / 10**18 # Convert from wei | ||
| elif api_type == "alchemy": | ||
| if "result" in response: | ||
| return float(response["result"]) / 10**18 # Convert from wei | ||
| else: | ||
| logger.warning(f"Unknown API type: {api_type}") | ||
| except Exception as e: | ||
| logger.error(f"Error parsing token balance from {api_type}: {e}") | ||
|
|
||
| return 0.0 |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
ResponseParser.parse_token_balance has 3 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def parse_token_holders(response: Dict[str, Any], api_type: str) -> List[Dict[str, Any]]: | ||
| """Parse token holder response from various APIs. | ||
|
|
||
| Args: | ||
| response: API response dictionary | ||
| api_type: Type of API (etherscan, ethplorer, alchemy) | ||
|
|
||
| Returns: | ||
| Standardized list of token holder dictionaries | ||
| """ | ||
| holders = [] | ||
|
|
||
| try: | ||
| if api_type == "etherscan": | ||
| if "result" in response and isinstance(response["result"], list): | ||
| for holder in response["result"]: | ||
| holders.append( | ||
| { | ||
| "address": holder.get("address", ""), | ||
| "balance": float(holder.get("value", 0)) / 10**18, # Convert from wei | ||
| "percentage": float(holder.get("share", 0)), | ||
| } | ||
| ) | ||
| elif api_type == "ethplorer": | ||
| if "holders" in response and isinstance(response["holders"], list): | ||
| for holder in response["holders"]: | ||
| holders.append( | ||
| { | ||
| "address": holder.get("address", ""), | ||
| "balance": float(holder.get("balance", 0)) / 10**18, # Convert from wei | ||
| "percentage": float(holder.get("share", 0)), | ||
| } | ||
| ) | ||
| elif api_type == "alchemy": | ||
| # Alchemy doesn't currently have a token holders endpoint | ||
| # This is a placeholder for when Alchemy adds this feature | ||
| pass | ||
| else: | ||
| logger.warning(f"Unknown API type: {api_type}") | ||
| except Exception as e: | ||
| logger.error(f"Error parsing token holders from {api_type}: {e}") | ||
|
|
||
| return holders |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
ResponseParser.parse_token_holders has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def parse_token_supply(response: Dict[str, Any], api_type: str) -> int: | ||
| """Parse token supply response from various APIs. | ||
|
|
||
| Args: | ||
| response: API response dictionary | ||
| api_type: Type of API (etherscan, ethplorer, alchemy) | ||
|
|
||
| Returns: | ||
| Token supply as an integer | ||
| """ | ||
| try: | ||
| if api_type == "etherscan": | ||
| if "result" in response: | ||
| return int(response["result"]) | ||
| elif api_type == "ethplorer": | ||
| if "totalSupply" in response: | ||
| return int(float(response["totalSupply"])) | ||
| elif api_type == "alchemy": | ||
| # Alchemy doesn't currently have a token supply endpoint | ||
| # This is a placeholder for when Alchemy adds this feature | ||
| pass | ||
| else: | ||
| logger.warning(f"Unknown API type: {api_type}") | ||
| except Exception as e: | ||
| logger.error(f"Error parsing token supply from {api_type}: {e}") | ||
|
|
||
| return 0 |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
ResponseParser.parse_token_supply has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
…eport_generator_base.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…essor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
…or_base.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
…istorical_report_generator.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| def create_dataframe_time_series_tables(time_series_data: Dict[str, pd.DataFrame]) -> str: | ||
| """Create HTML tables for time series data stored in DataFrames. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of DataFrames | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, df in time_series_data.items(): | ||
| if not isinstance(df, pd.DataFrame) or df.empty: | ||
| continue | ||
|
|
||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in df.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in df.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in df.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Code Duplication
The module contains 2 functions with similar structure: create_dataframe_time_series_tables,create_dict_time_series_tables
| def create_time_series_section( | ||
| protocol: str, time_series_data: Union[Dict[str, pd.DataFrame], pd.DataFrame], viz_dir: str, timestamp: str | ||
| ) -> str: | ||
| """Create the time series section of the historical report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| time_series_data: Time series data (DataFrame or Dict of DataFrames) | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| HTML string for the time series section | ||
| """ | ||
| if time_series_data is None: | ||
| return "" | ||
|
|
||
| html_content = """ | ||
| <div class="section"> | ||
| <h2>Time Series Analysis</h2> | ||
| """ | ||
|
|
||
| # Process time series data and create visualizations | ||
| visualizations = [] | ||
| tables_html = "" | ||
|
|
||
| # Handle dictionary of DataFrames | ||
| if isinstance(time_series_data, dict): | ||
| tables_html = create_dataframe_time_series_tables(time_series_data) | ||
| for metric_name, df in time_series_data.items(): | ||
| if not isinstance(df, pd.DataFrame) or df.empty: | ||
| continue | ||
| viz_path = _create_time_series_chart(protocol, df, metric_name, viz_dir, timestamp) | ||
| if viz_path: | ||
| visualizations.append( | ||
| { | ||
| "title": f"{metric_name.replace('_', ' ').title()} Over Time", | ||
| "path": viz_path, | ||
| "description": f"Historical trend of {metric_name.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Handle single DataFrame | ||
| elif isinstance(time_series_data, pd.DataFrame) and not time_series_data.empty: | ||
| tables_html = create_dict_time_series_tables({"metrics": time_series_data}) | ||
| for column in time_series_data.columns: | ||
| if column in ["date", "timestamp"]: | ||
| continue | ||
| viz_path = _create_time_series_chart(protocol, time_series_data, column, viz_dir, timestamp) | ||
| if viz_path: | ||
| visualizations.append( | ||
| { | ||
| "title": f"{column.replace('_', ' ').title()} Over Time", | ||
| "path": viz_path, | ||
| "description": f"Historical trend of {column.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Add visualizations to HTML content | ||
| for viz in visualizations: | ||
| viz_path = os.path.basename(viz["path"]) | ||
| html_content += f""" | ||
| <div class="visualization"> | ||
| <h3>{viz["title"]}</h3> | ||
| <img src="{viz_path}" alt="{viz["title"]}"> | ||
| <p>{viz["description"]}</p> | ||
| </div> | ||
| """ | ||
|
|
||
| # Add tables to HTML content | ||
| html_content += tables_html | ||
| html_content += "</div>\n" | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_time_series_section has a cyclomatic complexity of 13, threshold = 9
| def create_dict_time_series_tables(time_series_data: Dict[str, Any]) -> str: | ||
| """Create HTML tables for time series data stored in dictionaries. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of time series data | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, data in time_series_data.items(): | ||
| if isinstance(data, pd.DataFrame) and not data.empty: | ||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in data.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in data.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in data.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_dict_time_series_tables has a cyclomatic complexity of 9, threshold = 9
| def create_dataframe_time_series_tables(time_series_data: Dict[str, pd.DataFrame]) -> str: | ||
| """Create HTML tables for time series data stored in DataFrames. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of DataFrames | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, df in time_series_data.items(): | ||
| if not isinstance(df, pd.DataFrame) or df.empty: | ||
| continue | ||
|
|
||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in df.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in df.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in df.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Complex Method
create_dataframe_time_series_tables has a cyclomatic complexity of 9, threshold = 9
| def create_time_series_section( | ||
| protocol: str, time_series_data: Union[Dict[str, pd.DataFrame], pd.DataFrame], viz_dir: str, timestamp: str | ||
| ) -> str: | ||
| """Create the time series section of the historical report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| time_series_data: Time series data (DataFrame or Dict of DataFrames) | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| HTML string for the time series section | ||
| """ | ||
| if time_series_data is None: | ||
| return "" | ||
|
|
||
| html_content = """ | ||
| <div class="section"> | ||
| <h2>Time Series Analysis</h2> | ||
| """ | ||
|
|
||
| # Process time series data and create visualizations | ||
| visualizations = [] | ||
| tables_html = "" | ||
|
|
||
| # Handle dictionary of DataFrames | ||
| if isinstance(time_series_data, dict): | ||
| tables_html = create_dataframe_time_series_tables(time_series_data) | ||
| for metric_name, df in time_series_data.items(): | ||
| if not isinstance(df, pd.DataFrame) or df.empty: | ||
| continue | ||
| viz_path = _create_time_series_chart(protocol, df, metric_name, viz_dir, timestamp) | ||
| if viz_path: | ||
| visualizations.append( | ||
| { | ||
| "title": f"{metric_name.replace('_', ' ').title()} Over Time", | ||
| "path": viz_path, | ||
| "description": f"Historical trend of {metric_name.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Handle single DataFrame | ||
| elif isinstance(time_series_data, pd.DataFrame) and not time_series_data.empty: | ||
| tables_html = create_dict_time_series_tables({"metrics": time_series_data}) | ||
| for column in time_series_data.columns: | ||
| if column in ["date", "timestamp"]: | ||
| continue | ||
| viz_path = _create_time_series_chart(protocol, time_series_data, column, viz_dir, timestamp) | ||
| if viz_path: | ||
| visualizations.append( | ||
| { | ||
| "title": f"{column.replace('_', ' ').title()} Over Time", | ||
| "path": viz_path, | ||
| "description": f"Historical trend of {column.replace('_', ' ')} over time.", | ||
| } | ||
| ) | ||
|
|
||
| # Add visualizations to HTML content | ||
| for viz in visualizations: | ||
| viz_path = os.path.basename(viz["path"]) | ||
| html_content += f""" | ||
| <div class="visualization"> | ||
| <h3>{viz["title"]}</h3> | ||
| <img src="{viz_path}" alt="{viz["title"]}"> | ||
| <p>{viz["description"]}</p> | ||
| </div> | ||
| """ | ||
|
|
||
| # Add tables to HTML content | ||
| html_content += tables_html | ||
| html_content += "</div>\n" | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
create_time_series_section has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def create_dict_time_series_tables(time_series_data: Dict[str, Any]) -> str: | ||
| """Create HTML tables for time series data stored in dictionaries. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of time series data | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, data in time_series_data.items(): | ||
| if isinstance(data, pd.DataFrame) and not data.empty: | ||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in data.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in data.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in data.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
create_dict_time_series_tables has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def create_dict_time_series_tables(time_series_data: Dict[str, Any]) -> str: | ||
| """Create HTML tables for time series data stored in dictionaries. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of time series data | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, data in time_series_data.items(): | ||
| if isinstance(data, pd.DataFrame) and not data.empty: | ||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in data.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in data.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in data.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Deep, Nested Complexity
create_dict_time_series_tables has a nested complexity depth of 6, threshold = 4
| def create_dataframe_time_series_tables(time_series_data: Dict[str, pd.DataFrame]) -> str: | ||
| """Create HTML tables for time series data stored in DataFrames. | ||
|
|
||
| Args: | ||
| time_series_data: Dictionary of DataFrames | ||
|
|
||
| Returns: | ||
| HTML string with tables | ||
| """ | ||
| html_content = "" | ||
|
|
||
| for metric_name, df in time_series_data.items(): | ||
| if not isinstance(df, pd.DataFrame) or df.empty: | ||
| continue | ||
|
|
||
| html_content += f""" | ||
| <h3>{metric_name.replace("_", " ").title()} Data</h3> | ||
| <div style="max-height: 300px; overflow-y: auto;"> | ||
| <table> | ||
| <tr> | ||
| """ | ||
|
|
||
| # Add column headers | ||
| for col in df.columns: | ||
| html_content += f"<th>{col.replace('_', ' ').title()}</th>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| # Add rows (limit to 100 rows to prevent huge tables) | ||
| for _, row in df.head(100).iterrows(): | ||
| html_content += "<tr>" | ||
| for col in df.columns: | ||
| value = row[col] | ||
| if isinstance(value, (int, float)): | ||
| formatted_value = f"{value:.4f}" if col not in ["date", "timestamp"] else str(value) | ||
| else: | ||
| formatted_value = str(value) | ||
| html_content += f"<td>{formatted_value}</td>" | ||
| html_content += "</tr>\n" | ||
|
|
||
| html_content += """ | ||
| </table> | ||
| </div> | ||
| """ | ||
|
|
||
| return html_content |
There was a problem hiding this comment.
❌ New issue: Deep, Nested Complexity
create_dataframe_time_series_tables has a nested complexity depth of 5, threshold = 4
| def _generate_historical_html_report( | ||
| protocol: str, | ||
| historical_data: Dict[str, Any], | ||
| output_path: str, | ||
| viz_dir: str, | ||
| timestamp: str, | ||
| ) -> str: | ||
| """Generate a historical analysis HTML report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| historical_data: Historical data dictionary | ||
| output_path: Path to save the report | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Path to the generated report | ||
| """ | ||
| # Create HTML content | ||
| html_content = create_historical_report_header(protocol) | ||
|
|
||
| # Process time series data with validation | ||
| time_series = historical_data.get("time_series") | ||
| if time_series is not None: | ||
| if isinstance(time_series, (list, dict)): # Adjust type as needed | ||
| html_content += create_time_series_section(protocol, time_series, viz_dir, timestamp) | ||
| else: | ||
| logging.warning("Expected 'time_series' to be a list or dict, got %s", type(time_series).__name__) | ||
| else: | ||
| logging.info("'time_series' key not found in historical_data; skipping time series section.") | ||
|
|
||
| # Process snapshot data | ||
| if "snapshots" in historical_data: | ||
| html_content += create_snapshots_section(protocol, historical_data["snapshots"]) | ||
|
|
||
| # Add conclusion | ||
| html_content += """ | ||
| <div class="section"> | ||
| <h2>Conclusion</h2> | ||
| <p>This historical analysis provides insights into how governance token distribution and participation metrics | ||
| have changed over time.</p> | ||
| </div> | ||
|
|
||
| <div class="footer"> | ||
| <p>Generated using Governance Token Distribution Analyzer</p> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| """ | ||
|
|
||
| # Write to file | ||
| with open(output_path, "w") as f: | ||
| f.write(html_content) | ||
|
|
||
| return output_path |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
_generate_historical_html_report has 5 arguments, threshold = 4
| def _create_time_series_chart( | ||
| protocol: str, df: pd.DataFrame, metric: str, viz_dir: str, timestamp: str | ||
| ) -> Optional[str]: | ||
| """Create a time series chart for a specific metric. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| df: DataFrame with time series data | ||
| metric: Metric name | ||
| viz_dir: Directory for visualizations | ||
| timestamp: Timestamp for the report | ||
|
|
||
| Returns: | ||
| Path to the generated chart or None if failed | ||
| """ | ||
| try: | ||
| from governance_token_analyzer.visualization.historical_charts import create_time_series_chart | ||
|
|
||
| chart_file = os.path.join(viz_dir, f"{protocol}_{metric}_{timestamp}.png") | ||
|
|
||
| create_time_series_chart( | ||
| time_series=df, | ||
| output_path=chart_file, | ||
| metric=metric, | ||
| title=f"{protocol.upper()} {metric.replace('_', ' ').title()} Over Time", | ||
| ) | ||
|
|
||
| return chart_file | ||
| except Exception as e: | ||
| logger.error(f"Error creating time series chart for {metric}: {e}") | ||
| return None |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
_create_time_series_chart has 5 arguments, threshold = 4
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| def _generate_sample_proposal_data(self, protocol: str, count: int) -> List[Dict[str, Any]]: | ||
| """Generate sample governance proposal data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name (compound, uniswap, aave) | ||
| count: Number of proposals to generate | ||
|
|
||
| Returns: | ||
| List of simulated governance proposal dictionaries | ||
| """ | ||
| if protocol.lower() not in PROTOCOL_INFO: | ||
| raise ValueError(f"Unsupported protocol: {protocol}") | ||
|
|
||
| protocol_info = PROTOCOL_INFO[protocol.lower()] | ||
|
|
||
| # Sample proposal titles and descriptions for each protocol | ||
| proposal_templates = { | ||
| "compound": [ | ||
| { | ||
| "title": "Adjust the reserve factor for {asset}", | ||
| "description": "This proposal adjusts the reserve factor for {asset} from {old_value}% to {new_value}%.", | ||
| }, | ||
| { | ||
| "title": "Add support for {asset}", | ||
| "description": "This proposal adds support for {asset} with the following parameters: Collateral factor: {cf}%, Reserve factor: {rf}%, Supply cap: {cap}.", | ||
| }, | ||
| { | ||
| "title": "Update {asset} risk parameters", | ||
| "description": "This proposal updates the risk parameters for {asset}. New collateral factor: {cf}%, New reserve factor: {rf}%, New supply cap: {cap}.", | ||
| }, | ||
| { | ||
| "title": "Upgrade Comptroller implementation", | ||
| "description": "This proposal upgrades the Comptroller implementation to address {issue} and add {feature}.", | ||
| }, | ||
| { | ||
| "title": "Distribute COMP to {recipient}", | ||
| "description": "This proposal distributes {amount} COMP to {recipient} for {reason}.", | ||
| }, | ||
| ], | ||
| "uniswap": [ | ||
| { | ||
| "title": "Deploy Uniswap v3 on {chain}", | ||
| "description": "This proposal deploys Uniswap v3 on {chain} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Adjust fee tier for {pair}", | ||
| "description": "This proposal adjusts the fee tier for {pair} from {old_fee}% to {new_fee}%.", | ||
| }, | ||
| { | ||
| "title": "Add new fee tier of {fee}%", | ||
| "description": "This proposal adds a new fee tier of {fee}% for pairs with {characteristic} characteristics.", | ||
| }, | ||
| { | ||
| "title": "Allocate UNI for {program}", | ||
| "description": "This proposal allocates {amount} UNI for the {program} program over the next {duration} months.", | ||
| }, | ||
| { | ||
| "title": "Update price oracle for {pair}", | ||
| "description": "This proposal updates the price oracle for {pair} to use {oracle} with {method}.", | ||
| }, | ||
| ], | ||
| "aave": [ | ||
| { | ||
| "title": "Add {asset} as collateral", | ||
| "description": "This proposal adds {asset} as collateral with the following parameters: LTV: {ltv}%, Liquidation threshold: {lt}%, Liquidation bonus: {lb}%.", | ||
| }, | ||
| { | ||
| "title": "Update interest rate strategy for {asset}", | ||
| "description": "This proposal updates the interest rate strategy for {asset} with the following parameters: Base: {base}%, Slope1: {slope1}%, Slope2: {slope2}%, Optimal utilization: {util}%.", | ||
| }, | ||
| { | ||
| "title": "Enable borrowing for {asset}", | ||
| "description": "This proposal enables borrowing for {asset} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Deploy Aave v3 on {chain}", | ||
| "description": "This proposal deploys Aave v3 on {chain} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Allocate {amount} AAVE to {recipient}", | ||
| "description": "This proposal allocates {amount} AAVE to {recipient} for {reason}.", | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| # Assets for each protocol | ||
| assets = { | ||
| "compound": ["USDC", "ETH", "DAI", "WBTC", "LINK", "UNI", "COMP"], | ||
| "uniswap": ["ETH/USDC", "ETH/DAI", "WBTC/ETH", "UNI/ETH", "USDC/DAI"], | ||
| "aave": ["USDC", "ETH", "DAI", "WBTC", "LINK", "UNI", "AAVE"], | ||
| } | ||
|
|
||
| # Chains | ||
| chains = ["Arbitrum", "Optimism", "Polygon", "Base", "zkSync Era", "Avalanche"] | ||
|
|
||
| # Generate proposals | ||
| proposals = [] | ||
| for i in range(count): | ||
| # Select template | ||
| templates = proposal_templates.get(protocol.lower(), proposal_templates["compound"]) | ||
| template = random.choice(templates) | ||
|
|
||
| # Fill in template variables | ||
| title = template["title"] | ||
| description = template["description"] | ||
|
|
||
| # Replace placeholders | ||
| if "{asset}" in title or "{asset}" in description: | ||
| asset = random.choice(assets.get(protocol.lower(), assets["compound"])) | ||
| title = title.replace("{asset}", asset) | ||
| description = description.replace("{asset}", asset) | ||
|
|
||
| if "{pair}" in title or "{pair}" in description: | ||
| pair = random.choice(assets.get("uniswap", assets["compound"])) | ||
| title = title.replace("{pair}", pair) | ||
| description = description.replace("{pair}", pair) | ||
|
|
||
| if "{chain}" in title or "{chain}" in description: | ||
| chain = random.choice(chains) | ||
| title = title.replace("{chain}", chain) | ||
| description = description.replace("{chain}", chain) | ||
|
|
||
| if "{amount}" in title or "{amount}" in description: | ||
| amount = f"{random.randint(10000, 1000000):,}" | ||
| title = title.replace("{amount}", amount) | ||
| description = description.replace("{amount}", amount) | ||
|
|
||
| if "{recipient}" in title or "{recipient}" in description: | ||
| recipient = f"0x{random.randint(0, 0xFFFFFFFF):08x}" | ||
| title = title.replace("{recipient}", recipient) | ||
| description = description.replace("{recipient}", recipient) | ||
|
|
||
| if "{reason}" in description: | ||
| reasons = [ | ||
| "community development", | ||
| "grants program", | ||
| "protocol improvements", | ||
| "security audits", | ||
| "bug bounties", | ||
| ] | ||
| reason = random.choice(reasons) | ||
| description = description.replace("{reason}", reason) | ||
|
|
||
| if "{old_value}" in description and "{new_value}" in description: | ||
| old_value = random.randint(5, 20) | ||
| new_value = random.randint(5, 20) | ||
| while new_value == old_value: | ||
| new_value = random.randint(5, 20) | ||
| description = description.replace("{old_value}", str(old_value)) | ||
| description = description.replace("{new_value}", str(new_value)) | ||
|
|
||
| if "{cf}" in description: | ||
| cf = random.randint(50, 85) | ||
| description = description.replace("{cf}", str(cf)) | ||
|
|
||
| if "{rf}" in description: | ||
| rf = random.randint(5, 25) | ||
| description = description.replace("{rf}", str(rf)) | ||
|
|
||
| if "{cap}" in description: | ||
| cap = f"{random.randint(1, 100):,}M" | ||
| description = description.replace("{cap}", cap) | ||
|
|
||
| if "{issue}" in description: | ||
| issues = ["gas optimization", "security vulnerability", "accounting error", "protocol efficiency"] | ||
| issue = random.choice(issues) | ||
| description = description.replace("{issue}", issue) | ||
|
|
||
| if "{feature}" in description: | ||
| features = [ | ||
| "improved liquidation mechanism", | ||
| "better interest rate model", | ||
| "new risk management features", | ||
| "enhanced governance", | ||
| ] | ||
| feature = random.choice(features) | ||
| description = description.replace("{feature}", feature) | ||
|
|
||
| if "{old_fee}" in description and "{new_fee}" in description: | ||
| old_fee = random.choice([0.05, 0.1, 0.3, 1.0]) | ||
| new_fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| while new_fee == old_fee: | ||
| new_fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| description = description.replace("{old_fee}", str(old_fee)) | ||
| description = description.replace("{new_fee}", str(new_fee)) | ||
|
|
||
| if "{fee}" in description: | ||
| fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| description = description.replace("{fee}", str(fee)) | ||
|
|
||
| if "{characteristic}" in description: | ||
| characteristics = ["high volatility", "stable coin", "low liquidity", "high volume"] | ||
| characteristic = random.choice(characteristics) | ||
| description = description.replace("{characteristic}", characteristic) | ||
|
|
||
| if "{program}" in description: | ||
| programs = ["liquidity mining", "developer grants", "ecosystem fund", "education"] | ||
| program = random.choice(programs) | ||
| description = description.replace("{program}", program) | ||
|
|
||
| if "{duration}" in description: | ||
| duration = random.randint(3, 24) | ||
| description = description.replace("{duration}", str(duration)) | ||
|
|
||
| if "{oracle}" in description: | ||
| oracles = ["Chainlink", "Uniswap TWAP", "Band Protocol", "API3"] | ||
| oracle = random.choice(oracles) | ||
| description = description.replace("{oracle}", oracle) | ||
|
|
||
| if "{method}" in description: | ||
| methods = ["time-weighted average", "volume-weighted average", "exponential moving average"] | ||
| method = random.choice(methods) | ||
| description = description.replace("{method}", method) | ||
|
|
||
| if "{ltv}" in description: | ||
| ltv = random.randint(50, 85) | ||
| description = description.replace("{ltv}", str(ltv)) | ||
|
|
||
| if "{lt}" in description: | ||
| lt = random.randint(60, 90) | ||
| description = description.replace("{lt}", str(lt)) | ||
|
|
||
| if "{lb}" in description: | ||
| lb = random.randint(5, 15) | ||
| description = description.replace("{lb}", str(lb)) | ||
|
|
||
| if "{base}" in description: | ||
| base = random.randint(0, 5) | ||
| description = description.replace("{base}", str(base)) | ||
|
|
||
| if "{slope1}" in description: | ||
| slope1 = random.randint(5, 15) | ||
| description = description.replace("{slope1}", str(slope1)) | ||
|
|
||
| if "{slope2}" in description: | ||
| slope2 = random.randint(50, 150) | ||
| description = description.replace("{slope2}", str(slope2)) | ||
|
|
||
| if "{util}" in description: | ||
| util = random.randint(70, 90) | ||
| description = description.replace("{util}", str(util)) | ||
|
|
||
| if "{params}" in description: | ||
| params = "standard protocol parameters" | ||
| description = description.replace("{params}", params) | ||
|
|
||
| # Generate random vote counts | ||
| for_votes = random.randint(100000, 1000000) | ||
| against_votes = random.randint(10000, for_votes) | ||
| abstain_votes = random.randint(1000, 100000) | ||
|
|
||
| # Generate timestamps | ||
| end_date = datetime.now() - timedelta(days=random.randint(1, 365)) | ||
| start_date = end_date - timedelta(days=random.randint(3, 7)) | ||
| created_date = start_date - timedelta(days=random.randint(1, 3)) | ||
|
|
||
| # Generate proposal | ||
| proposal = { | ||
| "id": str(count - i), # Newest proposals first | ||
| "title": title, | ||
| "description": description, | ||
| "proposer": f"0x{random.randint(0, 0xFFFFFFFF):08x}", | ||
| "targets": [f"0x{random.randint(0, 0xFFFFFFFF):08x}"], | ||
| "values": ["0"], | ||
| "signatures": [f"function{random.randint(1, 5)}(address,uint256)"], | ||
| "calldatas": [f"0x{random.randint(0, 0xFFFFFFFF):08x}"], | ||
| "startBlock": random.randint(10000000, 15000000), | ||
| "endBlock": random.randint(15000001, 20000000), | ||
| "forVotes": str(for_votes), | ||
| "againstVotes": str(against_votes), | ||
| "abstainVotes": str(abstain_votes), | ||
| "canceled": False, | ||
| "queued": random.random() > 0.1, | ||
| "executed": random.random() > 0.2, | ||
| "eta": int((end_date + timedelta(days=2)).timestamp()), | ||
| "createdAt": int(created_date.timestamp()), | ||
| "updatedAt": int(end_date.timestamp()), | ||
| "votes": self._generate_sample_vote_data(protocol, count - i), | ||
| } | ||
|
|
||
| proposals.append(proposal) | ||
|
|
||
| return proposals |
There was a problem hiding this comment.
❌ New issue: Complex Method
ProtocolClient._generate_sample_proposal_data has a cyclomatic complexity of 40, threshold = 9
| def _fetch_governance_proposals(self, protocol: str, limit: int) -> List[Dict[str, Any]]: | ||
| """Fetch governance proposals from The Graph API. | ||
|
|
||
| Args: | ||
| protocol: Protocol name (compound, uniswap, aave) | ||
| limit: Maximum number of proposals to return | ||
|
|
||
| Returns: | ||
| List of governance proposal dictionaries | ||
| """ | ||
| if protocol.lower() not in self.graph_clients: | ||
| logger.warning(f"No Graph client available for {protocol}") | ||
| return self._generate_sample_proposal_data(protocol, limit) | ||
|
|
||
| graph_client = self.graph_clients[protocol.lower()] | ||
| query = GOVERNANCE_QUERIES.get(protocol.lower(), "") | ||
|
|
||
| if not query: | ||
| logger.warning(f"No GraphQL query defined for {protocol}") | ||
| return self._generate_sample_proposal_data(protocol, limit) | ||
|
|
||
| try: | ||
| # Batch fetch proposals | ||
| batch_size = 100 | ||
| all_proposals = [] | ||
|
|
||
| for offset in range(0, limit, batch_size): | ||
| variables = { | ||
| "first": min(batch_size, limit - offset), | ||
| "skip": offset, | ||
| } | ||
|
|
||
| response = graph_client.execute_query(query, variables) | ||
|
|
||
| if "data" in response and "proposals" in response["data"]: | ||
| proposals = response["data"]["proposals"] | ||
| all_proposals.extend(proposals) | ||
|
|
||
| # Break if we have enough proposals | ||
| if len(all_proposals) >= limit: | ||
| break | ||
| else: | ||
| logger.warning(f"Invalid response from The Graph API for {protocol}") | ||
| break | ||
|
|
||
| # Fetch votes for each proposal | ||
| for proposal in all_proposals: | ||
| if proposal_id := proposal.get("id"): | ||
| votes = self._fetch_governance_votes(protocol, proposal_id) | ||
| proposal["votes"] = votes | ||
|
|
||
| return all_proposals[:limit] | ||
| except Exception as e: | ||
| logger.error(f"Error fetching governance proposals for {protocol}: {e}") | ||
| return self._generate_sample_proposal_data(protocol, limit) |
There was a problem hiding this comment.
❌ New issue: Complex Method
ProtocolClient._fetch_governance_proposals has a cyclomatic complexity of 11, threshold = 9
| def _generate_sample_proposal_data(self, protocol: str, count: int) -> List[Dict[str, Any]]: | ||
| """Generate sample governance proposal data. | ||
|
|
||
| Args: | ||
| protocol: Protocol name (compound, uniswap, aave) | ||
| count: Number of proposals to generate | ||
|
|
||
| Returns: | ||
| List of simulated governance proposal dictionaries | ||
| """ | ||
| if protocol.lower() not in PROTOCOL_INFO: | ||
| raise ValueError(f"Unsupported protocol: {protocol}") | ||
|
|
||
| protocol_info = PROTOCOL_INFO[protocol.lower()] | ||
|
|
||
| # Sample proposal titles and descriptions for each protocol | ||
| proposal_templates = { | ||
| "compound": [ | ||
| { | ||
| "title": "Adjust the reserve factor for {asset}", | ||
| "description": "This proposal adjusts the reserve factor for {asset} from {old_value}% to {new_value}%.", | ||
| }, | ||
| { | ||
| "title": "Add support for {asset}", | ||
| "description": "This proposal adds support for {asset} with the following parameters: Collateral factor: {cf}%, Reserve factor: {rf}%, Supply cap: {cap}.", | ||
| }, | ||
| { | ||
| "title": "Update {asset} risk parameters", | ||
| "description": "This proposal updates the risk parameters for {asset}. New collateral factor: {cf}%, New reserve factor: {rf}%, New supply cap: {cap}.", | ||
| }, | ||
| { | ||
| "title": "Upgrade Comptroller implementation", | ||
| "description": "This proposal upgrades the Comptroller implementation to address {issue} and add {feature}.", | ||
| }, | ||
| { | ||
| "title": "Distribute COMP to {recipient}", | ||
| "description": "This proposal distributes {amount} COMP to {recipient} for {reason}.", | ||
| }, | ||
| ], | ||
| "uniswap": [ | ||
| { | ||
| "title": "Deploy Uniswap v3 on {chain}", | ||
| "description": "This proposal deploys Uniswap v3 on {chain} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Adjust fee tier for {pair}", | ||
| "description": "This proposal adjusts the fee tier for {pair} from {old_fee}% to {new_fee}%.", | ||
| }, | ||
| { | ||
| "title": "Add new fee tier of {fee}%", | ||
| "description": "This proposal adds a new fee tier of {fee}% for pairs with {characteristic} characteristics.", | ||
| }, | ||
| { | ||
| "title": "Allocate UNI for {program}", | ||
| "description": "This proposal allocates {amount} UNI for the {program} program over the next {duration} months.", | ||
| }, | ||
| { | ||
| "title": "Update price oracle for {pair}", | ||
| "description": "This proposal updates the price oracle for {pair} to use {oracle} with {method}.", | ||
| }, | ||
| ], | ||
| "aave": [ | ||
| { | ||
| "title": "Add {asset} as collateral", | ||
| "description": "This proposal adds {asset} as collateral with the following parameters: LTV: {ltv}%, Liquidation threshold: {lt}%, Liquidation bonus: {lb}%.", | ||
| }, | ||
| { | ||
| "title": "Update interest rate strategy for {asset}", | ||
| "description": "This proposal updates the interest rate strategy for {asset} with the following parameters: Base: {base}%, Slope1: {slope1}%, Slope2: {slope2}%, Optimal utilization: {util}%.", | ||
| }, | ||
| { | ||
| "title": "Enable borrowing for {asset}", | ||
| "description": "This proposal enables borrowing for {asset} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Deploy Aave v3 on {chain}", | ||
| "description": "This proposal deploys Aave v3 on {chain} with the following parameters: {params}.", | ||
| }, | ||
| { | ||
| "title": "Allocate {amount} AAVE to {recipient}", | ||
| "description": "This proposal allocates {amount} AAVE to {recipient} for {reason}.", | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| # Assets for each protocol | ||
| assets = { | ||
| "compound": ["USDC", "ETH", "DAI", "WBTC", "LINK", "UNI", "COMP"], | ||
| "uniswap": ["ETH/USDC", "ETH/DAI", "WBTC/ETH", "UNI/ETH", "USDC/DAI"], | ||
| "aave": ["USDC", "ETH", "DAI", "WBTC", "LINK", "UNI", "AAVE"], | ||
| } | ||
|
|
||
| # Chains | ||
| chains = ["Arbitrum", "Optimism", "Polygon", "Base", "zkSync Era", "Avalanche"] | ||
|
|
||
| # Generate proposals | ||
| proposals = [] | ||
| for i in range(count): | ||
| # Select template | ||
| templates = proposal_templates.get(protocol.lower(), proposal_templates["compound"]) | ||
| template = random.choice(templates) | ||
|
|
||
| # Fill in template variables | ||
| title = template["title"] | ||
| description = template["description"] | ||
|
|
||
| # Replace placeholders | ||
| if "{asset}" in title or "{asset}" in description: | ||
| asset = random.choice(assets.get(protocol.lower(), assets["compound"])) | ||
| title = title.replace("{asset}", asset) | ||
| description = description.replace("{asset}", asset) | ||
|
|
||
| if "{pair}" in title or "{pair}" in description: | ||
| pair = random.choice(assets.get("uniswap", assets["compound"])) | ||
| title = title.replace("{pair}", pair) | ||
| description = description.replace("{pair}", pair) | ||
|
|
||
| if "{chain}" in title or "{chain}" in description: | ||
| chain = random.choice(chains) | ||
| title = title.replace("{chain}", chain) | ||
| description = description.replace("{chain}", chain) | ||
|
|
||
| if "{amount}" in title or "{amount}" in description: | ||
| amount = f"{random.randint(10000, 1000000):,}" | ||
| title = title.replace("{amount}", amount) | ||
| description = description.replace("{amount}", amount) | ||
|
|
||
| if "{recipient}" in title or "{recipient}" in description: | ||
| recipient = f"0x{random.randint(0, 0xFFFFFFFF):08x}" | ||
| title = title.replace("{recipient}", recipient) | ||
| description = description.replace("{recipient}", recipient) | ||
|
|
||
| if "{reason}" in description: | ||
| reasons = [ | ||
| "community development", | ||
| "grants program", | ||
| "protocol improvements", | ||
| "security audits", | ||
| "bug bounties", | ||
| ] | ||
| reason = random.choice(reasons) | ||
| description = description.replace("{reason}", reason) | ||
|
|
||
| if "{old_value}" in description and "{new_value}" in description: | ||
| old_value = random.randint(5, 20) | ||
| new_value = random.randint(5, 20) | ||
| while new_value == old_value: | ||
| new_value = random.randint(5, 20) | ||
| description = description.replace("{old_value}", str(old_value)) | ||
| description = description.replace("{new_value}", str(new_value)) | ||
|
|
||
| if "{cf}" in description: | ||
| cf = random.randint(50, 85) | ||
| description = description.replace("{cf}", str(cf)) | ||
|
|
||
| if "{rf}" in description: | ||
| rf = random.randint(5, 25) | ||
| description = description.replace("{rf}", str(rf)) | ||
|
|
||
| if "{cap}" in description: | ||
| cap = f"{random.randint(1, 100):,}M" | ||
| description = description.replace("{cap}", cap) | ||
|
|
||
| if "{issue}" in description: | ||
| issues = ["gas optimization", "security vulnerability", "accounting error", "protocol efficiency"] | ||
| issue = random.choice(issues) | ||
| description = description.replace("{issue}", issue) | ||
|
|
||
| if "{feature}" in description: | ||
| features = [ | ||
| "improved liquidation mechanism", | ||
| "better interest rate model", | ||
| "new risk management features", | ||
| "enhanced governance", | ||
| ] | ||
| feature = random.choice(features) | ||
| description = description.replace("{feature}", feature) | ||
|
|
||
| if "{old_fee}" in description and "{new_fee}" in description: | ||
| old_fee = random.choice([0.05, 0.1, 0.3, 1.0]) | ||
| new_fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| while new_fee == old_fee: | ||
| new_fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| description = description.replace("{old_fee}", str(old_fee)) | ||
| description = description.replace("{new_fee}", str(new_fee)) | ||
|
|
||
| if "{fee}" in description: | ||
| fee = random.choice([0.01, 0.05, 0.1, 0.3, 1.0]) | ||
| description = description.replace("{fee}", str(fee)) | ||
|
|
||
| if "{characteristic}" in description: | ||
| characteristics = ["high volatility", "stable coin", "low liquidity", "high volume"] | ||
| characteristic = random.choice(characteristics) | ||
| description = description.replace("{characteristic}", characteristic) | ||
|
|
||
| if "{program}" in description: | ||
| programs = ["liquidity mining", "developer grants", "ecosystem fund", "education"] | ||
| program = random.choice(programs) | ||
| description = description.replace("{program}", program) | ||
|
|
||
| if "{duration}" in description: | ||
| duration = random.randint(3, 24) | ||
| description = description.replace("{duration}", str(duration)) | ||
|
|
||
| if "{oracle}" in description: | ||
| oracles = ["Chainlink", "Uniswap TWAP", "Band Protocol", "API3"] | ||
| oracle = random.choice(oracles) | ||
| description = description.replace("{oracle}", oracle) | ||
|
|
||
| if "{method}" in description: | ||
| methods = ["time-weighted average", "volume-weighted average", "exponential moving average"] | ||
| method = random.choice(methods) | ||
| description = description.replace("{method}", method) | ||
|
|
||
| if "{ltv}" in description: | ||
| ltv = random.randint(50, 85) | ||
| description = description.replace("{ltv}", str(ltv)) | ||
|
|
||
| if "{lt}" in description: | ||
| lt = random.randint(60, 90) | ||
| description = description.replace("{lt}", str(lt)) | ||
|
|
||
| if "{lb}" in description: | ||
| lb = random.randint(5, 15) | ||
| description = description.replace("{lb}", str(lb)) | ||
|
|
||
| if "{base}" in description: | ||
| base = random.randint(0, 5) | ||
| description = description.replace("{base}", str(base)) | ||
|
|
||
| if "{slope1}" in description: | ||
| slope1 = random.randint(5, 15) | ||
| description = description.replace("{slope1}", str(slope1)) | ||
|
|
||
| if "{slope2}" in description: | ||
| slope2 = random.randint(50, 150) | ||
| description = description.replace("{slope2}", str(slope2)) | ||
|
|
||
| if "{util}" in description: | ||
| util = random.randint(70, 90) | ||
| description = description.replace("{util}", str(util)) | ||
|
|
||
| if "{params}" in description: | ||
| params = "standard protocol parameters" | ||
| description = description.replace("{params}", params) | ||
|
|
||
| # Generate random vote counts | ||
| for_votes = random.randint(100000, 1000000) | ||
| against_votes = random.randint(10000, for_votes) | ||
| abstain_votes = random.randint(1000, 100000) | ||
|
|
||
| # Generate timestamps | ||
| end_date = datetime.now() - timedelta(days=random.randint(1, 365)) | ||
| start_date = end_date - timedelta(days=random.randint(3, 7)) | ||
| created_date = start_date - timedelta(days=random.randint(1, 3)) | ||
|
|
||
| # Generate proposal | ||
| proposal = { | ||
| "id": str(count - i), # Newest proposals first | ||
| "title": title, | ||
| "description": description, | ||
| "proposer": f"0x{random.randint(0, 0xFFFFFFFF):08x}", | ||
| "targets": [f"0x{random.randint(0, 0xFFFFFFFF):08x}"], | ||
| "values": ["0"], | ||
| "signatures": [f"function{random.randint(1, 5)}(address,uint256)"], | ||
| "calldatas": [f"0x{random.randint(0, 0xFFFFFFFF):08x}"], | ||
| "startBlock": random.randint(10000000, 15000000), | ||
| "endBlock": random.randint(15000001, 20000000), | ||
| "forVotes": str(for_votes), | ||
| "againstVotes": str(against_votes), | ||
| "abstainVotes": str(abstain_votes), | ||
| "canceled": False, | ||
| "queued": random.random() > 0.1, | ||
| "executed": random.random() > 0.2, | ||
| "eta": int((end_date + timedelta(days=2)).timestamp()), | ||
| "createdAt": int(created_date.timestamp()), | ||
| "updatedAt": int(end_date.timestamp()), | ||
| "votes": self._generate_sample_vote_data(protocol, count - i), | ||
| } | ||
|
|
||
| proposals.append(proposal) | ||
|
|
||
| return proposals |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
ProtocolClient._generate_sample_proposal_data has 3 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
| def _fetch_governance_proposals(self, protocol: str, limit: int) -> List[Dict[str, Any]]: | ||
| """Fetch governance proposals from The Graph API. | ||
|
|
||
| Args: | ||
| protocol: Protocol name (compound, uniswap, aave) | ||
| limit: Maximum number of proposals to return | ||
|
|
||
| Returns: | ||
| List of governance proposal dictionaries | ||
| """ | ||
| if protocol.lower() not in self.graph_clients: | ||
| logger.warning(f"No Graph client available for {protocol}") | ||
| return self._generate_sample_proposal_data(protocol, limit) | ||
|
|
||
| graph_client = self.graph_clients[protocol.lower()] | ||
| query = GOVERNANCE_QUERIES.get(protocol.lower(), "") | ||
|
|
||
| if not query: | ||
| logger.warning(f"No GraphQL query defined for {protocol}") | ||
| return self._generate_sample_proposal_data(protocol, limit) | ||
|
|
||
| try: | ||
| # Batch fetch proposals | ||
| batch_size = 100 | ||
| all_proposals = [] | ||
|
|
||
| for offset in range(0, limit, batch_size): | ||
| variables = { | ||
| "first": min(batch_size, limit - offset), | ||
| "skip": offset, | ||
| } | ||
|
|
||
| response = graph_client.execute_query(query, variables) | ||
|
|
||
| if "data" in response and "proposals" in response["data"]: | ||
| proposals = response["data"]["proposals"] | ||
| all_proposals.extend(proposals) | ||
|
|
||
| # Break if we have enough proposals | ||
| if len(all_proposals) >= limit: | ||
| break | ||
| else: | ||
| logger.warning(f"Invalid response from The Graph API for {protocol}") | ||
| break | ||
|
|
||
| # Fetch votes for each proposal | ||
| for proposal in all_proposals: | ||
| if proposal_id := proposal.get("id"): | ||
| votes = self._fetch_governance_votes(protocol, proposal_id) | ||
| proposal["votes"] = votes | ||
|
|
||
| return all_proposals[:limit] | ||
| except Exception as e: | ||
| logger.error(f"Error fetching governance proposals for {protocol}: {e}") | ||
| return self._generate_sample_proposal_data(protocol, limit) |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
ProtocolClient._fetch_governance_proposals has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function
Resolved issues in the following files with DeepSource Autofix: 1. src/governance_token_analyzer/core/api_client/data_fetcher.py 2. src/governance_token_analyzer/visualization/report_generator/comprehensive_report.py 3. src/governance_token_analyzer/visualization/report_generator/historical_report_generator.py 4. src/governance_token_analyzer/visualization/report_generator/report_generator_base.py
left a comment
There was a problem hiding this comment.
Gates Failed
New code is healthy
(6 new files with code health below 9.00)
Enforce critical code health rules
(6 files with Bumpy Road Ahead, Deep, Nested Complexity)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 6 rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 4 rules | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 6 rules | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 5 rules | 10.00 → 7.96 | Suppress |
| ethereum_client.py | 3 rules | 10.00 → 8.55 | Suppress |
| response_parser.py | 2 rules | 10.00 → 8.99 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| historical_report_generator.py | 2 critical rules | 10.00 → 6.84 | Suppress |
| protocol_client.py | 1 critical rule | 10.00 → 7.28 | Suppress |
| comprehensive_report.py | 1 critical rule | 10.00 → 7.47 | Suppress |
| html_report_generator.py | 1 critical rule | 10.00 → 7.96 | Suppress |
| response_parser.py | 1 critical rule | 10.00 → 8.99 | Suppress |
| data_fetcher.py | 1 critical rule | 10.00 → 9.84 | Suppress |
Quality Gate Profile: The Bare Minimum
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| @@ -0,0 +1,640 @@ | |||
| #!/usr/bin/env python | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 6.45 across 11 functions. The mean complexity threshold is 4
| def generate_comprehensive_report( | ||
| protocol: str, | ||
| current_data: Dict[str, Any], | ||
| governance_data: List[Dict[str, Any]], | ||
| votes_data: List[Dict[str, Any]], | ||
| historical_data: Optional[Dict[str, Any]] = None, | ||
| output_dir: str = "reports", | ||
| output_format: str = "html", | ||
| output_path: Optional[str] = None, | ||
| ) -> str: | ||
| """Generate a comprehensive analysis report. | ||
|
|
||
| This function creates a complete analysis including current data, | ||
| governance proposals, voting data, and historical analysis if available. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| current_data: Current distribution data | ||
| governance_data: Governance proposals data | ||
| votes_data: Voting data | ||
| historical_data: Historical data dictionary | ||
| output_dir: Directory where reports will be saved | ||
| output_format: Output format ('html', 'json', 'pdf') | ||
| output_path: Path to save the report | ||
|
|
||
| Returns: | ||
| Path to the generated report | ||
| """ | ||
| # Set up output directory and report path | ||
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | ||
| report_dir = output_dir | ||
| os.makedirs(report_dir, exist_ok=True) | ||
|
|
||
| # Create visualization directory | ||
| viz_dir = os.path.join(report_dir, "visualizations") | ||
| os.makedirs(viz_dir, exist_ok=True) | ||
|
|
||
| # Set the output path if not provided | ||
| if output_path is None: | ||
| output_path = os.path.join(report_dir, f"{protocol}_comprehensive_report_{timestamp}.{output_format.lower()}") | ||
|
|
||
| # Generate report based on format | ||
| if output_format == "html": | ||
| return _generate_comprehensive_html_report( | ||
| protocol=protocol, | ||
| current_data=current_data, | ||
| governance_data=governance_data, | ||
| votes_data=votes_data, | ||
| historical_data=historical_data, | ||
| output_path=output_path, | ||
| viz_dir=viz_dir, | ||
| timestamp=timestamp, | ||
| ) | ||
| if output_format == "json": | ||
| # JSON report generation | ||
| # ... (implementation details) | ||
| return "JSON report generation not implemented yet" | ||
| if output_format == "pdf": | ||
| # PDF report generation | ||
| # ... (implementation details) | ||
| raise NotImplementedError("PDF report generation not implemented yet") | ||
| raise ValueError(f"Unsupported format: {output_format}") |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
generate_comprehensive_report has 8 arguments, threshold = 4
| @@ -0,0 +1,414 @@ | |||
| #!/usr/bin/env python | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 6.50 across 8 functions. The mean complexity threshold is 4
| def generate_historical_analysis_report( | ||
| protocol: str, | ||
| historical_data: Dict[str, Any], | ||
| output_dir: str = "reports", | ||
| output_format: str = "html", | ||
| output_path: Optional[str] = None, | ||
| ) -> str: | ||
| """Generate a historical analysis report. | ||
|
|
||
| Args: | ||
| protocol: Protocol name | ||
| historical_data: Historical data dictionary | ||
| output_dir: Directory where reports will be saved | ||
| output_format: Output format ('html', 'json', 'pdf') | ||
| output_path: Path to save the report | ||
|
|
||
| Returns: | ||
| Path to the generated report | ||
| """ | ||
| # Set up output directory and report path | ||
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | ||
| report_dir = output_dir | ||
| os.makedirs(report_dir, exist_ok=True) | ||
|
|
||
| # Create visualization directory | ||
| viz_dir = os.path.join(report_dir, "visualizations") | ||
| os.makedirs(viz_dir, exist_ok=True) | ||
|
|
||
| # Set the output path if not provided | ||
| if output_path is None: | ||
| output_path = os.path.join(report_dir, f"{protocol}_historical_report_{timestamp}.{output_format.lower()}") | ||
|
|
||
| # Generate report based on format | ||
| if output_format == "html": | ||
| return _generate_historical_html_report( | ||
| protocol=protocol, | ||
| historical_data=historical_data, | ||
| output_path=output_path, | ||
| viz_dir=viz_dir, | ||
| timestamp=timestamp, | ||
| ) | ||
| if output_format == "json": | ||
| # JSON report generation | ||
| # ... (implementation details) | ||
| return "JSON report generation not implemented yet" | ||
| if output_format == "pdf": | ||
| # PDF report generation | ||
| # ... (implementation details) | ||
| raise NotImplementedError("PDF report generation not implemented yet") | ||
| raise ValueError(f"Unsupported format: {output_format}") |
There was a problem hiding this comment.
❌ New issue: Excess Number of Function Arguments
generate_historical_analysis_report has 5 arguments, threshold = 4
Summary by Sourcery
Refactor large modules into smaller, focused packages to enforce a 500-line file size limit and improve maintainability while preserving backward compatibility.
Enhancements:
Documentation:
Chores: