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
@@ -1,735 +0,0 @@
1
- """Results screen for GitFlow Analytics TUI."""
2
-
3
- import json
4
- from datetime import datetime
5
- from pathlib import Path
6
- from typing import Any, Optional
7
-
8
- from rich.table import Table
9
- from textual.binding import Binding
10
- from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
11
- from textual.screen import Screen
12
- from textual.widgets import (
13
- Button,
14
- Footer,
15
- Header,
16
- Label,
17
- RadioButton,
18
- Rule,
19
- Static,
20
- TabbedContent,
21
- TabPane,
22
- )
23
-
24
- from gitflow_analytics.config import Config
25
-
26
- from ..widgets.data_table import EnhancedDataTable
27
- from ..widgets.export_modal import ExportModal
28
-
29
-
30
- class ResultsScreen(Screen):
31
- """
32
- Screen displaying comprehensive analysis results with interactive exploration.
33
-
34
- WHY: Analysis results are complex and multi-dimensional, requiring an
35
- interactive interface that allows users to explore different aspects of
36
- the data. Tabbed layout organizes information logically while providing
37
- powerful data exploration capabilities.
38
-
39
- DESIGN DECISION: Uses tabbed interface to separate different result categories
40
- while providing consistent export functionality across all views. Interactive
41
- tables allow users to sort, filter, and drill down into specific data points.
42
- """
43
-
44
- BINDINGS = [
45
- Binding("escape", "back", "Back to Main"),
46
- Binding("ctrl+s", "export", "Export Results"),
47
- Binding("ctrl+f", "filter", "Filter Data"),
48
- Binding("r", "refresh", "Refresh View"),
49
- Binding("ctrl+e", "export_current", "Export Current View"),
50
- ]
51
-
52
- def __init__(
53
- self,
54
- commits: list[dict],
55
- prs: list[dict],
56
- developers: list[dict],
57
- config: Config,
58
- *,
59
- name: Optional[str] = None,
60
- id: Optional[str] = None,
61
- ) -> None:
62
- super().__init__(name=name, id=id)
63
- self.commits = commits
64
- self.prs = prs
65
- self.developers = developers
66
- self.config = config
67
- self.current_tab = "summary"
68
-
69
- def compose(self):
70
- """Compose the results screen."""
71
- yield Header()
72
-
73
- with Container(id="results-container"):
74
- yield Label("GitFlow Analytics - Results", classes="screen-title")
75
-
76
- with TabbedContent(initial="summary"):
77
- # Summary Tab
78
- with TabPane("Summary", id="summary"):
79
- yield self._create_summary_panel()
80
-
81
- # Developers Tab
82
- with TabPane("Developers", id="developers"):
83
- yield self._create_developers_panel()
84
-
85
- # Commits Tab
86
- with TabPane("Commits", id="commits"):
87
- yield self._create_commits_panel()
88
-
89
- # Pull Requests Tab (if available)
90
- if self.prs:
91
- with TabPane("Pull Requests", id="pull-requests"):
92
- yield self._create_prs_panel()
93
-
94
- # Qualitative Insights Tab (if available)
95
- if self._has_qualitative_data():
96
- with TabPane("Qualitative Insights", id="qualitative"):
97
- yield self._create_qualitative_panel()
98
-
99
- # Export Tab
100
- with TabPane("Export", id="export"):
101
- yield self._create_export_panel()
102
-
103
- yield Footer()
104
-
105
- def _create_summary_panel(self) -> ScrollableContainer:
106
- """
107
- Create comprehensive summary statistics panel.
108
-
109
- WHY: Provides high-level overview of all analysis results in a single view,
110
- allowing users to quickly understand the overall scope and key metrics
111
- without diving into detailed data tables.
112
- """
113
- # Build all widgets first, then add to container
114
- widgets = []
115
-
116
- # Key metrics section
117
- widgets.append(Label("Analysis Summary", classes="section-title"))
118
-
119
- # Create summary table
120
- summary_table = Table(show_header=False, show_edge=False, pad_edge=False)
121
- summary_table.add_column("Metric", style="bold cyan", width=25)
122
- summary_table.add_column("Value", style="green", width=15)
123
- summary_table.add_column("Details", style="dim", width=40)
124
-
125
- # Calculate key metrics
126
- total_commits = len(self.commits)
127
- total_prs = len(self.prs)
128
- total_developers = len(self.developers)
129
-
130
- # Time range
131
- if self.commits:
132
- dates = [c.get("timestamp") for c in self.commits if c.get("timestamp")]
133
- if dates:
134
- min_date = min(dates).strftime("%Y-%m-%d")
135
- max_date = max(dates).strftime("%Y-%m-%d")
136
- date_range = f"{min_date} to {max_date}"
137
- else:
138
- date_range = "Unknown"
139
- else:
140
- date_range = "No data"
141
-
142
- # Story points
143
- total_story_points = sum(c.get("story_points", 0) or 0 for c in self.commits)
144
-
145
- # Ticket coverage
146
- commits_with_tickets = sum(1 for c in self.commits if c.get("ticket_references"))
147
- ticket_coverage = (commits_with_tickets / total_commits * 100) if total_commits > 0 else 0
148
-
149
- # Add metrics to table
150
- summary_table.add_row(
151
- "Total Commits", f"{total_commits:,}", "All commits in analysis period"
152
- )
153
- summary_table.add_row("Total Pull Requests", f"{total_prs:,}", "Detected pull requests")
154
- summary_table.add_row(
155
- "Active Developers", f"{total_developers:,}", "Unique developer identities"
156
- )
157
- summary_table.add_row("Analysis Period", date_range, "Date range of analyzed commits")
158
- summary_table.add_row(
159
- "Story Points", f"{total_story_points:,}", "Total story points completed"
160
- )
161
- summary_table.add_row(
162
- "Ticket Coverage", f"{ticket_coverage:.1f}%", "Commits with ticket references"
163
- )
164
-
165
- from rich.panel import Panel
166
-
167
- widgets.append(Static(Panel(summary_table, title="Key Metrics", border_style="blue")))
168
-
169
- # Top contributors section
170
- widgets.append(Rule())
171
- widgets.append(Label("Top Contributors", classes="section-title"))
172
-
173
- if self.developers:
174
- top_devs = sorted(
175
- self.developers, key=lambda d: d.get("total_commits", 0), reverse=True
176
- )[:10]
177
-
178
- contrib_table = Table(show_header=True, header_style="bold magenta")
179
- contrib_table.add_column("Developer", width=25)
180
- contrib_table.add_column("Commits", justify="right", width=10)
181
- contrib_table.add_column("Story Points", justify="right", width=12)
182
- contrib_table.add_column("Avg Points/Commit", justify="right", width=15)
183
-
184
- for dev in top_devs:
185
- commits = dev.get("total_commits", 0)
186
- points = dev.get("total_story_points", 0)
187
- avg_points = points / commits if commits > 0 else 0
188
-
189
- contrib_table.add_row(
190
- dev.get("primary_name", "Unknown")[:23],
191
- f"{commits:,}",
192
- f"{points:,}",
193
- f"{avg_points:.1f}",
194
- )
195
-
196
- widgets.append(
197
- Static(Panel(contrib_table, title="Developer Activity", border_style="green"))
198
- )
199
-
200
- # Qualitative insights summary (if available)
201
- if self._has_qualitative_data():
202
- widgets.append(Rule())
203
- widgets.append(Label("Qualitative Analysis Summary", classes="section-title"))
204
- widgets.append(Static(self._create_qualitative_summary()))
205
-
206
- # Create container with all widgets
207
- return ScrollableContainer(*widgets)
208
-
209
- def _create_developers_panel(self) -> Container:
210
- """Create interactive developers data panel."""
211
- # Create enhanced data table
212
- developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
213
-
214
- # Create container with all widgets
215
- return Container(
216
- Label("Developer Statistics", classes="section-title"),
217
- Static(
218
- f"Showing {len(self.developers)} unique developers. Click column headers to sort.",
219
- classes="help-text",
220
- ),
221
- developers_table,
222
- Horizontal(
223
- Button("Export Developers", id="export-developers"),
224
- Button("Show Identity Details", id="show-identities"),
225
- classes="action-bar",
226
- ),
227
- )
228
-
229
- def _create_commits_panel(self) -> Container:
230
- """Create interactive commits data panel."""
231
- # Build widgets list
232
- widgets = []
233
-
234
- widgets.append(Label("Commit Analysis", classes="section-title"))
235
- widgets.append(
236
- Static(
237
- f"Showing {len(self.commits)} commits. Click column headers to sort.",
238
- classes="help-text",
239
- )
240
- )
241
-
242
- # Create commits table with meaningful columns
243
- commits_table = Table(show_header=True, header_style="bold magenta", expand=True)
244
- commits_table.add_column("Date", width=12)
245
- commits_table.add_column("Developer", width=20)
246
- commits_table.add_column("Message", width=50)
247
- commits_table.add_column("Points", width=8, justify="right")
248
- commits_table.add_column("Tickets", width=15)
249
- commits_table.add_column("Files", width=8, justify="right")
250
-
251
- # Add rows (limit to recent 100 for performance)
252
- for commit in sorted(self.commits, key=lambda c: c.get("timestamp", ""), reverse=True)[
253
- :100
254
- ]:
255
- timestamp = commit.get("timestamp", "")
256
- date_str = timestamp.strftime("%Y-%m-%d") if timestamp else "Unknown"
257
-
258
- developer = commit.get("author_name", "Unknown")[:18]
259
- message = commit.get("message", "")[:48]
260
- story_points = commit.get("story_points", 0) or 0
261
- tickets = ", ".join(commit.get("ticket_references", []))[:13]
262
- files = commit.get("files_changed", 0)
263
-
264
- commits_table.add_row(
265
- date_str,
266
- developer,
267
- message,
268
- str(story_points) if story_points > 0 else "",
269
- tickets,
270
- str(files) if files > 0 else "",
271
- )
272
-
273
- # Create enhanced data table
274
- commits_table_widget = EnhancedDataTable(data=self.commits[:100], id="commits-table")
275
-
276
- widgets.append(commits_table_widget)
277
-
278
- # Action buttons
279
- widgets.append(
280
- Horizontal(
281
- Button("Export Commits", id="export-commits"),
282
- Button("Show Untracked", id="show-untracked"),
283
- classes="action-bar",
284
- )
285
- )
286
-
287
- return Container(*widgets)
288
-
289
- def _create_prs_panel(self) -> Container:
290
- """Create pull requests data panel."""
291
- widgets = []
292
-
293
- widgets.append(Label("Pull Request Analysis", classes="section-title"))
294
- widgets.append(Static(f"Showing {len(self.prs)} pull requests.", classes="help-text"))
295
-
296
- # Create PR table
297
- pr_table = Table(show_header=True, header_style="bold magenta", expand=True)
298
- pr_table.add_column("PR #", width=8)
299
- pr_table.add_column("Title", width=40)
300
- pr_table.add_column("Author", width=20)
301
- pr_table.add_column("Status", width=10)
302
- pr_table.add_column("Created", width=12)
303
- pr_table.add_column("Commits", width=8, justify="right")
304
-
305
- # Add rows
306
- for pr in self.prs[:100]: # Limit for performance
307
- pr_number = pr.get("number", "")
308
- title = pr.get("title", "")[:38]
309
- author = pr.get("author", "Unknown")[:18]
310
- status = pr.get("state", "unknown")
311
- created = pr.get("created_at", "")
312
- if created:
313
- created_str = (
314
- created.strftime("%Y-%m-%d")
315
- if hasattr(created, "strftime")
316
- else str(created)[:10]
317
- )
318
- else:
319
- created_str = "Unknown"
320
- commit_count = pr.get("commit_count", 0)
321
-
322
- pr_table.add_row(
323
- str(pr_number),
324
- title,
325
- author,
326
- status,
327
- created_str,
328
- str(commit_count) if commit_count > 0 else "",
329
- )
330
-
331
- # Create enhanced data table
332
- prs_table = EnhancedDataTable(data=self.prs, id="prs-table")
333
-
334
- widgets.append(prs_table)
335
-
336
- # Action buttons
337
- widgets.append(Horizontal(Button("Export PRs", id="export-prs"), classes="action-bar"))
338
-
339
- return Container(*widgets)
340
-
341
- def _create_qualitative_panel(self) -> ScrollableContainer:
342
- """Create qualitative analysis panel with enhanced insights."""
343
- widgets = []
344
-
345
- widgets.append(Label("Qualitative Analysis Results", classes="section-title"))
346
-
347
- if self._has_qualitative_data():
348
- widgets.append(Static("AI-powered insights from commit analysis", classes="help-text"))
349
- widgets.append(
350
- Static(
351
- "Note: Qualitative analysis is based on commit messages and may not reflect "
352
- "actual implementation details.",
353
- classes="warning-text",
354
- )
355
- )
356
- else:
357
- widgets.append(
358
- Static(
359
- "No qualitative analysis data available. Enable OpenAI integration to generate insights.",
360
- classes="warning-text",
361
- )
362
- )
363
- return ScrollableContainer(*widgets)
364
-
365
- # Get qualitative data
366
- qual_data = self._get_qualitative_data()
367
- summary_data = qual_data.get("summary", {})
368
-
369
- # Change Type Distribution
370
- widgets.append(Label("Change Type Distribution", classes="subsection-title"))
371
-
372
- change_table = Table(show_header=True, header_style="bold cyan")
373
- change_table.add_column("Type", width=20)
374
- change_table.add_column("Count", width=10, justify="right")
375
- change_table.add_column("Percentage", width=12, justify="right")
376
-
377
- change_types = summary_data.get("change_types", {})
378
- total_changes = sum(change_types.values())
379
-
380
- for change_type, count in sorted(change_types.items(), key=lambda x: x[1], reverse=True):
381
- percentage = (count / total_changes * 100) if total_changes > 0 else 0
382
- change_table.add_row(change_type.title(), str(count), f"{percentage:.1f}%")
383
-
384
- from rich.panel import Panel
385
-
386
- widgets.append(Static(Panel(change_table, title="Change Types", border_style="cyan")))
387
-
388
- # Risk Level Distribution
389
- widgets.append(Rule())
390
- widgets.append(Label("Risk Level Distribution", classes="subsection-title"))
391
-
392
- risk_table = Table(show_header=True, header_style="bold red")
393
- risk_table.add_column("Risk Level", width=20)
394
- risk_table.add_column("Count", width=10, justify="right")
395
- risk_table.add_column("Percentage", width=12, justify="right")
396
-
397
- risk_levels = summary_data.get("risk_levels", {})
398
- for level, count in risk_levels.items():
399
- percentage = (count / total_changes * 100) if total_changes > 0 else 0
400
- risk_table.add_row(level.title(), str(count), f"{percentage:.1f}%")
401
-
402
- widgets.append(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
403
-
404
- # Business Domain Activity
405
- widgets.append(Rule())
406
- widgets.append(Label("Business Domain Activity", classes="subsection-title"))
407
-
408
- domain_table = Table(show_header=True, header_style="bold green")
409
- domain_table.add_column("Domain", width=20)
410
- domain_table.add_column("Commits", width=10, justify="right")
411
- domain_table.add_column("Focus", width=12, justify="right")
412
-
413
- domains = summary_data.get("business_domains", {})
414
- for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True)[:10]:
415
- percentage = (count / total_changes * 100) if total_changes > 0 else 0
416
- domain_table.add_row(domain.title(), str(count), f"{percentage:.1f}%")
417
-
418
- widgets.append(Static(Panel(domain_table, title="Business Domains", border_style="green")))
419
-
420
- # Analysis Confidence
421
- if "confidence_scores" in summary_data:
422
- widgets.append(Rule())
423
- widgets.append(Label("Analysis Confidence", classes="subsection-title"))
424
-
425
- conf_scores = summary_data["confidence_scores"]
426
- conf_table = Table(show_header=False, show_edge=False)
427
- conf_table.add_column("Metric", width=25)
428
- conf_table.add_column("Score", width=15)
429
-
430
- conf_table.add_row("Average Confidence", f"{conf_scores.get('average', 0):.1f}%")
431
- conf_table.add_row(
432
- "High Confidence", f"{conf_scores.get('high_confidence_pct', 0):.1f}%"
433
- )
434
- conf_table.add_row("Low Confidence", f"{conf_scores.get('low_confidence_pct', 0):.1f}%")
435
-
436
- widgets.append(
437
- Static(Panel(conf_table, title="ML Model Confidence", border_style="yellow"))
438
- )
439
-
440
- return ScrollableContainer(*widgets)
441
-
442
- def _create_export_panel(self) -> Container:
443
- """Create export options panel."""
444
- widgets = []
445
-
446
- widgets.append(Label("Export Analysis Results", classes="section-title"))
447
- widgets.append(
448
- Static(
449
- "Select export format and click export to save results.",
450
- classes="help-text",
451
- )
452
- )
453
-
454
- # Export options
455
- export_options = Vertical(
456
- RadioButton("CSV Reports (Multiple Files)", id="export-csv", value=True),
457
- RadioButton("JSON (Complete Data)", id="export-json"),
458
- RadioButton("Markdown Report", id="export-markdown"),
459
- Rule(),
460
- Button("Export Now", variant="primary", id="export-now"),
461
- Label("", id="export-path-label"),
462
- id="export-options",
463
- )
464
-
465
- widgets.append(export_options)
466
- widgets.append(Rule())
467
- widgets.append(Static("", id="export-status"))
468
-
469
- return Container(*widgets)
470
-
471
- def _has_qualitative_data(self) -> bool:
472
- """Check if qualitative analysis data is available."""
473
- return any("change_type" in commit for commit in self.commits)
474
-
475
- def _create_qualitative_summary(self) -> str:
476
- """Create a text summary of qualitative insights."""
477
- if not self._has_qualitative_data():
478
- return "No qualitative data available"
479
-
480
- # Count change types and risk levels
481
- change_types = {}
482
- risk_levels = {}
483
-
484
- for commit in self.commits:
485
- if "change_type" in commit:
486
- change_type = commit.get("change_type", "unknown")
487
- change_types[change_type] = change_types.get(change_type, 0) + 1
488
-
489
- risk_level = commit.get("risk_level", "unknown")
490
- risk_levels[risk_level] = risk_levels.get(risk_level, 0) + 1
491
-
492
- # Find most common values
493
- top_change_type = (
494
- max(change_types.items(), key=lambda x: x[1]) if change_types else ("unknown", 0)
495
- )
496
- top_risk_level = (
497
- max(risk_levels.items(), key=lambda x: x[1]) if risk_levels else ("unknown", 0)
498
- )
499
-
500
- total_analyzed = sum(change_types.values())
501
-
502
- return f"""Qualitative Analysis Summary:
503
- • Total commits analyzed: {total_analyzed:,}
504
- • Most common change type: {top_change_type[0]} ({top_change_type[1]} commits)
505
- • Most common risk level: {top_risk_level[0]} ({top_risk_level[1]} commits)
506
- • Coverage: {(total_analyzed/len(self.commits)*100):.1f}% of all commits"""
507
-
508
- def on_button_pressed(self, event: Button.Pressed) -> None:
509
- """Handle button press events."""
510
- export_actions = {
511
- "export-summary-csv": lambda: self._export_data("summary", "csv"),
512
- "export-developers-csv": lambda: self._export_data("developers", "csv"),
513
- "export-commits-csv": lambda: self._export_data("commits", "csv"),
514
- "export-prs-csv": lambda: self._export_data("prs", "csv"),
515
- "export-qualitative-csv": lambda: self._export_data("qualitative", "csv"),
516
- "export-json": lambda: self._export_data("complete", "json"),
517
- "export-markdown": lambda: self._export_data("report", "markdown"),
518
- }
519
-
520
- action = export_actions.get(event.button.id)
521
- if action:
522
- action()
523
- else:
524
- # Handle other button actions
525
- if event.button.id == "show-identities":
526
- self._show_identity_details()
527
- elif event.button.id == "show-pr-metrics":
528
- self._show_pr_metrics()
529
-
530
- def _export_data(self, data_type: str, format_type: str) -> None:
531
- """
532
- Export specific data type in specified format.
533
-
534
- WHY: Provides flexible export functionality that allows users to
535
- export exactly the data they need in their preferred format.
536
- """
537
- try:
538
- # Determine data and filename based on type
539
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
540
-
541
- if data_type == "summary":
542
- data = self._prepare_summary_data()
543
- elif data_type == "developers":
544
- data = self.developers
545
- elif data_type == "commits":
546
- data = self.commits
547
- elif data_type == "prs":
548
- data = self.prs
549
- elif data_type == "qualitative":
550
- data = self._prepare_qualitative_data()
551
- elif data_type == "complete":
552
- data = {
553
- "commits": self.commits,
554
- "prs": self.prs,
555
- "developers": self.developers,
556
- "config": self.config.__dict__ if hasattr(self.config, "__dict__") else {},
557
- }
558
- else:
559
- self.notify("Unknown export type", severity="error")
560
- return
561
-
562
- # Show export modal
563
- export_modal = ExportModal(
564
- available_formats=[format_type.upper()],
565
- default_path=Path("./reports"),
566
- data_info={
567
- "type": data_type,
568
- "row_count": len(data) if isinstance(data, list) else "N/A",
569
- "timestamp": timestamp,
570
- },
571
- )
572
-
573
- def handle_export(config):
574
- if config:
575
- self._perform_export(data, config, format_type)
576
-
577
- self.app.push_screen(export_modal, handle_export)
578
-
579
- except Exception as e:
580
- self.notify(f"Export preparation failed: {e}", severity="error")
581
-
582
- def _perform_export(self, data: Any, export_config: dict[str, Any], format_type: str) -> None:
583
- """Perform the actual export operation."""
584
- try:
585
- export_path = export_config["path"]
586
-
587
- if format_type == "csv":
588
- self._export_to_csv(data, export_path, export_config)
589
- elif format_type == "json":
590
- self._export_to_json(data, export_path, export_config)
591
- elif format_type == "markdown":
592
- self._export_to_markdown(data, export_path, export_config)
593
-
594
- self.notify(f"Successfully exported to {export_path}", severity="success")
595
-
596
- except Exception as e:
597
- self.notify(f"Export failed: {e}", severity="error")
598
-
599
- def _export_to_csv(self, data: list[dict], path: Path, config: dict[str, Any]) -> None:
600
- """Export data to CSV format."""
601
- import csv
602
-
603
- if not data:
604
- return
605
-
606
- # Ensure parent directory exists
607
- path.parent.mkdir(parents=True, exist_ok=True)
608
-
609
- with open(path, "w", newline="", encoding="utf-8") as csvfile:
610
- fieldnames = list(data[0].keys())
611
- writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
612
-
613
- if config.get("include_headers", True):
614
- writer.writeheader()
615
-
616
- for row in data:
617
- # Anonymize if requested
618
- if config.get("anonymize", False):
619
- row = self._anonymize_row(row)
620
- writer.writerow(row)
621
-
622
- def _export_to_json(self, data: Any, path: Path, config: dict[str, Any]) -> None:
623
- """Export data to JSON format."""
624
- # Ensure parent directory exists
625
- path.parent.mkdir(parents=True, exist_ok=True)
626
-
627
- # Anonymize if requested
628
- if config.get("anonymize", False):
629
- data = self._anonymize_data(data)
630
-
631
- with open(path, "w", encoding="utf-8") as jsonfile:
632
- json.dump(data, jsonfile, indent=2, default=str)
633
-
634
- def _export_to_markdown(self, data: Any, path: Path, config: dict[str, Any]) -> None:
635
- """Export data as markdown report."""
636
- self.notify("Markdown export not yet implemented", severity="info")
637
- # TODO: Implement markdown report generation
638
-
639
- def _prepare_summary_data(self) -> list[dict]:
640
- """Prepare summary statistics for export."""
641
- return [
642
- {"metric": "Total Commits", "value": len(self.commits)},
643
- {"metric": "Total PRs", "value": len(self.prs)},
644
- {"metric": "Active Developers", "value": len(self.developers)},
645
- {
646
- "metric": "Total Story Points",
647
- "value": sum(c.get("story_points", 0) or 0 for c in self.commits),
648
- },
649
- ]
650
-
651
- def _prepare_qualitative_data(self) -> list[dict]:
652
- """Prepare qualitative analysis data for export."""
653
- qualitative_commits = []
654
- for commit in self.commits:
655
- if "change_type" in commit:
656
- qual_commit = {
657
- "commit_hash": commit.get("hash"),
658
- "author": commit.get("author_name"),
659
- "message": commit.get("message"),
660
- "change_type": commit.get("change_type"),
661
- "business_domain": commit.get("business_domain"),
662
- "risk_level": commit.get("risk_level"),
663
- "confidence_score": commit.get("confidence_score"),
664
- }
665
- qualitative_commits.append(qual_commit)
666
- return qualitative_commits
667
-
668
- def _anonymize_row(self, row: dict) -> dict:
669
- """Anonymize sensitive data in a row."""
670
- # Simple anonymization - replace names with hashed versions
671
- anonymized = row.copy()
672
-
673
- # Fields to anonymize
674
- sensitive_fields = ["author_name", "author_email", "primary_name", "primary_email"]
675
-
676
- for field in sensitive_fields:
677
- if field in anonymized and anonymized[field]:
678
- # Simple hash-based anonymization
679
- import hashlib
680
-
681
- hash_value = hashlib.md5(str(anonymized[field]).encode()).hexdigest()[:8]
682
- anonymized[field] = f"User_{hash_value}"
683
-
684
- return anonymized
685
-
686
- def _anonymize_data(self, data: Any) -> Any:
687
- """Anonymize data structure recursively."""
688
- if isinstance(data, list):
689
- return [self._anonymize_data(item) for item in data]
690
- elif isinstance(data, dict):
691
- return {key: self._anonymize_data(value) for key, value in data.items()}
692
- else:
693
- return data
694
-
695
- def _show_identity_details(self) -> None:
696
- """Show detailed developer identity information."""
697
- self.notify("Identity details view not yet implemented", severity="info")
698
-
699
- def _show_pr_metrics(self) -> None:
700
- """Show detailed pull request metrics."""
701
- self.notify("PR metrics view not yet implemented", severity="info")
702
-
703
- def action_back(self) -> None:
704
- """Go back to main screen."""
705
- self.app.pop_screen()
706
-
707
- def action_export(self) -> None:
708
- """Show export options."""
709
- # Switch to export tab
710
- tabbed_content = self.query_one(TabbedContent)
711
- tabbed_content.active = "export"
712
-
713
- def action_filter(self) -> None:
714
- """Show filter options for current tab."""
715
- self.notify("Filtering functionality not yet implemented", severity="info")
716
-
717
- def action_refresh(self) -> None:
718
- """Refresh current view."""
719
- self.refresh()
720
-
721
- def action_export_current(self) -> None:
722
- """Export data from currently active tab."""
723
- tabbed_content = self.query_one(TabbedContent)
724
- current_tab = tabbed_content.active
725
-
726
- if current_tab == "developers":
727
- self._export_data("developers", "csv")
728
- elif current_tab == "commits":
729
- self._export_data("commits", "csv")
730
- elif current_tab == "pull-requests":
731
- self._export_data("prs", "csv")
732
- elif current_tab == "qualitative":
733
- self._export_data("qualitative", "csv")
734
- else:
735
- self._export_data("summary", "csv")