gitflow-analytics 3.6.2__py3-none-any.whl → 3.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. gitflow_analytics/__init__.py +8 -12
  2. gitflow_analytics/_version.py +1 -1
  3. gitflow_analytics/cli.py +151 -170
  4. gitflow_analytics/cli_wizards/install_wizard.py +5 -5
  5. gitflow_analytics/models/database.py +229 -8
  6. gitflow_analytics/security/reports/__init__.py +5 -0
  7. gitflow_analytics/security/reports/security_report.py +358 -0
  8. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/METADATA +2 -4
  9. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/RECORD +13 -24
  10. gitflow_analytics/tui/__init__.py +0 -5
  11. gitflow_analytics/tui/app.py +0 -726
  12. gitflow_analytics/tui/progress_adapter.py +0 -313
  13. gitflow_analytics/tui/screens/__init__.py +0 -8
  14. gitflow_analytics/tui/screens/analysis_progress_screen.py +0 -857
  15. gitflow_analytics/tui/screens/configuration_screen.py +0 -523
  16. gitflow_analytics/tui/screens/loading_screen.py +0 -348
  17. gitflow_analytics/tui/screens/main_screen.py +0 -321
  18. gitflow_analytics/tui/screens/results_screen.py +0 -735
  19. gitflow_analytics/tui/widgets/__init__.py +0 -7
  20. gitflow_analytics/tui/widgets/data_table.py +0 -255
  21. gitflow_analytics/tui/widgets/export_modal.py +0 -301
  22. gitflow_analytics/tui/widgets/progress_widget.py +0 -187
  23. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/WHEEL +0 -0
  24. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/entry_points.txt +0 -0
  25. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/licenses/LICENSE +0 -0
  26. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.0.dist-info}/top_level.txt +0 -0
@@ -5,20 +5,16 @@ from ._version import __version__, __version_info__
5
5
  __author__ = "Bob Matyas"
6
6
  __email__ = "bobmatnyc@gmail.com"
7
7
 
8
- from .core.analyzer import GitAnalyzer
9
- from .core.cache import GitAnalysisCache
10
- from .core.identity import DeveloperIdentityResolver
11
- from .extractors.story_points import StoryPointExtractor
12
- from .extractors.tickets import TicketExtractor
13
- from .reports.csv_writer import CSVReportGenerator
8
+ # Heavy imports removed from package __init__ for CLI performance
9
+ # Import these directly when needed in your code:
10
+ # from gitflow_analytics.core.analyzer import GitAnalyzer
11
+ # from gitflow_analytics.core.cache import GitAnalysisCache
12
+ # from gitflow_analytics.core.identity import DeveloperIdentityResolver
13
+ # from gitflow_analytics.extractors.story_points import StoryPointExtractor
14
+ # from gitflow_analytics.extractors.tickets import TicketExtractor
15
+ # from gitflow_analytics.reports.csv_writer import CSVReportGenerator
14
16
 
15
17
  __all__ = [
16
18
  "__version__",
17
19
  "__version_info__",
18
- "GitAnalyzer",
19
- "GitAnalysisCache",
20
- "DeveloperIdentityResolver",
21
- "StoryPointExtractor",
22
- "TicketExtractor",
23
- "CSVReportGenerator",
24
20
  ]
@@ -1,4 +1,4 @@
1
1
  """Version information for gitflow-analytics."""
2
2
 
3
- __version__ = "3.6.2"
3
+ __version__ = "3.7.0"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
gitflow_analytics/cli.py CHANGED
@@ -14,25 +14,28 @@ from typing import Any, Optional, cast
14
14
 
15
15
  import click
16
16
  import git
17
- import pandas as pd
18
17
  import yaml
19
18
 
20
19
  from ._version import __version__
21
20
  from .config import ConfigLoader
22
- from .core.analyzer import GitAnalyzer
23
- from .core.cache import GitAnalysisCache
24
- from .core.git_auth import preflight_git_authentication
25
- from .core.identity import DeveloperIdentityResolver
26
- from .integrations.orchestrator import IntegrationOrchestrator
27
- from .metrics.dora import DORAMetricsCalculator
28
- from .reports.analytics_writer import AnalyticsReportGenerator
29
- from .reports.csv_writer import CSVReportGenerator
30
- from .reports.json_exporter import ComprehensiveJSONExporter
31
- from .reports.narrative_writer import NarrativeReportGenerator
32
- from .reports.weekly_trends_writer import WeeklyTrendsWriter
33
- from .training.pipeline import CommitClassificationTrainer
34
21
  from .ui.progress_display import create_progress_display
35
22
 
23
+ # Heavy imports are lazy-loaded to improve CLI startup time
24
+ # These imports add 1-2 seconds to startup but are only needed for actual analysis
25
+ # from .core.analyzer import GitAnalyzer
26
+ # from .core.cache import GitAnalysisCache
27
+ # from .core.git_auth import preflight_git_authentication
28
+ # from .core.identity import DeveloperIdentityResolver
29
+ # from .integrations.orchestrator import IntegrationOrchestrator
30
+ # from .metrics.dora import DORAMetricsCalculator
31
+ # from .reports.analytics_writer import AnalyticsReportGenerator
32
+ # from .reports.csv_writer import CSVReportGenerator
33
+ # from .reports.json_exporter import ComprehensiveJSONExporter
34
+ # from .reports.narrative_writer import NarrativeReportGenerator
35
+ # from .reports.weekly_trends_writer import WeeklyTrendsWriter
36
+ # from .training.pipeline import CommitClassificationTrainer
37
+ # import pandas as pd # Only used in one location, lazy loaded
38
+
36
39
  logger = logging.getLogger(__name__)
37
40
 
38
41
 
@@ -257,64 +260,33 @@ class ImprovedErrorHandler:
257
260
  click.echo("\nFor help: gitflow-analytics help", err=True)
258
261
 
259
262
 
260
- class TUIAsDefaultGroup(click.Group):
263
+ class AnalyzeAsDefaultGroup(click.Group):
261
264
  """
262
- Custom Click group that defaults to TUI when no explicit subcommand is provided.
263
- This allows 'gitflow-analytics -c config.yaml' to launch the TUI by default.
264
- For explicit CLI analysis, use 'gitflow-analytics analyze -c config.yaml'
265
+ Custom Click group that defaults to analyze when no explicit subcommand is provided.
266
+ This allows 'gitflow-analytics -c config.yaml' to run analysis by default.
265
267
  """
266
268
 
267
269
  def parse_args(self, ctx, args):
268
- """Override parse_args to default to TUI unless explicit subcommand or CLI-only options."""
270
+ """Override parse_args to default to analyze unless explicit subcommand provided."""
269
271
  # Check if the first argument is a known subcommand
270
272
  if args and args[0] in self.list_commands(ctx):
271
273
  return super().parse_args(ctx, args)
272
274
 
273
- # Check for global options that should NOT be routed to TUI
275
+ # Check for global options that should NOT be routed to analyze
274
276
  global_options = {"--version", "--help", "-h"}
275
277
  if args and args[0] in global_options:
276
278
  return super().parse_args(ctx, args)
277
279
 
278
- # Check if we have arguments that indicate explicit CLI analysis request
279
- cli_only_indicators = {
280
- "--no-rich",
281
- "--generate-csv",
282
- "--validate-only",
283
- "--warm-cache",
284
- "--validate-cache",
285
- "--qualitative-only",
286
- }
287
-
288
- # If user explicitly requests CLI-only features, route to analyze
289
- if args and any(arg in cli_only_indicators for arg in args):
280
+ # For all other cases (including -c config.yaml), default to analyze
281
+ if args and args[0].startswith("-"):
290
282
  new_args = ["analyze"] + args
291
283
  return super().parse_args(ctx, new_args)
292
284
 
293
- # For all other cases (including -c config.yaml), default to TUI
294
- if args and args[0].startswith("-"):
295
- # Check if TUI dependencies are available
296
- try:
297
- import importlib.util
298
-
299
- textual_spec = importlib.util.find_spec("textual")
300
- # TUI is available - route to TUI
301
- if textual_spec is not None:
302
- new_args = ["tui"] + args
303
- return super().parse_args(ctx, new_args)
304
- else:
305
- # TUI not available - fallback to analyze
306
- new_args = ["analyze"] + args
307
- return super().parse_args(ctx, new_args)
308
- except (ImportError, ValueError):
309
- # TUI not available - fallback to analyze
310
- new_args = ["analyze"] + args
311
- return super().parse_args(ctx, new_args)
312
-
313
285
  # Otherwise, use default behavior
314
286
  return super().parse_args(ctx, args)
315
287
 
316
288
 
317
- @click.group(cls=TUIAsDefaultGroup, invoke_without_command=True)
289
+ @click.group(cls=AnalyzeAsDefaultGroup, invoke_without_command=True)
318
290
  @click.version_option(version=__version__, prog_name="GitFlow Analytics")
319
291
  @click.help_option("-h", "--help")
320
292
  @click.pass_context
@@ -374,119 +346,6 @@ def cli(ctx: click.Context) -> None:
374
346
  ctx.exit(0)
375
347
 
376
348
 
377
- # TUI command - Terminal User Interface
378
- @cli.command(name="tui")
379
- @click.option(
380
- "--config",
381
- "-c",
382
- type=click.Path(exists=True, path_type=Path),
383
- default=None,
384
- help="Path to YAML configuration file (optional - can be loaded in TUI)",
385
- )
386
- @click.option(
387
- "--weeks",
388
- "-w",
389
- type=int,
390
- default=None,
391
- help="Number of weeks to analyze (passed to TUI)",
392
- )
393
- @click.option(
394
- "--clear-cache",
395
- is_flag=True,
396
- help="Clear cache before analysis (passed to TUI)",
397
- )
398
- @click.option(
399
- "--output",
400
- "-o",
401
- type=click.Path(path_type=Path),
402
- default=None,
403
- help="Output directory for reports (passed to TUI)",
404
- )
405
- def tui_command(
406
- config: Optional[Path],
407
- weeks: Optional[int],
408
- clear_cache: bool,
409
- output: Optional[Path],
410
- ) -> None:
411
- """Launch the Terminal User Interface for GitFlow Analytics.
412
-
413
- \b
414
- The TUI provides an interactive interface for:
415
- - Loading and editing configuration files
416
- - Running analysis with real-time progress updates
417
- - Viewing results in an organized, navigable format
418
- - Exporting reports in various formats
419
-
420
- \b
421
- FEATURES:
422
- • Full-screen terminal interface with keyboard navigation
423
- • Real-time progress tracking during analysis
424
- • Interactive configuration management
425
- • Results browser with filtering and export options
426
- • Built-in help system and keyboard shortcuts
427
-
428
- \b
429
- EXAMPLES:
430
- # Launch TUI without pre-loading configuration
431
- gitflow-analytics tui
432
-
433
- # Launch TUI with a specific configuration file
434
- gitflow-analytics tui -c config.yaml
435
-
436
- \b
437
- KEYBOARD SHORTCUTS:
438
- • Ctrl+Q / Ctrl+C: Quit application
439
- • F1: Show help
440
- • Ctrl+D: Toggle dark/light mode
441
- • Escape: Go back/cancel current action
442
- """
443
- try:
444
- # Import TUI components only when needed
445
- from .tui.app import GitFlowAnalyticsApp
446
-
447
- # Create and run the TUI application
448
- app = GitFlowAnalyticsApp()
449
-
450
- # Pass CLI parameters to TUI
451
- if weeks is not None:
452
- app.default_weeks = weeks
453
- if clear_cache:
454
- app.clear_cache_on_start = True
455
- if output:
456
- app.default_output_dir = output
457
-
458
- # If config path provided, try to load it
459
- if config:
460
- try:
461
- from .config import ConfigLoader
462
-
463
- loaded_config = ConfigLoader.load(config)
464
- app.config = loaded_config
465
- app.config_path = config
466
- app.initialization_complete = True
467
- except Exception as e:
468
- # Don't fail - let TUI handle config loading
469
- click.echo(f"⚠️ Could not pre-load config: {e}")
470
- click.echo(" You can load the configuration within the TUI.")
471
-
472
- # Run the TUI
473
- app.run()
474
-
475
- except ImportError as e:
476
- click.echo("❌ TUI dependencies not installed.", err=True)
477
- click.echo(f" Error: {e}", err=True)
478
- click.echo("\n💡 Install TUI dependencies:", err=True)
479
- click.echo(" pip install 'gitflow-analytics[tui]'", err=True)
480
- click.echo(" # or", err=True)
481
- click.echo(" pip install textual>=0.41.0", err=True)
482
- sys.exit(1)
483
- except Exception as e:
484
- click.echo(f"❌ Failed to launch TUI: {e}", err=True)
485
- if "--debug" in sys.argv:
486
- raise
487
- sys.exit(1)
488
-
489
-
490
349
  @cli.command(name="analyze")
491
350
  @click.option(
492
351
  "--config",
@@ -534,7 +393,7 @@ def tui_command(
534
393
  "--no-rich",
535
394
  is_flag=True,
536
395
  default=True,
537
- help="Disable rich terminal output (simple output is default to prevent TUI hanging)",
396
+ help="Disable rich terminal output (use simple text progress instead)",
538
397
  )
539
398
  @click.option(
540
399
  "--log",
@@ -697,8 +556,20 @@ def analyze(
697
556
  ) -> None:
698
557
  """Analyze Git repositories using configuration file."""
699
558
 
700
- # Initialize progress service early with the correct style
559
+ # Lazy imports: Only load heavy dependencies when actually running analysis
560
+ # This improves CLI startup time from ~2s to <100ms for commands like --help
561
+ from .core.analyzer import GitAnalyzer
562
+ from .core.cache import GitAnalysisCache
563
+ from .core.git_auth import preflight_git_authentication
564
+ from .core.identity import DeveloperIdentityResolver
701
565
  from .core.progress import get_progress_service
566
+ from .integrations.orchestrator import IntegrationOrchestrator
567
+ from .metrics.dora import DORAMetricsCalculator
568
+ from .reports.analytics_writer import AnalyticsReportGenerator
569
+ from .reports.csv_writer import CSVReportGenerator
570
+ from .reports.json_exporter import ComprehensiveJSONExporter
571
+ from .reports.narrative_writer import NarrativeReportGenerator
572
+ from .reports.weekly_trends_writer import WeeklyTrendsWriter
702
573
 
703
574
  try:
704
575
  from ._version import __version__
@@ -710,7 +581,7 @@ def analyze(
710
581
  # Initialize progress service with user's preference
711
582
  progress = get_progress_service(display_style=progress_style, version=version)
712
583
 
713
- # Initialize display - simple output by default to prevent TUI hanging
584
+ # Initialize display - simple output by default for better compatibility
714
585
  # Create display - only create if rich output is explicitly enabled (--no-rich=False)
715
586
  display = (
716
587
  create_progress_display(style="simple" if no_rich else "rich", version=__version__)
@@ -1886,6 +1757,102 @@ def analyze(
1886
1757
  repo_path = Path(repo_config.path)
1887
1758
  project_key = repo_config.project_key or repo_path.name
1888
1759
 
1760
+ # Check if repo exists, clone if needed (critical for organization mode)
1761
+ if not repo_path.exists():
1762
+ if repo_config.github_repo and cfg.github.organization:
1763
+ if display:
1764
+ display.print_status(
1765
+ f" 📥 Cloning {repo_config.github_repo} from GitHub...",
1766
+ "info",
1767
+ )
1768
+ else:
1769
+ click.echo(f" 📥 Cloning {repo_config.github_repo} from GitHub...")
1770
+ try:
1771
+ # Ensure parent directory exists
1772
+ repo_path.parent.mkdir(parents=True, exist_ok=True)
1773
+
1774
+ # Build clone URL with authentication
1775
+ clone_url = f"https://github.com/{repo_config.github_repo}.git"
1776
+ if cfg.github.token:
1777
+ clone_url = f"https://{cfg.github.token}@github.com/{repo_config.github_repo}.git"
1778
+
1779
+ # Clone using subprocess for better control
1780
+ env = os.environ.copy()
1781
+ env["GIT_TERMINAL_PROMPT"] = "0"
1782
+ env["GIT_ASKPASS"] = ""
1783
+ env["GCM_INTERACTIVE"] = "never"
1784
+
1785
+ cmd = ["git", "clone", "--config", "credential.helper="]
1786
+ if repo_config.branch:
1787
+ cmd.extend(["-b", repo_config.branch])
1788
+ cmd.extend([clone_url, str(repo_path)])
1789
+
1790
+ result = subprocess.run(
1791
+ cmd,
1792
+ env=env,
1793
+ capture_output=True,
1794
+ text=True,
1795
+ timeout=30,
1796
+ )
1797
+
1798
+ if result.returncode != 0:
1799
+ error_msg = result.stderr or result.stdout
1800
+ if any(
1801
+ x in error_msg.lower()
1802
+ for x in ["authentication", "permission denied", "401", "403"]
1803
+ ):
1804
+ if display:
1805
+ display.print_status(
1806
+ f" ❌ Authentication failed for {repo_config.github_repo}",
1807
+ "error",
1808
+ )
1809
+ else:
1810
+ click.echo(
1811
+ f" ❌ Authentication failed for {repo_config.github_repo}"
1812
+ )
1813
+ continue
1814
+ else:
1815
+ raise subprocess.CalledProcessError(
1816
+ result.returncode, cmd, result.stdout, result.stderr
1817
+ )
1818
+
1819
+ if display:
1820
+ display.print_status(
1821
+ f" ✅ Cloned {repo_config.github_repo}", "success"
1822
+ )
1823
+ else:
1824
+ click.echo(f" ✅ Cloned {repo_config.github_repo}")
1825
+
1826
+ except subprocess.TimeoutExpired:
1827
+ if display:
1828
+ display.print_status(
1829
+ f" ❌ Clone timeout for {repo_config.github_repo}",
1830
+ "error",
1831
+ )
1832
+ else:
1833
+ click.echo(f" ❌ Clone timeout for {repo_config.github_repo}")
1834
+ continue
1835
+ except Exception as e:
1836
+ if display:
1837
+ display.print_status(
1838
+ f" ❌ Failed to clone {repo_config.github_repo}: {e}",
1839
+ "error",
1840
+ )
1841
+ else:
1842
+ click.echo(
1843
+ f" ❌ Failed to clone {repo_config.github_repo}: {e}"
1844
+ )
1845
+ continue
1846
+ else:
1847
+ # No github_repo configured, can't clone
1848
+ if display:
1849
+ display.print_status(
1850
+ f" ❌ Repository not found: {repo_path}", "error"
1851
+ )
1852
+ else:
1853
+ click.echo(f" ❌ Repository not found: {repo_path}")
1854
+ continue
1855
+
1889
1856
  # Progress callback for fetch
1890
1857
  def progress_callback(message: str):
1891
1858
  if display:
@@ -3567,6 +3534,9 @@ def analyze(
3567
3534
  logger.debug("Starting narrative report generation")
3568
3535
  narrative_gen = NarrativeReportGenerator()
3569
3536
 
3537
+ # Lazy import pandas - only needed for CSV reading in narrative generation
3538
+ import pandas as pd
3539
+
3570
3540
  # Load activity distribution data
3571
3541
  logger.debug("Loading activity distribution data")
3572
3542
  activity_df = pd.read_csv(activity_report)
@@ -4111,7 +4081,7 @@ def analyze(
4111
4081
  "--no-rich",
4112
4082
  is_flag=True,
4113
4083
  default=True,
4114
- help="Disable rich terminal output (simple output is default to prevent TUI hanging)",
4084
+ help="Disable rich terminal output (use simple text progress instead)",
4115
4085
  )
4116
4086
  def fetch(
4117
4087
  config: Path,
@@ -4161,7 +4131,7 @@ def fetch(
4161
4131
  - Use --clear-cache to force fresh fetch
4162
4132
  """
4163
4133
  # Initialize display
4164
- # Create display - simple output by default to prevent TUI hanging, rich only when explicitly enabled
4134
+ # Create display - simple output by default for better compatibility, rich only when explicitly enabled
4165
4135
  display = (
4166
4136
  create_progress_display(style="simple" if no_rich else "rich", version=__version__)
4167
4137
  if not no_rich
@@ -4186,6 +4156,10 @@ def fetch(
4186
4156
  logger = logging.getLogger(__name__)
4187
4157
 
4188
4158
  try:
4159
+ # Lazy imports
4160
+ from .core.cache import GitAnalysisCache
4161
+ from .integrations.orchestrator import IntegrationOrchestrator
4162
+
4189
4163
  if display:
4190
4164
  display.show_header()
4191
4165
 
@@ -5604,6 +5578,9 @@ def train(
5604
5578
  # Initialize trainer
5605
5579
  click.echo("🧠 Initializing training pipeline...")
5606
5580
  try:
5581
+ # Lazy import - only needed for train command
5582
+ from .training.pipeline import CommitClassificationTrainer
5583
+
5607
5584
  trainer = CommitClassificationTrainer(
5608
5585
  config=cfg, cache=cache, orchestrator=orchestrator, training_config=training_config
5609
5586
  )
@@ -5939,6 +5916,10 @@ def training_statistics(config: Path) -> None:
5939
5916
  - Compare different model versions
5940
5917
  """
5941
5918
  try:
5919
+ # Lazy imports
5920
+ from .core.cache import GitAnalysisCache
5921
+ from .training.pipeline import CommitClassificationTrainer
5922
+
5942
5923
  cfg = ConfigLoader.load(config)
5943
5924
  cache = GitAnalysisCache(cfg.cache.directory)
5944
5925
 
@@ -117,7 +117,7 @@ class InstallWizard:
117
117
  return getpass.getpass(prompt)
118
118
  else:
119
119
  click.echo(f"⚠️ Non-interactive mode detected - {field_name} will be visible", err=True)
120
- return click.prompt(prompt, hide_input=False)
120
+ return click.prompt(prompt, hide_input=False).strip()
121
121
 
122
122
  def _select_profile(self) -> dict:
123
123
  """Let user select installation profile."""
@@ -484,7 +484,7 @@ class InstallWizard:
484
484
  if not click.confirm("Add anyway?", default=False):
485
485
  continue
486
486
 
487
- repo_name = click.prompt("Repository name", default=path_obj.name)
487
+ repo_name = click.prompt("Repository name", default=path_obj.name).strip()
488
488
 
489
489
  repositories.append({"name": repo_name, "path": str(path_obj)})
490
490
  click.echo(f"Added repository #{len(repositories)}\n")
@@ -850,7 +850,7 @@ class InstallWizard:
850
850
  "Output directory for reports",
851
851
  type=str,
852
852
  default="./reports",
853
- )
853
+ ).strip()
854
854
  output_path = self._validate_directory_path(output_dir, "Output directory")
855
855
  if output_path is not None:
856
856
  output_dir = str(output_path)
@@ -863,7 +863,7 @@ class InstallWizard:
863
863
  "Cache directory",
864
864
  type=str,
865
865
  default="./.gitflow-cache",
866
- )
866
+ ).strip()
867
867
  cache_path = self._validate_directory_path(cache_dir, "Cache directory")
868
868
  if cache_path is not None:
869
869
  cache_dir = str(cache_path)
@@ -928,7 +928,7 @@ class InstallWizard:
928
928
  # Use existing file
929
929
  aliases_path = click.prompt(
930
930
  "Path to aliases.yaml (relative to config)", default="../shared/aliases.yaml"
931
- )
931
+ ).strip()
932
932
 
933
933
  # Ensure analysis.identity section exists
934
934
  if "identity" not in self.config_data.get("analysis", {}):