Skip to content

Refactor/file size limit - #45

Open
d-ulker wants to merge 14 commits into
mainfrom
refactor/file-size-limit
Open

Refactor/file size limit#45
d-ulker wants to merge 14 commits into
mainfrom
refactor/file-size-limit

Conversation

@d-ulker

@d-ulker d-ulker commented Jun 19, 2025

Copy link
Copy Markdown
Owner

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:

  • Split core/api_client.py into a dedicated package with base_client, ethereum_client, protocol_client, data_fetcher, response_parser, and graph_client modules
  • Refactor core/data_processor.py into a package containing data_processor_base, metrics_processor, data_standardizer, report_processor, and visualization_processor modules
  • Decompose visualization/report_generator.py into a package with report_generator_base, html_report_generator, historical_report_generator, and comprehensive_report modules

Documentation:

  • Add docs/refactoring_plan.md detailing the 500-line file size limit refactoring strategy

Chores:

  • Introduce init.py stubs in refactored directories to re-export existing APIClient, DataProcessor, and ReportGenerator classes for backward compatibility

@d-ulker
d-ulker requested a review from Copilot June 19, 2025 10:16
@d-ulker d-ulker self-assigned this Jun 19, 2025
@sourcery-ai

sourcery-ai Bot commented Jun 19, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The 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

Change Details Files
Split monolithic API client into core.api_client sub-package
  • Extract base APIClient into base_client.py
  • Move Ethereum-specific logic to ethereum_client.py
  • Move protocol-specific logic to protocol_client.py
  • Extract data fetching into data_fetcher.py and response parsing into response_parser.py
  • Introduce TheGraphAPI client in graph_client.py
src/governance_token_analyzer/core/api_client/base_client.py
src/governance_token_analyzer/core/api_client/ethereum_client.py
src/governance_token_analyzer/core/api_client/protocol_client.py
src/governance_token_analyzer/core/api_client/data_fetcher.py
src/governance_token_analyzer/core/api_client/response_parser.py
src/governance_token_analyzer/core/api_client/graph_client.py
src/governance_token_analyzer/core/api_client/__init__.py
src/governance_token_analyzer/core/api_client.py
Decompose the data processor into core.data_processor sub-package
  • Extract DataProcessor core to data_processor_base.py
  • Move metrics calculations to metrics_processor.py
  • Move visualization helpers to visualization_processor.py
  • Move report generation logic to report_processor.py
  • Add data_standardizer.py for holder data standardization
src/governance_token_analyzer/core/data_processor/data_processor_base.py
src/governance_token_analyzer/core/data_processor/metrics_processor.py
src/governance_token_analyzer/core/data_processor/visualization_processor.py
src/governance_token_analyzer/core/data_processor/report_processor.py
src/governance_token_analyzer/core/data_processor/data_standardizer.py
src/governance_token_analyzer/core/data_processor/__init__.py
src/governance_token_analyzer/core/data_processor.py
Refactor report generator into visualization.report_generator sub-package
  • Introduce report_generator_base.py with core ReportGenerator
  • Move HTML-specific logic to html_report_generator.py
  • Move historical report logic to historical_report_generator.py
  • Add comprehensive_report.py for combined reporting
src/governance_token_analyzer/visualization/report_generator/report_generator_base.py
src/governance_token_analyzer/visualization/report_generator/html_report_generator.py
src/governance_token_analyzer/visualization/report_generator/historical_report_generator.py
src/governance_token_analyzer/visualization/report_generator/comprehensive_report.py
src/governance_token_analyzer/visualization/report_generator/__init__.py
Preserve public API and update imports via package init.py stubs
  • Add init.py to each sub-package to re-export key classes
  • Replace monolithic modules with stubs importing from new structure
  • Define all lists to maintain backward compatibility
src/governance_token_analyzer/core/api_client/__init__.py
src/governance_token_analyzer/core/data_processor/__init__.py
src/governance_token_analyzer/visualization/report_generator/__init__.py
Document the refactoring plan
  • Add docs/refactoring_plan.md outlining strategy, roadmap, and target file splits
docs/refactoring_plan.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

