@theihtisham/dev-pulse 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/devpulse/__init__.py +4 -0
- package/devpulse/api/__init__.py +1 -0
- package/devpulse/api/app.py +371 -0
- package/devpulse/cli/__init__.py +1 -0
- package/devpulse/cli/dashboard.py +131 -0
- package/devpulse/cli/main.py +678 -0
- package/devpulse/cli/render.py +175 -0
- package/devpulse/core/__init__.py +34 -0
- package/devpulse/core/analytics.py +487 -0
- package/devpulse/core/config.py +77 -0
- package/devpulse/core/database.py +612 -0
- package/devpulse/core/github_client.py +281 -0
- package/devpulse/core/models.py +142 -0
- package/devpulse/core/report_generator.py +454 -0
- package/devpulse/static/.gitkeep +1 -0
- package/devpulse/templates/report.html +64 -0
- package/jest.config.js +7 -0
- package/package.json +35 -0
- package/pyproject.toml +80 -0
- package/requirements.txt +14 -0
- package/tests/__init__.py +1 -0
- package/tests/conftest.py +208 -0
- package/tests/test_analytics.py +284 -0
- package/tests/test_api.py +313 -0
- package/tests/test_cli.py +204 -0
- package/tests/test_config.py +47 -0
- package/tests/test_database.py +255 -0
- package/tests/test_models.py +107 -0
- package/tests/test_report_generator.py +173 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Rich terminal rendering helpers for DevPulse."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.columns import Columns
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_heatmap(console: Console, data: list[dict[str, Any]], days: int = 365) -> None:
|
|
14
|
+
"""Render a GitHub-style contribution heatmap in the terminal.
|
|
15
|
+
|
|
16
|
+
Each column represents a week, each row a day of the week (Mon-Sun).
|
|
17
|
+
Cell characters indicate intensity:
|
|
18
|
+
. = 0 commits
|
|
19
|
+
o = 1-2
|
|
20
|
+
+ = 3-5
|
|
21
|
+
# = 6-9
|
|
22
|
+
@ = 10+
|
|
23
|
+
"""
|
|
24
|
+
if not data:
|
|
25
|
+
console.print("[yellow]No activity data to display.[/yellow]")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
# Build a lookup: date_str -> count
|
|
29
|
+
counts: dict[str, int] = {d["day"]: d["count"] for d in data}
|
|
30
|
+
|
|
31
|
+
# Determine date range
|
|
32
|
+
today = datetime.utcnow().date()
|
|
33
|
+
start = today - timedelta(days=days)
|
|
34
|
+
|
|
35
|
+
# Determine max for scaling
|
|
36
|
+
max_count = max(d["count"] for d in data) if data else 1
|
|
37
|
+
if max_count == 0:
|
|
38
|
+
max_count = 1
|
|
39
|
+
|
|
40
|
+
day_labels = ["Mon", " ", "Wed", " ", "Fri", " ", "Sun"]
|
|
41
|
+
|
|
42
|
+
# Build grid: 7 rows x N weeks
|
|
43
|
+
# Each week column
|
|
44
|
+
weeks: list[list[str]] = []
|
|
45
|
+
current = start
|
|
46
|
+
# Align to start of week (Monday)
|
|
47
|
+
while current.weekday() != 0:
|
|
48
|
+
current -= timedelta(days=1)
|
|
49
|
+
|
|
50
|
+
total_commits = 0
|
|
51
|
+
total_active_days = 0
|
|
52
|
+
|
|
53
|
+
while current <= today:
|
|
54
|
+
week: list[str] = []
|
|
55
|
+
for dow in range(7):
|
|
56
|
+
d = current
|
|
57
|
+
key = d.isoformat()
|
|
58
|
+
count = counts.get(key, 0)
|
|
59
|
+
total_commits += count
|
|
60
|
+
if count > 0:
|
|
61
|
+
total_active_days += 1
|
|
62
|
+
|
|
63
|
+
if count == 0:
|
|
64
|
+
cell = "[dim].[/dim]"
|
|
65
|
+
elif count <= max_count * 0.2:
|
|
66
|
+
cell = f"[color(28)]o[/color(28)]"
|
|
67
|
+
elif count <= max_count * 0.4:
|
|
68
|
+
cell = f"[color(34)]+[/color(34)]"
|
|
69
|
+
elif count <= max_count * 0.7:
|
|
70
|
+
cell = f"[color(40)]#[/color(40)]"
|
|
71
|
+
else:
|
|
72
|
+
cell = f"[color(46)]@[/color(46)]"
|
|
73
|
+
|
|
74
|
+
week.append(cell)
|
|
75
|
+
current += timedelta(days=1)
|
|
76
|
+
weeks.append(week)
|
|
77
|
+
|
|
78
|
+
console.print(Panel("[bold]Activity Heatmap[/bold]", style="blue"))
|
|
79
|
+
|
|
80
|
+
# Print month labels
|
|
81
|
+
month_line = " "
|
|
82
|
+
last_month = ""
|
|
83
|
+
for week in weeks:
|
|
84
|
+
# Check the Wednesday of this week for month
|
|
85
|
+
mid_date = start + timedelta(weeks=weeks.index(week), days=2)
|
|
86
|
+
month_name = mid_date.strftime("%b")
|
|
87
|
+
if month_name != last_month:
|
|
88
|
+
month_line += f"{month_name:<4}"
|
|
89
|
+
last_month = month_name
|
|
90
|
+
else:
|
|
91
|
+
month_line += " "
|
|
92
|
+
console.print(month_line)
|
|
93
|
+
|
|
94
|
+
# Print each day row
|
|
95
|
+
for dow in range(7):
|
|
96
|
+
line = f"{day_labels[dow]} "
|
|
97
|
+
for week in weeks:
|
|
98
|
+
if dow < len(week):
|
|
99
|
+
line += week[dow] + " "
|
|
100
|
+
else:
|
|
101
|
+
line += " "
|
|
102
|
+
console.print(line)
|
|
103
|
+
|
|
104
|
+
console.print(f"\n Total: [bold]{total_commits}[/bold] commits across [bold]{total_active_days}[/bold] active days")
|
|
105
|
+
console.print(" Legend: [dim].[/dim]=0 [color(28)]o[/color(28)]=low [color(34)]+[/color(34)]=med [color(40)]#[/color(40)]=high [color(46)]@[/color(46)]=very high")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def render_metrics_panel(console: Console, metrics: Any) -> None:
|
|
109
|
+
"""Render a single developer's metrics as a rich panel."""
|
|
110
|
+
console.print(Panel(f"[bold]{metrics.author}[/bold]", style="cyan", expand=False))
|
|
111
|
+
|
|
112
|
+
table = Table.grid(padding=(0, 2))
|
|
113
|
+
table.add_column(justify="right", style="dim")
|
|
114
|
+
table.add_column()
|
|
115
|
+
|
|
116
|
+
table.add_row("Commits:", str(metrics.commits_count))
|
|
117
|
+
table.add_row("Active Days:", str(metrics.active_days))
|
|
118
|
+
table.add_row("Commits/Day:", str(metrics.commits_per_day))
|
|
119
|
+
table.add_row("PRs:", f"{metrics.prs_created} created, {metrics.prs_merged} merged")
|
|
120
|
+
table.add_row("Avg Merge Time:", f"{metrics.avg_pr_merge_time_hours}h")
|
|
121
|
+
table.add_row("Reviews:", str(metrics.reviews_given))
|
|
122
|
+
table.add_row("Lines:", f"+{metrics.lines_added} / -{metrics.lines_removed}")
|
|
123
|
+
|
|
124
|
+
console.print(table)
|
|
125
|
+
console.print()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_insight_card(console: Console, insight: dict[str, str], index: int) -> None:
|
|
129
|
+
"""Render a single insight as a styled card."""
|
|
130
|
+
severity = insight.get("severity", "low")
|
|
131
|
+
color = {"high": "red", "medium": "yellow", "low": "green"}.get(severity, "white")
|
|
132
|
+
icon = {"high": "!!", "medium": "!", "low": "*"}.get(severity, "*")
|
|
133
|
+
|
|
134
|
+
console.print(f" [{color}][{icon}][/{color}] {insight['message']}")
|
|
135
|
+
console.print(f" [dim]-> {insight['recommendation']}[/dim]")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def render_burndown(console: Console, points: list[dict[str, Any]]) -> None:
|
|
139
|
+
"""Render an ASCII burndown chart."""
|
|
140
|
+
if not points:
|
|
141
|
+
console.print("[yellow]No burndown data.[/yellow]")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
max_pts = max(p.get("remaining", 0) for p in points)
|
|
145
|
+
if max_pts == 0:
|
|
146
|
+
max_pts = 1
|
|
147
|
+
width = 60
|
|
148
|
+
|
|
149
|
+
console.print(f"\n{'Day':>4} | {'Remaining':>10} | Chart")
|
|
150
|
+
console.print("-" * 80)
|
|
151
|
+
|
|
152
|
+
for p in points:
|
|
153
|
+
remaining = p.get("remaining", 0)
|
|
154
|
+
ideal = p.get("ideal", 0)
|
|
155
|
+
bar_len = int(remaining / max_pts * width)
|
|
156
|
+
ideal_len = int(ideal / max_pts * width)
|
|
157
|
+
|
|
158
|
+
bar = "[green]" + "#" * bar_len + "[/green]"
|
|
159
|
+
ideal_marker = "[dim]|[/dim]" if ideal_len < width else ""
|
|
160
|
+
|
|
161
|
+
line = f"{p['day']:>4} | {remaining:>10.1f} | {bar}"
|
|
162
|
+
console.print(line)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def render_header(console: Console) -> None:
|
|
166
|
+
"""Render the DevPulse ASCII art header."""
|
|
167
|
+
header = Text()
|
|
168
|
+
header.append(" ____ _ ____ _ \n", style="bold cyan")
|
|
169
|
+
header.append(" | _ \\ _ _ ___| | __ | _ \\ __ _ _ __ ___| |__\n", style="bold cyan")
|
|
170
|
+
header.append(" | | | | | | |/ __| |/ / | |_) / _` | '_ \\ / __| '_ \\\n", style="bold cyan")
|
|
171
|
+
header.append(" | |_| | |_| | (__| < | __/ (_| | | | | (__| | | |\n", style="bold cyan")
|
|
172
|
+
header.append(" |____/ \\__,_|\\___|_|\\_\\ |_| \\__,_|_| |_|\\___|_| |_|\n", style="bold cyan")
|
|
173
|
+
header.append("\n AI-Powered Developer Productivity Dashboard", style="dim")
|
|
174
|
+
console.print(header)
|
|
175
|
+
console.print()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Core configuration and settings for DevPulse."""
|
|
2
|
+
|
|
3
|
+
from devpulse.core.config import Settings, get_settings
|
|
4
|
+
from devpulse.core.database import Database
|
|
5
|
+
from devpulse.core.models import (
|
|
6
|
+
Commit,
|
|
7
|
+
PullRequest,
|
|
8
|
+
Issue,
|
|
9
|
+
Review,
|
|
10
|
+
DeveloperMetrics,
|
|
11
|
+
SprintData,
|
|
12
|
+
TeamHealth,
|
|
13
|
+
CodeQuality,
|
|
14
|
+
Goal,
|
|
15
|
+
DailyReport,
|
|
16
|
+
WeeklyReport,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Settings",
|
|
21
|
+
"get_settings",
|
|
22
|
+
"Database",
|
|
23
|
+
"Commit",
|
|
24
|
+
"PullRequest",
|
|
25
|
+
"Issue",
|
|
26
|
+
"Review",
|
|
27
|
+
"DeveloperMetrics",
|
|
28
|
+
"SprintData",
|
|
29
|
+
"TeamHealth",
|
|
30
|
+
"CodeQuality",
|
|
31
|
+
"Goal",
|
|
32
|
+
"DailyReport",
|
|
33
|
+
"WeeklyReport",
|
|
34
|
+
]
|