From 10f56170bf0288324e3a7466c68c9d4af4a288b2 Mon Sep 17 00:00:00 2001 From: Thakor-Yashpal Date: Wed, 5 Nov 2025 02:47:35 +0530 Subject: [PATCH] Repo_growth chart with DI container setup R --- .../src/bot/commands/analytics_commands.py | 96 ++++++++++++++----- discord_bot/src/core/container.py | 46 +++++++++ discord_bot/src/core/singleton.py | 21 ++++ discord_bot/src/services/__init__.py | 17 ++++ discord_bot/src/services/analytics_service.py | 0 5 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 discord_bot/src/core/container.py create mode 100644 discord_bot/src/core/singleton.py create mode 100644 discord_bot/src/services/analytics_service.py diff --git a/discord_bot/src/bot/commands/analytics_commands.py b/discord_bot/src/bot/commands/analytics_commands.py index bc3731c..1b2013b 100644 --- a/discord_bot/src/bot/commands/analytics_commands.py +++ b/discord_bot/src/bot/commands/analytics_commands.py @@ -1,32 +1,34 @@ -""" -Analytics Commands Module - -Handles analytics and visualization-related Discord commands. -""" - import discord from discord import app_commands -from ...utils.analytics import create_top_contributors_chart, create_activity_comparison_chart, create_activity_trend_chart, create_time_series_chart + +from src.core.interfaces import IRepoAnalyticsService + +from src.utils.analytics.chart_generator import ( + create_top_contributors_chart, + create_activity_comparison_chart, + create_activity_trend_chart, + create_time_series_chart, + create_repo_growth_chart +) from shared.firestore import get_document class AnalyticsCommands: - """Handles analytics and visualization Discord commands.""" - def __init__(self, bot): + def __init__(self, bot: discord.Client, analytics_service: IRepoAnalyticsService): self.bot = bot + self.analytics_service = analytics_service def register_commands(self): - """Register all analytics commands with the bot.""" self.bot.tree.add_command(self._show_top_contributors_command()) self.bot.tree.add_command(self._show_activity_comparison_command()) self.bot.tree.add_command(self._show_activity_trends_command()) self.bot.tree.add_command(self._show_time_series_command()) - + self.bot.tree.add_command(self._repo_growth_command()) + def _show_top_contributors_command(self): - """Create the show-top-contributors command.""" @app_commands.command(name="show-top-contributors", description="Show top contributors chart") async def show_top_contributors(interaction: discord.Interaction): - await interaction.response.defer() + await interaction.response.defer(ephemeral=True) try: analytics_data = get_document('repo_stats', 'analytics') @@ -42,7 +44,7 @@ async def show_top_contributors(interaction: discord.Interaction): return file = discord.File(chart_buffer, filename="top_contributors.png") - await interaction.followup.send("Top contributors by PR count:", file=file) + await interaction.followup.send("Top contributors by PR count:", file=file, ephemeral=True) except Exception as e: print(f"Error in show-top-contributors command: {e}") @@ -51,10 +53,9 @@ async def show_top_contributors(interaction: discord.Interaction): return show_top_contributors def _show_activity_comparison_command(self): - """Create the show-activity-comparison command.""" @app_commands.command(name="show-activity-comparison", description="Show activity comparison chart") async def show_activity_comparison(interaction: discord.Interaction): - await interaction.response.defer() + await interaction.response.defer(ephemeral=True) try: analytics_data = get_document('repo_stats', 'analytics') @@ -70,7 +71,7 @@ async def show_activity_comparison(interaction: discord.Interaction): return file = discord.File(chart_buffer, filename="activity_comparison.png") - await interaction.followup.send("Activity comparison chart:", file=file) + await interaction.followup.send("Activity comparison chart:", file=file, ephemeral=True) except Exception as e: print(f"Error in show-activity-comparison command: {e}") @@ -79,10 +80,9 @@ async def show_activity_comparison(interaction: discord.Interaction): return show_activity_comparison def _show_activity_trends_command(self): - """Create the show-activity-trends command.""" @app_commands.command(name="show-activity-trends", description="Show recent activity trends") async def show_activity_trends(interaction: discord.Interaction): - await interaction.response.defer() + await interaction.response.defer(ephemeral=True) try: analytics_data = get_document('repo_stats', 'analytics') @@ -98,7 +98,7 @@ async def show_activity_trends(interaction: discord.Interaction): return file = discord.File(chart_buffer, filename="activity_trends.png") - await interaction.followup.send("Recent activity trends:", file=file) + await interaction.followup.send("Recent activity trends:", file=file, ephemeral=True) except Exception as e: print(f"Error in show-activity-trends command: {e}") @@ -107,17 +107,15 @@ async def show_activity_trends(interaction: discord.Interaction): return show_activity_trends def _show_time_series_command(self): - """Create the show-time-series command.""" @app_commands.command(name="show-time-series", description="Show time series chart with customizable metrics and date range") @app_commands.describe( metrics="Comma-separated metrics to display (prs,issues,commits,total)", days="Number of days to show (7-90, default: 30)" ) async def show_time_series(interaction: discord.Interaction, metrics: str = "prs,issues,commits", days: int = 30): - await interaction.response.defer() + await interaction.response.defer(ephemeral=True) try: - # Validate inputs if days < 7 or days > 90: await interaction.followup.send("Days must be between 7 and 90.", ephemeral=True) return @@ -148,10 +146,58 @@ async def show_time_series(interaction: discord.Interaction, metrics: str = "prs return file = discord.File(chart_buffer, filename="time_series.png") - await interaction.followup.send(f"Time series chart for last {days} days:", file=file) + await interaction.followup.send(f"Time series chart for last {days} days:", file=file, ephemeral=True) except Exception as e: print(f"Error in show-time-series command: {e}") await interaction.followup.send("Error generating time series chart.", ephemeral=True) - return show_time_series \ No newline at end of file + return show_time_series + + def _repo_growth_command(self): + + @app_commands.command( + name="repo_growth", + description="Shows a chart of the repository's cumulative growth." + ) + @app_commands.checks.has_permissions(administrator=True) + async def cmd(interaction: discord.Interaction): + try: + await interaction.response.defer(thinking=True, ephemeral=True) + + stats = await self.analytics_service.get_code_frequency_stats() + + if not stats: + await interaction.followup.send( + "Sorry, I couldn't fetch the repository stats. This can happen if GitHub " + "is caching the data. Please try again in a few minutes.", + ephemeral=True + ) + return + + chart_buffer = create_repo_growth_chart(stats) + + if chart_buffer is None: + await interaction.followup.send("An error occurred while generating the chart.", ephemeral=True) + return + + file = discord.File(fp=chart_buffer, filename="repo_growth.png") + + embed = discord.Embed( + title="Repository Growth", + description="Here is the cumulative net line-of-code growth over time.", + color=discord.Color.blue() + ) + embed.set_image(url="attachment://repo_growth.png") + embed.set_footer(text="Data sourced from GitHub API") + + await interaction.followup.send(embed=embed, file=file, ephemeral=True) + + except Exception as e: + print(f"Error in /repo_growth command: {e}") + if not interaction.response.is_done(): + await interaction.response.send_message("An unexpected error occurred.", ephemeral=True) + else: + await interaction.followup.send("An unexpected error occurred.", ephemeral=True) + + return cmd \ No newline at end of file diff --git a/discord_bot/src/core/container.py b/discord_bot/src/core/container.py new file mode 100644 index 0000000..026ed23 --- /dev/null +++ b/discord_bot/src/core/container.py @@ -0,0 +1,46 @@ +from .singleton import Singleton +from .interfaces import ( + IRepoAnalyticsService, + IGuildService, + IGitHubService, + IRoleService, + INotificationService +) +from .services import ( + GitHubAnalyticsService, + GuildService, + GitHubService, + RoleService, + NotificationService +) + +class ServiceContainer: + def __init__(self): + self._services = {} + + def register_singleton(self, interface, implementation): + self._services[interface] = Singleton(implementation) + print(f"Registered {interface.__name__} -> {implementation.__name__}") + + def resolve(self, interface): + try: + implementation_factory = self._services[interface] + return implementation_factory.get_instance() + except KeyError: + raise Exception(f"No service registered for interface: {interface.__name__}") + +def setup_dependencies() -> ServiceContainer: + container = ServiceContainer() + + # container.register_singleton(IGuildService, GuildService) + # container.register_singleton(IGitHubService, GitHubService) + # container.register_singleton(IRoleService, RoleService) + # container.register_singleton(INotificationService, NotificationService) + + container.register_singleton(IRepoAnalyticsService, GitHubAnalyticsService) + + print("-" * 30) + print("Service container setup complete.") + print("-" * 30) + + return container \ No newline at end of file diff --git a/discord_bot/src/core/singleton.py b/discord_bot/src/core/singleton.py new file mode 100644 index 0000000..8d1f775 --- /dev/null +++ b/discord_bot/src/core/singleton.py @@ -0,0 +1,21 @@ +""" +Singleton wrapper for the service container. +""" + +class Singleton: + """ + A wrapper to ensure a class is only instantiated once + by the ServiceContainer. + """ + def __init__(self, implementation_class): + self._implementation_class = implementation_class + self._instance = None + + def get_instance(self): + """ + Get the singleton instance. + Creates it if it doesn't exist yet. + """ + if self._instance is None: + self._instance = self._implementation_class() + return self._instance \ No newline at end of file diff --git a/discord_bot/src/services/__init__.py b/discord_bot/src/services/__init__.py index e69de29..9d831d3 100644 --- a/discord_bot/src/services/__init__.py +++ b/discord_bot/src/services/__init__.py @@ -0,0 +1,17 @@ +""" +Service implementations. +""" + +from .guild_service import GuildService +from .github_service import GitHubService +from .role_service import RoleService +from .notification_service import NotificationService +from .analytics_service import GitHubAnalyticsService # ✨ ADD THIS LINE + +__all__ = [ + 'GuildService', + 'GitHubService', + 'RoleService', + 'NotificationService', + 'GitHubAnalyticsService', # ✨ AND ADD THIS LINE +] \ No newline at end of file diff --git a/discord_bot/src/services/analytics_service.py b/discord_bot/src/services/analytics_service.py new file mode 100644 index 0000000..e69de29