This commit fixes the style issues introduced in e5a85f2 according to the output
from Ruff Formatter.

Details: #45
@deepsource-io

deepsource-io Bot commented Jun 19, 2025

Copy link
Copy Markdown
Contributor

Here's the code health analysis summary for commits 7b9263c..5af2b30. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Shell LogoShell✅ SuccessView Check ↗
DeepSource Python LogoPython❌ Failure
❗ 348 occurences introduced
🎯 137 occurences resolved
View Check ↗
DeepSource Test coverage LogoTest coverage⚠️ Artifact not reportedTimed out: Artifact was never reportedView Check ↗
DeepSource Secrets LogoSecrets✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py file has been completely restructured. Its functionality is now distributed across a new api_client package, 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.py file has undergone a significant refactor. Its responsibilities are now encapsulated within a new data_processor package, 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_generator package. 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.md document 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

  1. 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.

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +422 to +494
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: _create_governance_visualization,_create_votes_visualization

Suppress

Comment on lines +497 to +570
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
_create_votes_visualization has a cyclomatic complexity of 13, threshold = 9

Suppress

Comment on lines +422 to +494
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
_create_governance_visualization has a cyclomatic complexity of 11, threshold = 9

Suppress

Comment on lines +359 to +419
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
_create_token_distribution_visualization has a cyclomatic complexity of 10, threshold = 9

Suppress

Comment on lines +299 to +356
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_visualizations_section has a cyclomatic complexity of 9, threshold = 9

Suppress

# Group votes by proposal
proposals = {}
for vote in votes_data:
if "proposal_id" in vote and "voter" in vote and "support" in vote:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Conditional
_create_votes_visualization has 1 complex conditionals with 2 branches, threshold = 2

Suppress

Comment on lines +299 to +356
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +86 to +156
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
_generate_comprehensive_html_report has 8 arguments, threshold = 4

Suppress

Comment on lines +299 to +356
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
create_visualizations_section has 6 arguments, threshold = 4

Suppress

@@ -0,0 +1,717 @@
#!/usr/bin/env python

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.81 across 16 functions. The mean complexity threshold is 4

Suppress

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 2 to -6

This module provides a unified interface for fetching governance token data
from various blockchain APIs including Etherscan, The Graph, and Alchemy.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +129 to +131
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +225 to +229
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/governance_token_analyzer/core/api_client/protocol_client.py Outdated
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]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Low code quality found in ProtocolClient._generate_sample_proposal_data - 7% (low-code-quality)


ExplanationThe 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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:

holders = []

try:
if api_type == "etherscan":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:

Token supply as an integer
"""
try:
if api_type == "etherscan":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Remove empty elif clause (remove-pass-elif)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +7
This file is maintained for backward compatibility and imports from the api_client package.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.
'''

Comment on lines +223 to +227
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 '✗'}}")

Comment on lines +50 to +51
logger.warning(f"No token data provided for {protocol}")
return {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a check to ensure that token_holders is a list before attempting to calculate metrics.

if not isinstance(token_holders, list):
    logger.warning(f"Invalid token_holders data type: {type(token_holders)}")
    return result

Comment on lines +114 to +115
logger.warning(f"No protocol data provided for {protocol}")
return {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a check to ensure that proposals is a list before attempting to calculate metrics.

if not isinstance(proposals, list):
    logger.warning(f"Invalid proposals data type: {type(proposals)}")
    return result

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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."""

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/governance_token_analyzer/core/data_processor/data_processor_base.py Outdated
Comment on lines +128 to +134
# Combine metrics
combined_metrics = {
**distribution_result.get("metrics", {}),
**governance_result.get("metrics", {}),
}

# Generate visualizations

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
        )

Comment thread src/governance_token_analyzer/core/data_processor/data_processor_base.py Outdated
Comment on lines +66 to +67
# Calculate Gini coefficient
gini = self._calculate_gini_coefficient(balances)

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/governance_token_analyzer/core/api_client/protocol_client.py Outdated
Comment thread src/governance_token_analyzer/core/api_client/protocol_client.py Outdated
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]]:

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Low code quality found in ProtocolClient._generate_sample_proposal_data - 7% (low-code-quality)


