gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.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.
Files changed (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4108 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +904 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +441 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1193 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,499 @@
1
+ """Report factory for creating and managing report generators.
2
+
3
+ This module implements the factory pattern for report generation,
4
+ allowing dynamic creation and registration of report generators.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Type, Union
10
+
11
+ from .base import BaseReportGenerator, CompositeReportGenerator, ReportData, ReportOutput
12
+ from .interfaces import IReportFactory, ReportFormat, ReportType
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ReportGeneratorRegistry:
18
+ """Registry for report generator classes."""
19
+
20
+ def __init__(self):
21
+ """Initialize the registry."""
22
+ self._generators: Dict[tuple, Type[BaseReportGenerator]] = {}
23
+ self._aliases: Dict[str, tuple] = {}
24
+
25
+ def register(
26
+ self,
27
+ report_type: ReportType,
28
+ format_type: ReportFormat,
29
+ generator_class: Type[BaseReportGenerator],
30
+ alias: Optional[str] = None
31
+ ) -> None:
32
+ """Register a report generator class.
33
+
34
+ Args:
35
+ report_type: Type of report
36
+ format_type: Format type
37
+ generator_class: Generator class to register
38
+ alias: Optional alias for the generator
39
+ """
40
+ key = (report_type, format_type)
41
+ self._generators[key] = generator_class
42
+
43
+ if alias:
44
+ self._aliases[alias] = key
45
+
46
+ logger.debug(f"Registered {generator_class.__name__} for {report_type.value}/{format_type.value}")
47
+
48
+ def get(
49
+ self,
50
+ report_type: Union[ReportType, str],
51
+ format_type: Union[ReportFormat, str]
52
+ ) -> Optional[Type[BaseReportGenerator]]:
53
+ """Get a registered generator class.
54
+
55
+ Args:
56
+ report_type: Type of report or alias
57
+ format_type: Format type
58
+
59
+ Returns:
60
+ Generator class if registered, None otherwise
61
+ """
62
+ # Handle string inputs
63
+ if isinstance(report_type, str):
64
+ if report_type in self._aliases:
65
+ key = self._aliases[report_type]
66
+ else:
67
+ try:
68
+ report_type = ReportType(report_type)
69
+ except ValueError:
70
+ return None
71
+
72
+ if isinstance(format_type, str):
73
+ try:
74
+ format_type = ReportFormat.from_string(format_type)
75
+ except ValueError:
76
+ return None
77
+
78
+ if isinstance(report_type, ReportType) and isinstance(format_type, ReportFormat):
79
+ key = (report_type, format_type)
80
+ return self._generators.get(key)
81
+
82
+ return None
83
+
84
+ def get_formats_for_report(self, report_type: ReportType) -> List[ReportFormat]:
85
+ """Get all registered formats for a report type.
86
+
87
+ Args:
88
+ report_type: Type of report
89
+
90
+ Returns:
91
+ List of supported formats
92
+ """
93
+ formats = []
94
+ for (r_type, f_type), _ in self._generators.items():
95
+ if r_type == report_type:
96
+ formats.append(f_type)
97
+ return formats
98
+
99
+ def get_report_types(self) -> List[ReportType]:
100
+ """Get all registered report types.
101
+
102
+ Returns:
103
+ List of report types
104
+ """
105
+ types = set()
106
+ for (r_type, _), _ in self._generators.items():
107
+ types.add(r_type)
108
+ return list(types)
109
+
110
+ def clear(self) -> None:
111
+ """Clear all registrations."""
112
+ self._generators.clear()
113
+ self._aliases.clear()
114
+
115
+
116
+ class ReportFactory(IReportFactory):
117
+ """Factory for creating report generators."""
118
+
119
+ def __init__(self):
120
+ """Initialize the factory."""
121
+ self._registry = ReportGeneratorRegistry()
122
+ self._default_config: Dict[str, Any] = {}
123
+ self._register_default_generators()
124
+
125
+ def create_generator(
126
+ self,
127
+ report_type: Union[ReportType, str],
128
+ format_type: Union[ReportFormat, str],
129
+ **kwargs
130
+ ) -> BaseReportGenerator:
131
+ """Create a report generator.
132
+
133
+ Args:
134
+ report_type: Type of report to generate
135
+ format_type: Format for the report
136
+ **kwargs: Additional configuration passed to generator
137
+
138
+ Returns:
139
+ Report generator instance
140
+
141
+ Raises:
142
+ ValueError: If no generator is registered for the combination
143
+ """
144
+ # Convert strings to enums if needed
145
+ if isinstance(report_type, str):
146
+ try:
147
+ report_type = ReportType(report_type)
148
+ except ValueError:
149
+ # Check if it's an alias
150
+ pass
151
+
152
+ if isinstance(format_type, str):
153
+ format_type = ReportFormat.from_string(format_type)
154
+
155
+ # Get generator class
156
+ generator_class = self._registry.get(report_type, format_type)
157
+
158
+ if not generator_class:
159
+ raise ValueError(
160
+ f"No generator registered for {report_type}/{format_type}. "
161
+ f"Available types: {self.get_supported_reports()}"
162
+ )
163
+
164
+ # Merge with default config
165
+ config = {**self._default_config, **kwargs}
166
+
167
+ # Create and return instance
168
+ return generator_class(**config)
169
+
170
+ def create_composite_generator(
171
+ self,
172
+ report_types: List[tuple[Union[ReportType, str], Union[ReportFormat, str]]],
173
+ **kwargs
174
+ ) -> CompositeReportGenerator:
175
+ """Create a composite generator for multiple report types.
176
+
177
+ Args:
178
+ report_types: List of (report_type, format_type) tuples
179
+ **kwargs: Configuration passed to all generators
180
+
181
+ Returns:
182
+ Composite generator instance
183
+ """
184
+ generators = []
185
+
186
+ for report_type, format_type in report_types:
187
+ generator = self.create_generator(report_type, format_type, **kwargs)
188
+ generators.append(generator)
189
+
190
+ return CompositeReportGenerator(generators, **kwargs)
191
+
192
+ def register_generator(
193
+ self,
194
+ report_type: ReportType,
195
+ format_type: ReportFormat,
196
+ generator_class: Type[BaseReportGenerator],
197
+ alias: Optional[str] = None
198
+ ) -> None:
199
+ """Register a report generator class.
200
+
201
+ Args:
202
+ report_type: Type of report
203
+ format_type: Format type
204
+ generator_class: Generator class to register
205
+ alias: Optional alias for quick access
206
+ """
207
+ self._registry.register(report_type, format_type, generator_class, alias)
208
+
209
+ def get_supported_formats(self, report_type: ReportType) -> List[ReportFormat]:
210
+ """Get supported formats for a report type.
211
+
212
+ Args:
213
+ report_type: Type of report
214
+
215
+ Returns:
216
+ List of supported formats
217
+ """
218
+ return self._registry.get_formats_for_report(report_type)
219
+
220
+ def get_supported_reports(self) -> List[ReportType]:
221
+ """Get list of supported report types.
222
+
223
+ Returns:
224
+ List of supported report types
225
+ """
226
+ return self._registry.get_report_types()
227
+
228
+ def set_default_config(self, config: Dict[str, Any]) -> None:
229
+ """Set default configuration for all generators.
230
+
231
+ Args:
232
+ config: Default configuration dictionary
233
+ """
234
+ self._default_config = config
235
+
236
+ def _register_default_generators(self) -> None:
237
+ """Register the default set of report generators."""
238
+ # Import here to avoid circular dependencies
239
+ try:
240
+ from .analytics_writer import AnalyticsReportGenerator
241
+ from .csv_writer import CSVReportGenerator
242
+ from .json_exporter import ComprehensiveJSONExporter
243
+ from .narrative_writer import NarrativeReportGenerator
244
+
245
+ # Register CSV generators
246
+ self.register_generator(
247
+ ReportType.WEEKLY_METRICS,
248
+ ReportFormat.CSV,
249
+ CSVReportGenerator
250
+ )
251
+ self.register_generator(
252
+ ReportType.DEVELOPER_STATS,
253
+ ReportFormat.CSV,
254
+ CSVReportGenerator
255
+ )
256
+
257
+ # Register analytics generators
258
+ self.register_generator(
259
+ ReportType.ACTIVITY_DISTRIBUTION,
260
+ ReportFormat.CSV,
261
+ AnalyticsReportGenerator
262
+ )
263
+ self.register_generator(
264
+ ReportType.DEVELOPER_FOCUS,
265
+ ReportFormat.CSV,
266
+ AnalyticsReportGenerator
267
+ )
268
+ self.register_generator(
269
+ ReportType.QUALITATIVE_INSIGHTS,
270
+ ReportFormat.CSV,
271
+ AnalyticsReportGenerator
272
+ )
273
+
274
+ # Register narrative generator
275
+ self.register_generator(
276
+ ReportType.NARRATIVE,
277
+ ReportFormat.MARKDOWN,
278
+ NarrativeReportGenerator
279
+ )
280
+
281
+ # Register JSON exporter for all types
282
+ for report_type in ReportType:
283
+ self.register_generator(
284
+ report_type,
285
+ ReportFormat.JSON,
286
+ ComprehensiveJSONExporter
287
+ )
288
+
289
+ logger.info("Default report generators registered")
290
+
291
+ except ImportError as e:
292
+ logger.warning(f"Could not import default generators: {e}")
293
+
294
+
295
+ class ReportBuilder:
296
+ """Builder pattern for constructing complex report configurations."""
297
+
298
+ def __init__(self, factory: Optional[ReportFactory] = None):
299
+ """Initialize the builder.
300
+
301
+ Args:
302
+ factory: Report factory to use (creates default if None)
303
+ """
304
+ self.factory = factory or ReportFactory()
305
+ self._report_types: List[tuple[ReportType, ReportFormat]] = []
306
+ self._config: Dict[str, Any] = {}
307
+ self._output_dir: Optional[Path] = None
308
+ self._data: Optional[ReportData] = None
309
+
310
+ def add_report(
311
+ self,
312
+ report_type: Union[ReportType, str],
313
+ format_type: Union[ReportFormat, str]
314
+ ) -> "ReportBuilder":
315
+ """Add a report to generate.
316
+
317
+ Args:
318
+ report_type: Type of report
319
+ format_type: Format type
320
+
321
+ Returns:
322
+ Self for chaining
323
+ """
324
+ if isinstance(report_type, str):
325
+ report_type = ReportType(report_type)
326
+ if isinstance(format_type, str):
327
+ format_type = ReportFormat.from_string(format_type)
328
+
329
+ self._report_types.append((report_type, format_type))
330
+ return self
331
+
332
+ def with_config(self, **kwargs) -> "ReportBuilder":
333
+ """Add configuration options.
334
+
335
+ Args:
336
+ **kwargs: Configuration options
337
+
338
+ Returns:
339
+ Self for chaining
340
+ """
341
+ self._config.update(kwargs)
342
+ return self
343
+
344
+ def with_output_dir(self, output_dir: Union[str, Path]) -> "ReportBuilder":
345
+ """Set output directory.
346
+
347
+ Args:
348
+ output_dir: Output directory path
349
+
350
+ Returns:
351
+ Self for chaining
352
+ """
353
+ self._output_dir = Path(output_dir)
354
+ return self
355
+
356
+ def with_data(self, data: ReportData) -> "ReportBuilder":
357
+ """Set report data.
358
+
359
+ Args:
360
+ data: Report data
361
+
362
+ Returns:
363
+ Self for chaining
364
+ """
365
+ self._data = data
366
+ return self
367
+
368
+ def build(self) -> Union[BaseReportGenerator, CompositeReportGenerator]:
369
+ """Build the report generator(s).
370
+
371
+ Returns:
372
+ Single generator or composite generator
373
+
374
+ Raises:
375
+ ValueError: If no reports have been added
376
+ """
377
+ if not self._report_types:
378
+ raise ValueError("No reports added to builder")
379
+
380
+ if len(self._report_types) == 1:
381
+ report_type, format_type = self._report_types[0]
382
+ return self.factory.create_generator(report_type, format_type, **self._config)
383
+ else:
384
+ return self.factory.create_composite_generator(self._report_types, **self._config)
385
+
386
+ def generate(self) -> Union[ReportOutput, List[ReportOutput]]:
387
+ """Build and generate reports.
388
+
389
+ Returns:
390
+ Report output(s)
391
+
392
+ Raises:
393
+ ValueError: If data has not been set
394
+ """
395
+ if not self._data:
396
+ raise ValueError("Report data has not been set")
397
+
398
+ generator = self.build()
399
+
400
+ if self._output_dir:
401
+ self._output_dir.mkdir(parents=True, exist_ok=True)
402
+
403
+ if isinstance(generator, CompositeReportGenerator):
404
+ # Generate multiple reports with appropriate names
405
+ outputs = []
406
+ for (report_type, format_type), gen in zip(self._report_types, generator.generators):
407
+ filename = f"{report_type.value}.{format_type.value}"
408
+ output_path = self._output_dir / filename
409
+ output = gen.generate(self._data, output_path)
410
+ outputs.append(output)
411
+ return outputs
412
+ else:
413
+ # Single report
414
+ report_type, format_type = self._report_types[0]
415
+ filename = f"{report_type.value}.{format_type.value}"
416
+ output_path = self._output_dir / filename
417
+ return generator.generate(self._data, output_path)
418
+ else:
419
+ return generator.generate(self._data)
420
+
421
+
422
+ # Singleton factory instance
423
+ _default_factory: Optional[ReportFactory] = None
424
+
425
+
426
+ def get_default_factory() -> ReportFactory:
427
+ """Get the default report factory instance.
428
+
429
+ Returns:
430
+ Default factory instance
431
+ """
432
+ global _default_factory
433
+ if _default_factory is None:
434
+ _default_factory = ReportFactory()
435
+ return _default_factory
436
+
437
+
438
+ def create_report(
439
+ report_type: Union[ReportType, str],
440
+ format_type: Union[ReportFormat, str],
441
+ data: ReportData,
442
+ output_path: Optional[Union[str, Path]] = None,
443
+ **kwargs
444
+ ) -> ReportOutput:
445
+ """Convenience function to create and generate a report.
446
+
447
+ Args:
448
+ report_type: Type of report
449
+ format_type: Format type
450
+ data: Report data
451
+ output_path: Optional output path
452
+ **kwargs: Additional configuration
453
+
454
+ Returns:
455
+ Report output
456
+ """
457
+ factory = get_default_factory()
458
+ generator = factory.create_generator(report_type, format_type, **kwargs)
459
+
460
+ if output_path:
461
+ output_path = Path(output_path)
462
+
463
+ return generator.generate(data, output_path)
464
+
465
+
466
+ def create_multiple_reports(
467
+ reports: List[tuple[Union[ReportType, str], Union[ReportFormat, str]]],
468
+ data: ReportData,
469
+ output_dir: Optional[Union[str, Path]] = None,
470
+ **kwargs
471
+ ) -> List[ReportOutput]:
472
+ """Convenience function to create multiple reports.
473
+
474
+ Args:
475
+ reports: List of (report_type, format_type) tuples
476
+ data: Report data
477
+ output_dir: Optional output directory
478
+ **kwargs: Additional configuration
479
+
480
+ Returns:
481
+ List of report outputs
482
+ """
483
+ builder = ReportBuilder()
484
+
485
+ for report_type, format_type in reports:
486
+ builder.add_report(report_type, format_type)
487
+
488
+ builder.with_config(**kwargs)
489
+ builder.with_data(data)
490
+
491
+ if output_dir:
492
+ builder.with_output_dir(output_dir)
493
+
494
+ result = builder.generate()
495
+
496
+ if isinstance(result, list):
497
+ return result
498
+ else:
499
+ return [result]