gitflow-analytics 1.0.0__py3-none-any.whl → 1.0.3__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 (58) hide show
  1. gitflow_analytics/__init__.py +11 -9
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/cli.py +691 -243
  4. gitflow_analytics/cli_rich.py +353 -0
  5. gitflow_analytics/config.py +389 -96
  6. gitflow_analytics/core/analyzer.py +175 -78
  7. gitflow_analytics/core/branch_mapper.py +132 -132
  8. gitflow_analytics/core/cache.py +242 -173
  9. gitflow_analytics/core/identity.py +214 -178
  10. gitflow_analytics/extractors/base.py +13 -11
  11. gitflow_analytics/extractors/story_points.py +70 -59
  12. gitflow_analytics/extractors/tickets.py +111 -88
  13. gitflow_analytics/integrations/github_integration.py +91 -77
  14. gitflow_analytics/integrations/jira_integration.py +284 -0
  15. gitflow_analytics/integrations/orchestrator.py +99 -72
  16. gitflow_analytics/metrics/dora.py +183 -179
  17. gitflow_analytics/models/database.py +191 -54
  18. gitflow_analytics/qualitative/__init__.py +30 -0
  19. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  20. gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
  21. gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
  22. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
  23. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
  24. gitflow_analytics/qualitative/core/__init__.py +13 -0
  25. gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
  26. gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
  27. gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
  28. gitflow_analytics/qualitative/core/processor.py +540 -0
  29. gitflow_analytics/qualitative/models/__init__.py +25 -0
  30. gitflow_analytics/qualitative/models/schemas.py +272 -0
  31. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  32. gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
  33. gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
  34. gitflow_analytics/qualitative/utils/metrics.py +347 -0
  35. gitflow_analytics/qualitative/utils/text_processing.py +243 -0
  36. gitflow_analytics/reports/analytics_writer.py +25 -8
  37. gitflow_analytics/reports/csv_writer.py +60 -32
  38. gitflow_analytics/reports/narrative_writer.py +21 -15
  39. gitflow_analytics/tui/__init__.py +5 -0
  40. gitflow_analytics/tui/app.py +721 -0
  41. gitflow_analytics/tui/screens/__init__.py +8 -0
  42. gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
  43. gitflow_analytics/tui/screens/configuration_screen.py +547 -0
  44. gitflow_analytics/tui/screens/loading_screen.py +358 -0
  45. gitflow_analytics/tui/screens/main_screen.py +304 -0
  46. gitflow_analytics/tui/screens/results_screen.py +698 -0
  47. gitflow_analytics/tui/widgets/__init__.py +7 -0
  48. gitflow_analytics/tui/widgets/data_table.py +257 -0
  49. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  50. gitflow_analytics/tui/widgets/progress_widget.py +192 -0
  51. gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
  52. gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
  53. gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
  54. gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
  55. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
  56. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
  57. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
  58. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,221 +1,221 @@
1
1
  """Map git branches to projects based on naming conventions."""
2
+
2
3
  import re
3
- from typing import Dict, List, Optional, Tuple
4
4
  from pathlib import Path
5
+ from typing import Optional
5
6
 
6
7
 
7
8
  class BranchToProjectMapper:
8
9
  """Maps git branches to project keys based on conventions."""
9
-
10
- def __init__(self, mapping_rules: Optional[Dict[str, List[str]]] = None):
10
+
11
+ def __init__(self, mapping_rules: Optional[dict[str, list[str]]] = None):
11
12
  """
12
13
  Initialize with custom mapping rules.
13
-
14
+
14
15
  Args:
15
16
  mapping_rules: Dict mapping project keys to list of branch patterns
16
17
  e.g., {'FRONTEND': ['feature/fe-*', 'frontend/*']}
17
18
  """
18
19
  self.mapping_rules = mapping_rules or self._get_default_rules()
19
20
  self.compiled_rules = self._compile_patterns()
20
-
21
- def _get_default_rules(self) -> Dict[str, List[str]]:
21
+
22
+ def _get_default_rules(self) -> dict[str, list[str]]:
22
23
  """Get default branch mapping rules."""