ExplanationThe 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):

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:

holders = []

try:
if api_type == "etherscan":

ghost Jun 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."""

Comment thread src/governance_token_analyzer/core/data_processor/metrics_processor.py Outdated
# Avoid division by zero
if total_tokens == 0 or holder_count == 0:
logger.warning("No tokens or holders found")
return {

Copilot AI Jun 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/governance_token_analyzer/core/data_processor/metrics_processor.py Outdated
deepsource-autofix[bot] and others added 3 commits June 19, 2025 10:37
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
This commit fixes the style issues introduced in 2a2cec8 according to the output
from Ruff Formatter.

Details: #45
…or_base.py

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +345 to +423
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}")

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
process_time_series has a cyclomatic complexity of 11, threshold = 9

Suppress

Comment on lines +213 to +289
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_governance_visualization has a cyclomatic complexity of 11, threshold = 9

Suppress

Comment on lines +146 to +210
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_token_distribution_visualization has a cyclomatic complexity of 11, threshold = 9

Suppress

Comment on lines +21 to +94
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,
)

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
generate_comprehensive_html_report has a cyclomatic complexity of 9, threshold = 9

Suppress

"""
visualizations = []

if not current_data or "token_holders" not in current_data or not current_data["token_holders"]:

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Conditional
create_token_distribution_visualization has 1 complex conditionals with 2 branches, threshold = 2

Suppress

Comment on lines +18 to +60
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
ResponseParser.parse_token_holders has a cyclomatic complexity of 12, threshold = 9

Suppress

Comment on lines +167 to +192
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
ResponseParser.parse_token_balance has a cyclomatic complexity of 9, threshold = 9

Suppress

Comment on lines +167 to +192
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +18 to +60
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +138 to +164
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

This commit fixes the style issues introduced in 43a5a08 according to the output
from Ruff Formatter.

Details: #45

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Deniz Ülker and others added 2 commits June 19, 2025 13:41
…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>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +242 to +286
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: create_dataframe_time_series_tables,create_dict_time_series_tables

Suppress

Comment on lines +166 to +239
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_time_series_section has a cyclomatic complexity of 13, threshold = 9

Suppress

Comment on lines +289 to +331
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_dict_time_series_tables has a cyclomatic complexity of 9, threshold = 9

Suppress

Comment on lines +242 to +286
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
create_dataframe_time_series_tables has a cyclomatic complexity of 9, threshold = 9

Suppress

Comment on lines +166 to +239
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +289 to +331
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +289 to +331
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Deep, Nested Complexity
create_dict_time_series_tables has a nested complexity depth of 6, threshold = 4

Suppress

Comment on lines +242 to +286
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Deep, Nested Complexity
create_dataframe_time_series_tables has a nested complexity depth of 5, threshold = 4

Suppress

Comment on lines +71 to +126
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
_generate_historical_html_report has 5 arguments, threshold = 4

Suppress

Comment on lines +385 to +415
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
_create_time_series_chart has 5 arguments, threshold = 4

Suppress

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +160 to +442
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
ProtocolClient._generate_sample_proposal_data has a cyclomatic complexity of 40, threshold = 9

Suppress

Comment on lines +67 to +121
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)

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Method
ProtocolClient._fetch_governance_proposals has a cyclomatic complexity of 11, threshold = 9

Suppress

Comment on lines +160 to +442
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

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

Comment on lines +67 to +121
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)

ghost Jun 19, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 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

Suppress

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

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ghost Jun 21, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 6.45 across 11 functions. The mean complexity threshold is 4

Suppress

Comment on lines +20 to +81
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}")

ghost Jun 21, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
generate_comprehensive_report has 8 arguments, threshold = 4

Suppress

@@ -0,0 +1,414 @@
#!/usr/bin/env python

ghost Jun 21, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 6.50 across 8 functions. The mean complexity threshold is 4

Suppress

Comment on lines +18 to +67
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}")

ghost Jun 21, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
generate_historical_analysis_report has 5 arguments, threshold = 4

Suppress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants