gitflow-analytics 3.9.3__py3-none-any.whl → 3.10.6__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.
@@ -1,4 +1,4 @@
1
1
  """Version information for gitflow-analytics."""
2
2
 
3
- __version__ = "3.9.3"
3
+ __version__ = "3.10.6"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
gitflow_analytics/cli.py CHANGED
@@ -628,6 +628,27 @@ def analyze(
628
628
 
629
629
  cfg = ConfigLoader.load(config)
630
630
 
631
+ # Helper function to check if qualitative analysis is enabled
632
+ # Supports both top-level cfg.qualitative and nested cfg.analysis.qualitative
633
+ def is_qualitative_enabled() -> bool:
634
+ """Check if qualitative analysis is enabled in either location."""
635
+ if cfg.qualitative and cfg.qualitative.enabled:
636
+ return True
637
+ return (
638
+ hasattr(cfg.analysis, "qualitative")
639
+ and cfg.analysis.qualitative
640
+ and cfg.analysis.qualitative.enabled
641
+ )
642
+
643
+ # Helper function to get qualitative config from either location
644
+ def get_qualitative_config():
645
+ """Get qualitative config from either top-level or nested location."""
646
+ if cfg.qualitative:
647
+ return cfg.qualitative
648
+ if hasattr(cfg.analysis, "qualitative") and cfg.analysis.qualitative:
649
+ return cfg.analysis.qualitative
650
+ return None
651
+
631
652
  # Apply CLI overrides for PM integration
632
653
  if disable_pm:
633
654
  # Disable PM integration if explicitly requested
@@ -2974,9 +2995,8 @@ def analyze(
2974
2995
  # Perform qualitative analysis if enabled
2975
2996
  qualitative_results = []
2976
2997
  qual_cost_stats = None
2977
- if (
2978
- enable_qualitative or qualitative_only or (cfg.qualitative and cfg.qualitative.enabled)
2979
- ) and cfg.qualitative:
2998
+ qual_config = get_qualitative_config()
2999
+ if (enable_qualitative or qualitative_only or is_qualitative_enabled()) and qual_config:
2980
3000
  if display:
2981
3001
  display.print_status("Performing qualitative analysis...", "info")
2982
3002
  else:
@@ -2988,7 +3008,7 @@ def analyze(
2988
3008
 
2989
3009
  # Initialize qualitative analysis components
2990
3010
  qual_db = Database(cfg.cache.directory / "qualitative.db")
2991
- qual_processor = QualitativeProcessor(cfg.qualitative, qual_db)
3011
+ qual_processor = QualitativeProcessor(qual_config, qual_db)
2992
3012
 
2993
3013
  # Validate setup
2994
3014
  is_valid, issues = qual_processor.validate_setup()
@@ -3143,8 +3163,8 @@ def analyze(
3143
3163
  display.print_status("Continuing with standard analysis...", "info")
3144
3164
  else:
3145
3165
  click.echo(" ⏭️ Continuing with standard analysis...")
3146
- elif enable_qualitative and not cfg.qualitative:
3147
- warning_msg = "Qualitative analysis requested but not configured in config file\n\nAdd a 'qualitative:' section to your configuration"
3166
+ elif enable_qualitative and not get_qualitative_config():
3167
+ warning_msg = "Qualitative analysis requested but not configured in config file\n\nAdd a 'qualitative:' section (top-level or under 'analysis:') to your configuration"
3148
3168
  if display:
3149
3169
  display.show_warning(warning_msg)
3150
3170
  else:
@@ -4085,12 +4105,12 @@ def analyze(
4085
4105
  )
4086
4106
  else:
4087
4107
  # Show note about token tracking when qualitative analysis is not configured
4088
- if not (enable_qualitative or (cfg.qualitative and cfg.qualitative.enabled)):
4108
+ if not (enable_qualitative or is_qualitative_enabled()):
4089
4109
  click.echo(
4090
4110
  "\n💡 Note: Token/cost tracking is only available with qualitative analysis enabled."
4091
4111
  )
4092
4112
  click.echo(
4093
- " Add 'qualitative:' section to your config to enable detailed LLM cost tracking."
4113
+ " Add 'qualitative:' section (top-level or under 'analysis:') to your config to enable detailed LLM cost tracking."
4094
4114
  )
4095
4115
 
4096
4116
  # Display cache statistics in simple format
@@ -7,6 +7,8 @@ and comprehensive configuration generation.
7
7
  import getpass
8
8
  import logging
9
9
  import os
10
+ import re
11
+ import shutil
10
12
  import stat
11
13
  import subprocess
12
14
  import sys
@@ -18,6 +20,8 @@ from typing import Optional
18
20
  import click
19
21
  import requests
20
22
  import yaml
23
+ from git import GitCommandError, Repo
24
+ from git.exc import InvalidGitRepositoryError
21
25
  from github import Github
22
26
  from github.GithubException import GithubException
23
27
 
@@ -490,6 +494,203 @@ class InstallWizard:
490
494
  logger.debug(f"Path validation error: {type(e).__name__}")
491
495
  return None
492
496
 
497
+ def _detect_git_url(self, input_str: str) -> Optional[str]:
498
+ """Detect if input is a Git URL and normalize it.
499
+
500
+ Args:
501
+ input_str: User input string
502
+
503
+ Returns:
504
+ Normalized Git URL if detected, None if it's a local path
505
+ """
506
+ import re
507
+
508
+ # HTTPS URL patterns
509
+ https_pattern = r"^https?://[^/]+/[^/]+/[^/]+(?:\.git)?$"
510
+ # SSH URL pattern
511
+ ssh_pattern = r"^git@[^:]+:[^/]+/[^/]+(?:\.git)?$"
512
+
513
+ input_str = input_str.strip()
514
+
515
+ if re.match(https_pattern, input_str, re.IGNORECASE) or re.match(ssh_pattern, input_str):
516
+ # Ensure .git extension for consistency
517
+ if not input_str.endswith(".git"):
518
+ input_str = input_str + ".git"
519
+ return input_str
520
+
521
+ return None
522
+
523
+ def _clone_git_repository(self, git_url: str) -> Optional[tuple[Path, str]]:
524
+ """Clone a Git repository to the local repos/ directory.
525
+
526
+ Args:
527
+ git_url: Git URL to clone
528
+
529
+ Returns:
530
+ Tuple of (local_path, original_url) if successful, None if failed
531
+ """
532
+ try:
533
+ # Extract repository name from URL
534
+ # Handle both HTTPS and SSH formats
535
+ match = re.search(r"/([^/]+?)(?:\.git)?$", git_url)
536
+ if not match:
537
+ click.echo("❌ Could not extract repository name from URL")
538
+ return None
539
+
540
+ repo_name = match.group(1)
541
+ click.echo(f"📦 Repository: {repo_name}")
542
+
543
+ # Create repos directory in current working directory
544
+ repos_dir = Path.cwd() / "repos"
545
+ repos_dir.mkdir(parents=True, exist_ok=True)
546
+ click.echo(f"📁 Clone directory: {repos_dir}")
547
+
548
+ # Target path for cloned repository
549
+ target_path = repos_dir / repo_name
550
+
551
+ # Check if repository already exists
552
+ if target_path.exists():
553
+ click.echo(f"⚠️ Directory already exists: {target_path}")
554
+
555
+ # Check if it's a valid git repository
556
+ try:
557
+ existing_repo = Repo(target_path)
558
+ if existing_repo.working_dir:
559
+ click.echo("✅ Found existing git repository")
560
+
561
+ # Check if remote URL matches
562
+ try:
563
+ origin_url = existing_repo.remotes.origin.url
564
+ if origin_url == git_url or self._normalize_git_url(
565
+ origin_url
566
+ ) == self._normalize_git_url(git_url):
567
+ click.echo(f"✅ Remote URL matches: {origin_url}")
568
+
569
+ # Offer to update
570
+ if click.confirm(
571
+ "Update existing repository (git pull)?", default=True
572
+ ):
573
+ click.echo("🔄 Updating repository...")
574
+ origin = existing_repo.remotes.origin
575
+ origin.pull()
576
+ click.echo("✅ Repository updated")
577
+
578
+ return (target_path, git_url)
579
+ else:
580
+ click.echo("⚠️ Remote URL mismatch:")
581
+ click.echo(f" Existing: {origin_url}")
582
+ click.echo(f" Requested: {git_url}")
583
+ if not click.confirm(
584
+ "Use existing repository anyway?", default=False
585
+ ):
586
+ return None
587
+ return (target_path, git_url)
588
+ except Exception as e:
589
+ click.echo(f"⚠️ Could not check remote URL: {type(e).__name__}")
590
+ if click.confirm("Use existing repository anyway?", default=False):
591
+ return (target_path, git_url)
592
+ return None
593
+ except InvalidGitRepositoryError:
594
+ click.echo("❌ Directory exists but is not a git repository")
595
+ if not click.confirm("Remove and re-clone?", default=False):
596
+ return None
597
+
598
+ # Remove existing directory
599
+ shutil.rmtree(target_path)
600
+ click.echo("🗑️ Removed existing directory")
601
+
602
+ # Clone the repository
603
+ click.echo(f"🔄 Cloning {git_url}...")
604
+ click.echo(" This may take a moment depending on repository size...")
605
+
606
+ # Clone with progress
607
+ Repo.clone_from(git_url, target_path, progress=self._get_git_progress())
608
+
609
+ # Verify clone succeeded
610
+ if not (target_path / ".git").exists():
611
+ click.echo("❌ Clone appeared to succeed but .git directory not found")
612
+ return None
613
+
614
+ click.echo(f"✅ Successfully cloned to: {target_path}")
615
+ return (target_path, git_url)
616
+
617
+ except GitCommandError as e:
618
+ click.echo("❌ Git clone failed")
619
+
620
+ # Parse error message for common issues
621
+ error_str = str(e).lower()
622
+ if "authentication failed" in error_str or "permission denied" in error_str:
623
+ click.echo("🔐 Authentication required")
624
+ click.echo(" For HTTPS: Configure Git credentials or use a personal access token")
625
+ click.echo(" For SSH: Ensure your SSH key is added to your Git provider")
626
+ elif "not found" in error_str or "does not exist" in error_str:
627
+ click.echo("🔍 Repository not found")
628
+ click.echo(" Check the URL and ensure you have access")
629
+ elif "network" in error_str or "timeout" in error_str:
630
+ click.echo("🌐 Network error")
631
+ click.echo(" Check your internet connection and try again")
632
+ else:
633
+ logger.debug(f"Git clone error type: {type(e).__name__}")
634
+
635
+ return None
636
+
637
+ except OSError as e:
638
+ error_type = type(e).__name__
639
+ click.echo(f"❌ File system error: {error_type}")
640
+ if "space" in str(e).lower():
641
+ click.echo("💾 Insufficient disk space")
642
+ logger.debug(f"Clone file system error: {error_type}")
643
+ return None
644
+
645
+ except Exception as e:
646
+ error_type = type(e).__name__
647
+ click.echo(f"❌ Unexpected error during clone: {error_type}")
648
+ logger.error(f"Clone error type: {error_type}")
649
+ return None
650
+
651
+ def _normalize_git_url(self, url: str) -> str:
652
+ """Normalize Git URL for comparison.
653
+
654
+ Args:
655
+ url: Git URL to normalize
656
+
657
+ Returns:
658
+ Normalized URL (lowercase, with .git extension)
659
+ """
660
+ url = url.lower().strip()
661
+ if not url.endswith(".git"):
662
+ url = url + ".git"
663
+ return url
664
+
665
+ def _get_git_progress(self):
666
+ """Get a Git progress handler for clone operations.
667
+
668
+ Returns:
669
+ Progress handler for GitPython or None
670
+ """
671
+ try:
672
+ from git import RemoteProgress
673
+
674
+ class CloneProgress(RemoteProgress):
675
+ """Progress handler for git clone operations."""
676
+
677
+ def __init__(self):
678
+ super().__init__()
679
+ self.last_percent = 0
680
+
681
+ def update(self, op_code, cur_count, max_count=None, message=""):
682
+ if max_count:
683
+ percent = int((cur_count / max_count) * 100)
684
+ # Only show updates every 10%
685
+ if percent >= self.last_percent + 10:
686
+ click.echo(f" Progress: {percent}%")
687
+ self.last_percent = percent
688
+
689
+ return CloneProgress()
690
+ except Exception:
691
+ # If progress handler fails, return None (clone will work without it)
692
+ return None
693
+
493
694
  def _setup_manual_repos(self) -> bool:
494
695
  """Setup manual repository configuration.
495
696
 
@@ -497,44 +698,66 @@ class InstallWizard:
497
698
  True if setup successful, False otherwise
498
699
  """
499
700
  click.echo("\n📦 Manual Repository Mode")
500
- click.echo("You can specify one or more local repository paths.")
701
+ click.echo("You can specify one or more local repository paths or Git URLs.")
702
+ click.echo("Supported formats:")
703
+ click.echo(" • Local path: /path/to/repo or ~/repos/myproject")
704
+ click.echo(" • HTTPS URL: https://github.com/owner/repo.git")
705
+ click.echo(" • SSH URL: git@github.com:owner/repo.git")
501
706
  click.echo()
502
707
 
503
708
  repositories = []
504
709
  while True:
505
- repo_path_str = click.prompt(
506
- "Enter repository path (or press Enter to finish)",
710
+ repo_input = click.prompt(
711
+ "Enter repository path or Git URL (or press Enter to finish)",
507
712
  type=str,
508
713
  default="",
509
714
  show_default=False,
510
715
  ).strip()
511
716
 
512
- if not repo_path_str:
717
+ if not repo_input:
513
718
  if not repositories:
514
719
  click.echo("❌ At least one repository is required")
515
720
  continue
516
721
  break
517
722
 
518
- # Validate path is safe
519
- path_obj = self._validate_directory_path(repo_path_str, "Repository path")
520
- if path_obj is None:
521
- continue # Re-prompt
522
-
523
- if not path_obj.exists():
524
- click.echo(f"⚠️ Path does not exist: {path_obj}")
525
- if not click.confirm("Add anyway?", default=False):
723
+ # Check if input is a Git URL
724
+ git_url = self._detect_git_url(repo_input)
725
+ if git_url:
726
+ # Handle Git URL cloning
727
+ result = self._clone_git_repository(git_url)
728
+ if result is None:
729
+ # Clone failed, ask user if they want to retry or skip
730
+ if not click.confirm("Try a different repository?", default=True):
731
+ if repositories:
732
+ break # User has other repos, can finish
733
+ continue # User has no repos yet, must add at least one
526
734
  continue
527
735
 
528
- # Check if it's a git repository
529
- if (path_obj / ".git").exists():
530
- click.echo(f" Valid git repository: {path_obj}")
736
+ # Clone successful
737
+ local_path, original_url = result
738
+ repositories.append({"path": str(local_path), "git_url": original_url})
739
+ click.echo(f"Added repository #{len(repositories)}")
531
740
  else:
532
- click.echo(f"⚠️ Not a git repository: {path_obj}")
533
- if not click.confirm("Add anyway?", default=False):
534
- continue
741
+ # Handle local path
742
+ path_obj = self._validate_directory_path(repo_input, "Repository path")
743
+ if path_obj is None:
744
+ continue # Re-prompt
745
+
746
+ if not path_obj.exists():
747
+ click.echo(f"⚠️ Path does not exist: {path_obj}")
748
+ if not click.confirm("Add anyway?", default=False):
749
+ continue
750
+
751
+ # Check if it's a git repository
752
+ if (path_obj / ".git").exists():
753
+ click.echo(f"✅ Valid git repository: {path_obj}")
754
+ else:
755
+ click.echo(f"⚠️ Not a git repository: {path_obj}")
756
+ if not click.confirm("Add anyway?", default=False):
757
+ continue
535
758
 
536
- repositories.append({"path": str(path_obj)})
537
- click.echo(f"Added repository #{len(repositories)}")
759
+ repositories.append({"path": str(path_obj)})
760
+ click.echo(f"Added repository #{len(repositories)}")
538
761
 
539
762
  if not click.confirm("Add another repository?", default=False):
540
763
  break
@@ -258,7 +258,14 @@ class ConfigLoader:
258
258
  jira_integration_config = cls._process_jira_integration_config(
259
259
  data.get("jira_integration", {})
260
260
  )
261
- qualitative_config = cls._process_qualitative_config(data.get("qualitative", {}))
261
+
262
+ # Check for qualitative config in both top-level and nested under analysis
263
+ # Prioritize top-level for backward compatibility, but support nested location
264
+ qualitative_data = data.get("qualitative", {})
265
+ if not qualitative_data and "analysis" in data:
266
+ qualitative_data = data["analysis"].get("qualitative", {})
267
+ qualitative_config = cls._process_qualitative_config(qualitative_data)
268
+
262
269
  pm_config = cls._process_pm_config(data.get("pm", {}))
263
270
  pm_integration_config = cls._process_pm_integration_config(data.get("pm_integration", {}))
264
271
 
@@ -536,6 +543,12 @@ class ConfigLoader:
536
543
  BranchAnalysisConfig(**branch_data) if branch_data else BranchAnalysisConfig()
537
544
  )
538
545
 
546
+ # Process qualitative configuration (support nested under analysis)
547
+ qualitative_data = analysis_data.get("qualitative", {})
548
+ qualitative_config = (
549
+ cls._process_qualitative_config(qualitative_data) if qualitative_data else None
550
+ )
551
+
539
552
  # Process aliases file and manual identity mappings
540
553
  manual_mappings = list(analysis_data.get("identity", {}).get("manual_mappings", []))
541
554
  aliases_file_path = None
@@ -595,6 +608,7 @@ class ConfigLoader:
595
608
  commit_classification=commit_classification_config,
596
609
  llm_classification=llm_classification_config,
597
610
  security=analysis_data.get("security", {}),
611
+ qualitative=qualitative_config,
598
612
  )
599
613
 
600
614
  @classmethod
@@ -316,6 +316,7 @@ class AnalysisConfig:
316
316
  )
317
317
  llm_classification: LLMClassificationConfig = field(default_factory=LLMClassificationConfig)
318
318
  security: Optional[dict[str, Any]] = field(default_factory=dict) # Security configuration
319
+ qualitative: Optional["QualitativeConfig"] = None # Nested qualitative config support
319
320
 
320
321
 
321
322
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitflow-analytics
3
- Version: 3.9.3
3
+ Version: 3.10.6
4
4
  Summary: Analyze Git repositories for developer productivity insights
5
5
  Author-email: Bob Matyas <bobmatnyc@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  gitflow_analytics/__init__.py,sha256=W3Jaey5wuT1nBPehVLTIRkVIyBa5jgYOlBKc_UFfh-4,773
2
- gitflow_analytics/_version.py,sha256=ORQnrflACA8f-2fsqGzATYAmqe7IrUi2k8I6B_HggXY,137
3
- gitflow_analytics/cli.py,sha256=pYW6V0b6SRa3-NyOmXGQhf5emcKHUHgOVL2PFOAS8LQ,273331
2
+ gitflow_analytics/_version.py,sha256=EZYcvAFHOt8s3T1uFte5WRCRLCJA_e9z4u9hjkDwMvE,138
3
+ gitflow_analytics/cli.py,sha256=j-xjxPWTViFVullTUwVY3qJqwPXBZOEwgADwAvq_xVA,274357
4
4
  gitflow_analytics/config.py,sha256=XRuxvzLWyn_ML7mDCcuZ9-YFNAEsnt33vIuWxQQ_jxg,1033
5
5
  gitflow_analytics/constants.py,sha256=GXEncUJS9ijOI5KWtQCTANwdqxPfXpw-4lNjhaWTKC4,2488
6
6
  gitflow_analytics/verify_activity.py,sha256=aRQnmypf5NDasXudf2iz_WdJnCWtwlbAiJ5go0DJLSU,27050
@@ -11,15 +11,15 @@ gitflow_analytics/classification/feature_extractor.py,sha256=W82vztPQO8-MFw9Yt17
11
11
  gitflow_analytics/classification/linguist_analyzer.py,sha256=HjLx9mM7hGXtrvMba6osovHJLAacTx9oDmN6CS5w0bE,17687
12
12
  gitflow_analytics/classification/model.py,sha256=2KbmFh9MpyvHMcNHbqwUTAAVLHHu3MiTfFIPyZSGa-8,16356
13
13
  gitflow_analytics/cli_wizards/__init__.py,sha256=D73D97cS1hZsB_fCQQaAiWtd_w2Lb8TtcGc9Pn2DIyE,343
14
- gitflow_analytics/cli_wizards/install_wizard.py,sha256=KcmDD2RiqFrPuXdBhmXCYvftlV_V-_7p82Z6Gu8NgO0,60817
14
+ gitflow_analytics/cli_wizards/install_wizard.py,sha256=gz5c1NYeGLCzs-plL6ju7GXn7VldF7VyMw8MO4CzUGk,70345
15
15
  gitflow_analytics/cli_wizards/run_launcher.py,sha256=J6G_C7IqxPg7_GhAfbV99D1dIIWwb1s_qmHC7Iv2iGI,15038
16
16
  gitflow_analytics/config/__init__.py,sha256=KziRIbBJctB5LOLcKLzELWA1rXwjS6-C2_DeM_hT9rM,1133
17
17
  gitflow_analytics/config/aliases.py,sha256=z9F0X6qbbF544Tw7sHlOoBj5mpRSddMkCpoKLzvVzDU,10960
18
18
  gitflow_analytics/config/errors.py,sha256=IBKhAIwJ4gscZFnLDyE3jEp03wn2stPR7JQJXNSIfok,10386
19
- gitflow_analytics/config/loader.py,sha256=EiksTB1Uqey63FxIvuud_kMdab3sNDfuICE_RwMLYFA,37290
19
+ gitflow_analytics/config/loader.py,sha256=Rjr3YyD6iFZsS0MRGy9w_GN8Q0CuDOIab-oBDpuc-Q8,37951
20
20
  gitflow_analytics/config/profiles.py,sha256=yUjFAWW6uzOUdi5qlPE-QV9681HigyrLiSJFpL8X9A0,7967
21
21
  gitflow_analytics/config/repository.py,sha256=u7JHcKvqmXOl3i7EmNUfJ6wtjzElxPMyXRkATnVyQ0I,4685
22
- gitflow_analytics/config/schema.py,sha256=tzXyckIY8RNRpS9ez9iBuVtZMe-WXjBBjsp9Pm-aetU,17088
22
+ gitflow_analytics/config/schema.py,sha256=CR9swU5LaQGBtcI9KgHzG1t17cas00ECI22LwJiRgqs,17179
23
23
  gitflow_analytics/config/validator.py,sha256=l7AHjXYJ8wEmyA1rn2WiItZXtAiRb9YBLjFCDl53qKM,5907
24
24
  gitflow_analytics/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  gitflow_analytics/core/analyzer.py,sha256=59kGObzjziOb8geFyZMKCUvWmo3hcXE0eTgrjYEc1XA,58736
@@ -120,9 +120,9 @@ gitflow_analytics/training/model_loader.py,sha256=xGZLSopGxDhC--2XN6ytRgi2CyjOKY
120
120
  gitflow_analytics/training/pipeline.py,sha256=PQegTk_-OsPexVyRDfiy-3Df-7pcs25C4vPASr-HT9E,19951
121
121
  gitflow_analytics/ui/__init__.py,sha256=UBhYhZMvwlSrCuGWjkIdoP2zNbiQxOHOli-I8mqIZUE,441
122
122
  gitflow_analytics/ui/progress_display.py,sha256=3xJnCOSs1DRVAfS-rTu37EsLfWDFW5-mbv-bPS9NMm4,59182
123
- gitflow_analytics-3.9.3.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
124
- gitflow_analytics-3.9.3.dist-info/METADATA,sha256=ZmTxikWCnWPdRW7RQSGGEkuexP1jhzPUNL9gmwykQgQ,36830
125
- gitflow_analytics-3.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
126
- gitflow_analytics-3.9.3.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
127
- gitflow_analytics-3.9.3.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
128
- gitflow_analytics-3.9.3.dist-info/RECORD,,
123
+ gitflow_analytics-3.10.6.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
124
+ gitflow_analytics-3.10.6.dist-info/METADATA,sha256=We2H93azJZOCshGIrmnL7RnufmwKnafTxd7wUpOVatw,36831
125
+ gitflow_analytics-3.10.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
126
+ gitflow_analytics-3.10.6.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
127
+ gitflow_analytics-3.10.6.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
128
+ gitflow_analytics-3.10.6.dist-info/RECORD,,