23
24
  return {
24
- 'FRONTEND': [
25
- r'^feature/fe[-/_]',
26
- r'^feature/frontend[-/_]',
27
- r'^frontend/',
28
- r'^fe/',
29
- r'[-/_]frontend[-/_]',
30
- r'[-/_]fe[-/_]',
31
- r'[-/_]ui[-/_]',
32
- r'[-/_]web[-/_]'
25
+ "FRONTEND": [
26
+ r"^feature/fe[-/_]",
27
+ r"^feature/frontend[-/_]",
28
+ r"^frontend/",
29
+ r"^fe/",
30
+ r"[-/_]frontend[-/_]",
31
+ r"[-/_]fe[-/_]",
32
+ r"[-/_]ui[-/_]",
33
+ r"[-/_]web[-/_]",
33
34
  ],
34
- 'BACKEND': [
35
- r'^feature/be[-/_]',
36
- r'^feature/backend[-/_]',
37
- r'^backend/',
38
- r'^be/',
39
- r'^api/',
40
- r'[-/_]backend[-/_]',
41
- r'[-/_]be[-/_]',
42
- r'[-/_]api[-/_]',
43
- r'[-/_]server[-/_]'
35
+ "BACKEND": [
36
+ r"^feature/be[-/_]",
37
+ r"^feature/backend[-/_]",
38
+ r"^backend/",
39
+ r"^be/",
40
+ r"^api/",
41
+ r"[-/_]backend[-/_]",
42
+ r"[-/_]be[-/_]",
43
+ r"[-/_]api[-/_]",
44
+ r"[-/_]server[-/_]",
44
45
  ],
45
- 'SERVICE': [
46
- r'^feature/service[-/_]',
47
- r'^feature/svc[-/_]',
48
- r'^service/',
49
- r'^svc/',
50
- r'[-/_]service[-/_]',
51
- r'[-/_]svc[-/_]',
52
- r'[-/_]microservice[-/_]'
46
+ "SERVICE": [
47
+ r"^feature/service[-/_]",
48
+ r"^feature/svc[-/_]",
49
+ r"^service/",
50
+ r"^svc/",
51
+ r"[-/_]service[-/_]",
52
+ r"[-/_]svc[-/_]",
53
+ r"[-/_]microservice[-/_]",
53
54
  ],
54
- 'MOBILE': [
55
- r'^feature/mobile[-/_]',
56
- r'^feature/app[-/_]',
57
- r'^mobile/',
58
- r'^app/',
59
- r'^ios/',
60
- r'^android/',
61
- r'[-/_]mobile[-/_]',
62
- r'[-/_]app[-/_]',
63
- r'[-/_]ios[-/_]',
64
- r'[-/_]android[-/_]'
55
+ "MOBILE": [
56
+ r"^feature/mobile[-/_]",
57
+ r"^feature/app[-/_]",
58
+ r"^mobile/",
59
+ r"^app/",
60
+ r"^ios/",
61
+ r"^android/",
62
+ r"[-/_]mobile[-/_]",
63
+ r"[-/_]app[-/_]",
64
+ r"[-/_]ios[-/_]",
65
+ r"[-/_]android[-/_]",
65
66
  ],
66
- 'DATA': [
67
- r'^feature/data[-/_]',
68
- r'^feature/etl[-/_]',
69
- r'^data/',
70
- r'^etl/',
71
- r'^pipeline/',
72
- r'[-/_]data[-/_]',
73
- r'[-/_]etl[-/_]',
74
- r'[-/_]pipeline[-/_]',
75
- r'[-/_]analytics[-/_]'
67
+ "DATA": [
68
+ r"^feature/data[-/_]",
69
+ r"^feature/etl[-/_]",
70
+ r"^data/",
71
+ r"^etl/",
72
+ r"^pipeline/",
73
+ r"[-/_]data[-/_]",
74
+ r"[-/_]etl[-/_]",
75
+ r"[-/_]pipeline[-/_]",
76
+ r"[-/_]analytics[-/_]",
76
77
  ],
77
- 'INFRA': [
78
- r'^feature/infra[-/_]',
79
- r'^feature/devops[-/_]',
80
- r'^infra/',
81
- r'^devops/',
82
- r'^ops/',
83
- r'[-/_]infra[-/_]',
84
- r'[-/_]devops[-/_]',
85
- r'[-/_]ops[-/_]',
86
- r'[-/_]deployment[-/_]'
78
+ "INFRA": [
79
+ r"^feature/infra[-/_]",
80
+ r"^feature/devops[-/_]",
81
+ r"^infra/",
82
+ r"^devops/",
83
+ r"^ops/",
84
+ r"[-/_]infra[-/_]",
85
+ r"[-/_]devops[-/_]",
86
+ r"[-/_]ops[-/_]",
87
+ r"[-/_]deployment[-/_]",
88
+ ],
89
+ "SCRAPER": [
90
+ r"^feature/scraper[-/_]",
91
+ r"^feature/crawler[-/_]",
92
+ r"^scraper/",
93
+ r"^crawler/",
94
+ r"[-/_]scraper[-/_]",
95
+ r"[-/_]crawler[-/_]",
96
+ r"[-/_]scraping[-/_]",
87
97
  ],
88
- 'SCRAPER': [
89
- r'^feature/scraper[-/_]',
90
- r'^feature/crawler[-/_]',
91
- r'^scraper/',
92
- r'^crawler/',
93
- r'[-/_]scraper[-/_]',
94
- r'[-/_]crawler[-/_]',
95
- r'[-/_]scraping[-/_]'
96
- ]
97
98
  }
98
-
99
- def _compile_patterns(self) -> Dict[str, List[re.Pattern]]:
99
+
100
+ def _compile_patterns(self) -> dict[str, list[re.Pattern]]:
100
101
  """Compile regex patterns for efficiency."""
101
102
  compiled = {}
102
103
  for project, patterns in self.mapping_rules.items():
103
104
  compiled[project] = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
104
105
  return compiled
105
-
106
+
106
107
  def map_branch_to_project(self, branch_name: str, repo_path: Optional[Path] = None) -> str:
107
108
  """
108
109
  Map a branch name to a project key.
109
-
110
+
110
111
  Args:
111
112
  branch_name: Git branch name
112
113
  repo_path: Optional repository path for context
113
-
114
+
114
115
  Returns:
115
116
  Project key or 'UNKNOWN'
116
117
  """
117
- if not branch_name or branch_name in ['main', 'master', 'develop', 'development']:
118
+ if not branch_name or branch_name in ["main", "master", "develop", "development"]:
118
119
  # Try to infer from repo path if available
119
120
  if repo_path:
120
121
  return self._infer_from_repo_path(repo_path)
121
- return 'UNKNOWN'
122
-
122
+ return "UNKNOWN"
123
+
123
124
  # Check against compiled patterns
124
125
  for project, patterns in self.compiled_rules.items():
125
126
  for pattern in patterns:
126
127
  if pattern.search(branch_name):
127
128
  return project
128
-
129
+
129
130
  # Try to extract from ticket references in branch name
130
131
  ticket_project = self._extract_from_ticket(branch_name)
131
132
  if ticket_project:
132
133
  return ticket_project
133
-
134
+
134
135
  # Try to infer from repo path if available
135
136
  if repo_path:
136
137
  return self._infer_from_repo_path(repo_path)
137
-
138
- return 'UNKNOWN'
139
-
138
+
139
+ return "UNKNOWN"
140
+
140
141
  def _extract_from_ticket(self, branch_name: str) -> Optional[str]:
141
142
  """Extract project from ticket reference in branch name."""
142
143
  # Common ticket patterns
143
144
  ticket_patterns = [
144
- r'([A-Z]{2,})-\d+', # JIRA style: PROJ-123
145
- r'#([A-Z]{2,})\d+', # Hash prefix: #PROJ123
146
- r'([A-Z]{2,})_\d+', # Underscore: PROJ_123
145
+ r"([A-Z]{2,})-\d+", # JIRA style: PROJ-123
146
+ r"#([A-Z]{2,})\d+", # Hash prefix: #PROJ123
147
+ r"([A-Z]{2,})_\d+", # Underscore: PROJ_123
147
148
  ]
148
-
149
+
149
150
  for pattern in ticket_patterns:
150
151
  match = re.search(pattern, branch_name, re.IGNORECASE)
151
152
  if match:
152
153
  prefix = match.group(1).upper()
153
154
  # Map common prefixes to projects
154
155
  prefix_map = {
155
- 'FE': 'FRONTEND',
156
- 'BE': 'BACKEND',
157
- 'SVC': 'SERVICE',
158
- 'MOB': 'MOBILE',
159
- 'DATA': 'DATA',
160
- 'ETL': 'DATA',
161
- 'INFRA': 'INFRA',
162
- 'OPS': 'INFRA',
163
- 'SCRAPE': 'SCRAPER',
164
- 'CRAWL': 'SCRAPER'
156
+ "FE": "FRONTEND",
157
+ "BE": "BACKEND",
158
+ "SVC": "SERVICE",
159
+ "MOB": "MOBILE",
160
+ "DATA": "DATA",
161
+ "ETL": "DATA",
162
+ "INFRA": "INFRA",
163
+ "OPS": "INFRA",
164
+ "SCRAPE": "SCRAPER",
165
+ "CRAWL": "SCRAPER",
165
166
  }
166
-
167
+
167
168
  if prefix in prefix_map:
168
169
  return prefix_map[prefix]
169
-
170
+
170
171
  # Check if prefix matches any project key
171
- for project in self.mapping_rules.keys():
172
+ for project in self.mapping_rules:
172
173
  if prefix == project or prefix in project:
173
174
  return project
174
-
175
+
175
176
  return None
176
-
177
+
177
178
  def _infer_from_repo_path(self, repo_path: Path) -> str:
178
179
  """Infer project from repository path."""
179
180
  repo_name = repo_path.name.lower()
180
-
181
+
181
182
  # Direct mapping
182
183
  path_map = {
183
- 'frontend': 'FRONTEND',
184
- 'backend': 'BACKEND',
185
- 'service': 'SERVICE',
186
- 'service-ts': 'SERVICE_TS',
187
- 'services': 'SERVICES',
188
- 'mobile': 'MOBILE',
189
- 'ios': 'MOBILE',
190
- 'android': 'MOBILE',
191
- 'data': 'DATA',
192
- 'etl': 'DATA',
193
- 'infra': 'INFRA',
194
- 'infrastructure': 'INFRA',
195
- 'scraper': 'SCRAPER',
196
- 'crawler': 'SCRAPER',
197
- 'scrapers': 'SCRAPER'
184
+ "frontend": "FRONTEND",
185
+ "backend": "BACKEND",
186
+ "service": "SERVICE",
187
+ "service-ts": "SERVICE_TS",
188
+ "services": "SERVICES",
189
+ "mobile": "MOBILE",
190
+ "ios": "MOBILE",
191
+ "android": "MOBILE",
192
+ "data": "DATA",
193
+ "etl": "DATA",
194
+ "infra": "INFRA",
195
+ "infrastructure": "INFRA",
196
+ "scraper": "SCRAPER",
197
+ "crawler": "SCRAPER",
198
+ "scrapers": "SCRAPER",
198
199
  }
199
-
200
+
200
201
  for key, project in path_map.items():
201
202
  if key in repo_name:
202
203
  return project
203
-
204
+
204
205
  # Check parent directory
205
206
  if repo_path.parent.name.lower() in path_map:
206
207
  return path_map[repo_path.parent.name.lower()]
207
-
208
- return 'UNKNOWN'
209
-
210
- def add_mapping_rule(self, project: str, patterns: List[str]):
208
+
209
+ return "UNKNOWN"
210
+
211
+ def add_mapping_rule(self, project: str, patterns: list[str]) -> None:
211
212
  """Add custom mapping rules for a project."""
212
213
  if project not in self.mapping_rules:
213
214
  self.mapping_rules[project] = []
214
-
215
+
215
216
  self.mapping_rules[project].extend(patterns)
216
-
217
+
217
218
  # Recompile patterns
218
219
  self.compiled_rules[project] = [
219
- re.compile(pattern, re.IGNORECASE)
220
- for pattern in self.mapping_rules[project]
221
- ]
220
+ re.compile(pattern, re.IGNORECASE) for pattern in self.mapping_rules[project]
221
+ ]