gitflow-analytics 1.0.1__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.
- gitflow_analytics/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/classification/__init__.py +31 -0
- gitflow_analytics/classification/batch_classifier.py +752 -0
- gitflow_analytics/classification/classifier.py +464 -0
- gitflow_analytics/classification/feature_extractor.py +725 -0
- gitflow_analytics/classification/linguist_analyzer.py +574 -0
- gitflow_analytics/classification/model.py +455 -0
- gitflow_analytics/cli.py +4490 -378
- gitflow_analytics/cli_rich.py +503 -0
- gitflow_analytics/config/__init__.py +43 -0
- gitflow_analytics/config/errors.py +261 -0
- gitflow_analytics/config/loader.py +904 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +441 -0
- gitflow_analytics/config/validator.py +154 -0
- gitflow_analytics/config.py +44 -398
- gitflow_analytics/core/analyzer.py +1320 -172
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +1554 -175
- gitflow_analytics/core/data_fetcher.py +1193 -0
- gitflow_analytics/core/identity.py +571 -185
- gitflow_analytics/core/metrics_storage.py +526 -0
- gitflow_analytics/core/progress.py +372 -0
- gitflow_analytics/core/schema_version.py +269 -0
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +77 -59
- gitflow_analytics/extractors/tickets.py +841 -89
- gitflow_analytics/identity_llm/__init__.py +6 -0
- gitflow_analytics/identity_llm/analysis_pass.py +231 -0
- gitflow_analytics/identity_llm/analyzer.py +464 -0
- gitflow_analytics/identity_llm/models.py +76 -0
- gitflow_analytics/integrations/github_integration.py +258 -87
- gitflow_analytics/integrations/jira_integration.py +572 -123
- gitflow_analytics/integrations/orchestrator.py +206 -82
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +542 -179
- gitflow_analytics/models/database.py +986 -59
- gitflow_analytics/pm_framework/__init__.py +115 -0
- gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
- gitflow_analytics/pm_framework/base.py +406 -0
- gitflow_analytics/pm_framework/models.py +211 -0
- gitflow_analytics/pm_framework/orchestrator.py +652 -0
- gitflow_analytics/pm_framework/registry.py +333 -0
- gitflow_analytics/qualitative/__init__.py +29 -0
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
- gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
- gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
- gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
- gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
- gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
- gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
- gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
- gitflow_analytics/qualitative/core/processor.py +673 -0
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +306 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
- gitflow_analytics/qualitative/utils/metrics.py +361 -0
- gitflow_analytics/qualitative/utils/text_processing.py +285 -0
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +550 -18
- gitflow_analytics/reports/base.py +648 -0
- gitflow_analytics/reports/branch_health_writer.py +322 -0
- gitflow_analytics/reports/classification_writer.py +924 -0
- gitflow_analytics/reports/cli_integration.py +427 -0
- gitflow_analytics/reports/csv_writer.py +1700 -216
- gitflow_analytics/reports/data_models.py +504 -0
- gitflow_analytics/reports/database_report_generator.py +427 -0
- gitflow_analytics/reports/example_usage.py +344 -0
- gitflow_analytics/reports/factory.py +499 -0
- gitflow_analytics/reports/formatters.py +698 -0
- gitflow_analytics/reports/html_generator.py +1116 -0
- gitflow_analytics/reports/interfaces.py +489 -0
- gitflow_analytics/reports/json_exporter.py +2770 -0
- gitflow_analytics/reports/narrative_writer.py +2289 -158
- gitflow_analytics/reports/story_point_correlation.py +1144 -0
- gitflow_analytics/reports/weekly_trends_writer.py +389 -0
- gitflow_analytics/training/__init__.py +5 -0
- gitflow_analytics/training/model_loader.py +377 -0
- gitflow_analytics/training/pipeline.py +550 -0
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +724 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
- gitflow_analytics/tui/screens/configuration_screen.py +523 -0
- gitflow_analytics/tui/screens/loading_screen.py +348 -0
- gitflow_analytics/tui/screens/main_screen.py +321 -0
- gitflow_analytics/tui/screens/results_screen.py +722 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +255 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +187 -0
- gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.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]
|