@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.
@@ -0,0 +1,454 @@
1
+ """Report generator — produces Markdown, JSON, and HTML reports."""
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime, timedelta
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from jinja2 import Environment, FileSystemLoader, BaseLoader
10
+
11
+ from devpulse.core.database import Database
12
+ from devpulse.core.analytics import AnalyticsEngine
13
+ from devpulse.core.config import get_settings
14
+
15
+
16
+ class ReportGenerator:
17
+ """Generate daily, weekly, and monthly reports in multiple formats."""
18
+
19
+ def __init__(self, db: Optional[Database] = None) -> None:
20
+ self.db = db or Database()
21
+ self.engine = AnalyticsEngine(db=self.db)
22
+ settings = get_settings()
23
+ self.reports_dir = settings.reports_dir
24
+ Path(self.reports_dir).mkdir(parents=True, exist_ok=True)
25
+
26
+ # Set up Jinja2 for HTML reports
27
+ template_dir = Path(__file__).parent.parent / "templates"
28
+ if template_dir.exists():
29
+ self.jinja_env = Environment(loader=FileSystemLoader(str(template_dir)))
30
+ else:
31
+ self.jinja_env = Environment(loader=BaseLoader())
32
+
33
+ # ── Daily Report ─────────────────────────────────────────────────
34
+
35
+ def daily_report(
36
+ self,
37
+ target_date: Optional[str] = None,
38
+ author: Optional[str] = None,
39
+ repo: Optional[str] = None,
40
+ ) -> dict[str, Any]:
41
+ """Generate a daily report."""
42
+ if target_date is None:
43
+ target_date = datetime.utcnow().strftime("%Y-%m-%d")
44
+
45
+ start = f"{target_date}T00:00:00Z"
46
+ end = f"{target_date}T23:59:59Z"
47
+
48
+ commits = self.db.get_commits(repo=repo, author=author, since=start, until=end, limit=1000)
49
+ prs = self.db.get_pull_requests(repo=repo, author=author, since=start, limit=500)
50
+ issues = self.db.get_issues(repo=repo, since=start, limit=500)
51
+ reviews = self.db.get_reviews(repo=repo, author=author, since=start, limit=500)
52
+
53
+ prs_opened = len([p for p in prs if (p.get("created_at") or "").startswith(target_date)])
54
+ prs_merged = len([p for p in prs if (p.get("merged_at") or "").startswith(target_date)])
55
+ issues_opened = len([i for i in issues if (i.get("created_at") or "").startswith(target_date)])
56
+ issues_closed = len([i for i in issues if (i.get("closed_at") or "").startswith(target_date)])
57
+ lines_changed = sum(c.get("additions", 0) + c.get("deletions", 0) for c in commits)
58
+
59
+ # Build summary
60
+ parts: list[str] = []
61
+ parts.append(f"**{len(commits)} commit(s)**")
62
+ if prs_opened:
63
+ parts.append(f"{prs_opened} PR(s) opened")
64
+ if prs_merged:
65
+ parts.append(f"{prs_merged} PR(s) merged")
66
+ if issues_closed:
67
+ parts.append(f"{issues_closed} issue(s) closed")
68
+ if not parts:
69
+ parts.append("No activity recorded")
70
+
71
+ return {
72
+ "date": target_date,
73
+ "author": author or "all",
74
+ "commits": len(commits),
75
+ "prs_opened": prs_opened,
76
+ "prs_merged": prs_merged,
77
+ "issues_opened": issues_opened,
78
+ "issues_closed": issues_closed,
79
+ "reviews_given": len(reviews),
80
+ "lines_changed": lines_changed,
81
+ "summary": ", ".join(parts),
82
+ "commit_details": [
83
+ {"message": c.get("message", ""), "repo": c.get("repo", ""), "sha": c.get("sha", "")[:7]}
84
+ for c in commits[:10]
85
+ ],
86
+ }
87
+
88
+ # ── Weekly Report ────────────────────────────────────────────────
89
+
90
+ def weekly_report(
91
+ self,
92
+ week_start: Optional[str] = None,
93
+ repo: Optional[str] = None,
94
+ ) -> dict[str, Any]:
95
+ """Generate a weekly report."""
96
+ if week_start is None:
97
+ today = datetime.utcnow()
98
+ week_start = (today - timedelta(days=today.weekday())).strftime("%Y-%m-%d")
99
+
100
+ start_dt = datetime.strptime(week_start, "%Y-%m-%d")
101
+ end_dt = start_dt + timedelta(days=6)
102
+ week_end = end_dt.strftime("%Y-%m-%d")
103
+ since = start_dt.isoformat()
104
+
105
+ commits = self.db.get_commits(repo=repo, since=since, limit=2000)
106
+ prs = self.db.get_pull_requests(repo=repo, since=since, limit=500)
107
+ issues = self.db.get_issues(repo=repo, since=since, limit=500)
108
+
109
+ # Top contributors
110
+ author_counts: dict[str, int] = {}
111
+ for c in commits:
112
+ a = c.get("author", "unknown")
113
+ author_counts[a] = author_counts.get(a, 0) + 1
114
+ top = sorted(author_counts.items(), key=lambda x: x[1], reverse=True)[:5]
115
+
116
+ # Avg merge time
117
+ merge_hours: list[float] = []
118
+ for p in prs:
119
+ if p.get("merged_at") and p.get("created_at"):
120
+ from devpulse.core.analytics import _days_between
121
+ merge_hours.append(_days_between(p["created_at"], p["merged_at"]))
122
+ avg_merge = sum(merge_hours) / len(merge_hours) if merge_hours else 0.0
123
+
124
+ # Insights
125
+ insights_data = self.engine.generate_insights(days=7, repo=repo)
126
+
127
+ closed_issues = [i for i in issues if i.get("state") == "closed"]
128
+
129
+ return {
130
+ "week_start": week_start,
131
+ "week_end": week_end,
132
+ "authors": list(author_counts.keys()),
133
+ "total_commits": len(commits),
134
+ "total_prs": len(prs),
135
+ "total_issues_closed": len(closed_issues),
136
+ "avg_merge_time_hours": round(avg_merge, 1),
137
+ "top_contributors": [{"author": a, "commits": c} for a, c in top],
138
+ "insights": [i["message"] for i in insights_data],
139
+ "recommendations": [i["recommendation"] for i in insights_data],
140
+ }
141
+
142
+ # ── Monthly Report ───────────────────────────────────────────────
143
+
144
+ def monthly_report(
145
+ self,
146
+ year: Optional[int] = None,
147
+ month: Optional[int] = None,
148
+ repo: Optional[str] = None,
149
+ ) -> dict[str, Any]:
150
+ """Generate a monthly report."""
151
+ now = datetime.utcnow()
152
+ year = year or now.year
153
+ month = month or now.month
154
+ since = f"{year}-{month:02d}-01T00:00:00Z"
155
+
156
+ # Compute end of month
157
+ if month == 12:
158
+ until_dt = datetime(year + 1, 1, 1)
159
+ else:
160
+ until_dt = datetime(year, month + 1, 1)
161
+ until = until_dt.isoformat()
162
+
163
+ commits = self.db.get_commits(repo=repo, since=since, until=until, limit=5000)
164
+ prs = self.db.get_pull_requests(repo=repo, since=since, limit=1000)
165
+ issues = self.db.get_issues(repo=repo, since=since, limit=1000)
166
+
167
+ # Team health
168
+ health = self.engine.team_health(days=30, repo=repo)
169
+ insights = self.engine.generate_insights(days=30, repo=repo)
170
+
171
+ author_counts: dict[str, int] = {}
172
+ for c in commits:
173
+ a = c.get("author", "unknown")
174
+ author_counts[a] = author_counts.get(a, 0) + 1
175
+ top = sorted(author_counts.items(), key=lambda x: x[1], reverse=True)[:10]
176
+
177
+ return {
178
+ "period": f"{year}-{month:02d}",
179
+ "total_commits": len(commits),
180
+ "total_prs": len(prs),
181
+ "total_issues_closed": len([i for i in issues if i.get("state") == "closed"]),
182
+ "team_health_score": health.overall_score,
183
+ "top_contributors": [{"author": a, "commits": c} for a, c in top],
184
+ "insights": [i["message"] for i in insights],
185
+ "recommendations": [i["recommendation"] for i in insights],
186
+ "velocity_trend": health.velocity_trend,
187
+ "burnout_risks": health.burnout_risk,
188
+ }
189
+
190
+ # ── Output Formats ───────────────────────────────────────────────
191
+
192
+ def to_markdown(self, data: dict[str, Any], report_type: str = "daily") -> str:
193
+ """Convert report data to Markdown."""
194
+ lines: list[str] = []
195
+
196
+ if report_type == "daily":
197
+ lines.append(f"# Daily Report — {data['date']}")
198
+ lines.append("")
199
+ lines.append(f"**Author**: {data['author']}")
200
+ lines.append(f"**Summary**: {data['summary']}")
201
+ lines.append("")
202
+ lines.append("## Metrics")
203
+ lines.append("")
204
+ lines.append(f"| Metric | Value |")
205
+ lines.append(f"|--------|-------|")
206
+ lines.append(f"| Commits | {data['commits']} |")
207
+ lines.append(f"| PRs Opened | {data['prs_opened']} |")
208
+ lines.append(f"| PRs Merged | {data['prs_merged']} |")
209
+ lines.append(f"| Issues Opened | {data['issues_opened']} |")
210
+ lines.append(f"| Issues Closed | {data['issues_closed']} |")
211
+ lines.append(f"| Reviews Given | {data['reviews_given']} |")
212
+ lines.append(f"| Lines Changed | {data['lines_changed']} |")
213
+ lines.append("")
214
+ if data.get("commit_details"):
215
+ lines.append("## Recent Commits")
216
+ lines.append("")
217
+ for c in data["commit_details"]:
218
+ lines.append(f"- `[{c['sha']}]` {c['message']} ({c['repo']})")
219
+ lines.append("")
220
+
221
+ elif report_type == "weekly":
222
+ lines.append(f"# Weekly Report — {data['week_start']} to {data['week_end']}")
223
+ lines.append("")
224
+ lines.append("## Overview")
225
+ lines.append("")
226
+ lines.append(f"- **Total Commits**: {data['total_commits']}")
227
+ lines.append(f"- **Total PRs**: {data['total_prs']}")
228
+ lines.append(f"- **Issues Closed**: {data['total_issues_closed']}")
229
+ lines.append(f"- **Avg Merge Time**: {data['avg_merge_time_hours']}h")
230
+ lines.append("")
231
+ lines.append("## Top Contributors")
232
+ lines.append("")
233
+ for tc in data["top_contributors"]:
234
+ lines.append(f"- **{tc['author']}**: {tc['commits']} commits")
235
+ lines.append("")
236
+ if data.get("insights"):
237
+ lines.append("## AI Insights")
238
+ lines.append("")
239
+ for ins in data["insights"]:
240
+ lines.append(f"- {ins}")
241
+ lines.append("")
242
+ if data.get("recommendations"):
243
+ lines.append("## Recommendations")
244
+ lines.append("")
245
+ for rec in data["recommendations"]:
246
+ lines.append(f"- {rec}")
247
+ lines.append("")
248
+
249
+ elif report_type == "monthly":
250
+ lines.append(f"# Monthly Report — {data['period']}")
251
+ lines.append("")
252
+ lines.append("## Overview")
253
+ lines.append("")
254
+ lines.append(f"- **Total Commits**: {data['total_commits']}")
255
+ lines.append(f"- **Total PRs**: {data['total_prs']}")
256
+ lines.append(f"- **Issues Closed**: {data['total_issues_closed']}")
257
+ lines.append(f"- **Team Health Score**: {data['team_health_score']}/100")
258
+ lines.append(f"- **Velocity Trend**: {data['velocity_trend']}")
259
+ lines.append("")
260
+ lines.append("## Top Contributors")
261
+ lines.append("")
262
+ for tc in data["top_contributors"]:
263
+ lines.append(f"- **{tc['author']}**: {tc['commits']} commits")
264
+ lines.append("")
265
+ if data.get("burnout_risks"):
266
+ lines.append("## Burnout Risk")
267
+ lines.append("")
268
+ for name, risk in data["burnout_risks"].items():
269
+ emoji = ":red_circle:" if risk >= 0.6 else ":yellow_circle:" if risk >= 0.3 else ":green_circle:"
270
+ lines.append(f"- {emoji} **{name}**: {risk:.0%}")
271
+ lines.append("")
272
+ if data.get("insights"):
273
+ lines.append("## AI Insights")
274
+ lines.append("")
275
+ for ins in data["insights"]:
276
+ lines.append(f"- {ins}")
277
+ lines.append("")
278
+
279
+ return "\n".join(lines)
280
+
281
+ def to_json(self, data: dict[str, Any]) -> str:
282
+ """Convert report data to JSON."""
283
+ return json.dumps(data, indent=2, default=str)
284
+
285
+ def to_html(self, data: dict[str, Any], report_type: str = "daily") -> str:
286
+ """Convert report data to a standalone HTML page."""
287
+ md_content = self.to_markdown(data, report_type)
288
+ # Simple markdown-to-HTML conversion for key elements
289
+ html_body = _md_to_html(md_content)
290
+
291
+ html = f"""<!DOCTYPE html>
292
+ <html lang="en">
293
+ <head>
294
+ <meta charset="UTF-8">
295
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
296
+ <title>DevPulse Report</title>
297
+ <style>
298
+ :root {{
299
+ --bg: #0d1117;
300
+ --card: #161b22;
301
+ --border: #30363d;
302
+ --text: #c9d1d9;
303
+ --heading: #f0f6fc;
304
+ --accent: #58a6ff;
305
+ --green: #3fb950;
306
+ --yellow: #d29922;
307
+ --red: #f85149;
308
+ }}
309
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
310
+ body {{
311
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
312
+ background: var(--bg);
313
+ color: var(--text);
314
+ line-height: 1.6;
315
+ padding: 2rem;
316
+ max-width: 960px;
317
+ margin: 0 auto;
318
+ }}
319
+ h1 {{ color: var(--heading); margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }}
320
+ h2 {{ color: var(--accent); margin-top: 1.5rem; margin-bottom: 0.5rem; }}
321
+ table {{ border-collapse: collapse; width: 100%; margin: 1rem 0; }}
322
+ th, td {{ border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }}
323
+ th {{ background: var(--card); color: var(--heading); }}
324
+ td {{ background: var(--card); }}
325
+ ul {{ padding-left: 1.5rem; }}
326
+ li {{ margin: 0.25rem 0; }}
327
+ strong {{ color: var(--heading); }}
328
+ code {{ background: var(--card); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.9em; }}
329
+ .header {{ text-align: center; margin-bottom: 2rem; }}
330
+ .header p {{ color: var(--accent); }}
331
+ .metric-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 1rem 0; }}
332
+ .metric-card {{ background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; text-align: center; }}
333
+ .metric-card .value {{ font-size: 2rem; font-weight: bold; color: var(--heading); }}
334
+ .metric-card .label {{ font-size: 0.85rem; color: var(--text); }}
335
+ .badge {{ display: inline-block; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.8rem; font-weight: 600; }}
336
+ .badge-green {{ background: #1a3a2a; color: var(--green); }}
337
+ .badge-yellow {{ background: #3a2a1a; color: var(--yellow); }}
338
+ .badge-red {{ background: #3a1a1a; color: var(--red); }}
339
+ </style>
340
+ </head>
341
+ <body>
342
+ <div class="header">
343
+ <h1>DevPulse Report</h1>
344
+ <p>AI-Powered Developer Productivity Dashboard</p>
345
+ </div>
346
+ {html_body}
347
+ </body>
348
+ </html>"""
349
+ return html
350
+
351
+ # ── Save to File ─────────────────────────────────────────────────
352
+
353
+ def save_report(
354
+ self,
355
+ data: dict[str, Any],
356
+ report_type: str,
357
+ fmt: str = "markdown",
358
+ ) -> str:
359
+ """Save report to file and return the path."""
360
+ period = data.get("date", data.get("week_start", data.get("period", "unknown")))
361
+
362
+ if fmt == "json":
363
+ content = self.to_json(data)
364
+ ext = "json"
365
+ elif fmt == "html":
366
+ content = self.to_html(data, report_type)
367
+ ext = "html"
368
+ else:
369
+ content = self.to_markdown(data, report_type)
370
+ ext = "md"
371
+
372
+ filename = f"{report_type}_report_{period}.{ext}"
373
+ filepath = os.path.join(self.reports_dir, filename)
374
+
375
+ with open(filepath, "w", encoding="utf-8") as f:
376
+ f.write(content)
377
+
378
+ # Cache in database
379
+ self.db.save_report(report_type, period, content)
380
+
381
+ return filepath
382
+
383
+
384
+ def _md_to_html(md: str) -> str:
385
+ """Minimal Markdown to HTML converter."""
386
+ lines = md.split("\n")
387
+ html_parts: list[str] = []
388
+ in_list = False
389
+ in_table = False
390
+
391
+ for line in lines:
392
+ stripped = line.strip()
393
+
394
+ if stripped.startswith("# "):
395
+ if in_list:
396
+ html_parts.append("</ul>")
397
+ in_list = False
398
+ html_parts.append(f"<h1>{stripped[2:]}</h1>")
399
+ elif stripped.startswith("## "):
400
+ if in_list:
401
+ html_parts.append("</ul>")
402
+ in_list = False
403
+ html_parts.append(f"<h2>{stripped[3:]}</h2>")
404
+ elif stripped.startswith("| ") and "---" not in stripped:
405
+ if not in_table:
406
+ html_parts.append("<table>")
407
+ in_table = True
408
+ cells = [c.strip() for c in stripped.split("|")[1:-1]]
409
+ tag = "th" if not html_parts or not any("<td>" in p for p in html_parts[-5:]) else "td"
410
+ row = "".join(f"<{tag}>{c}</{tag}>" for c in cells)
411
+ html_parts.append(f"<tr>{row}</tr>")
412
+ elif stripped.startswith("- "):
413
+ if in_table:
414
+ html_parts.append("</table>")
415
+ in_table = False
416
+ if not in_list:
417
+ html_parts.append("<ul>")
418
+ in_list = True
419
+ content = stripped[2:]
420
+ # Bold
421
+ content = content.replace("**", "<strong>", 1).replace("**", "</strong>", 1)
422
+ # Code
423
+ if "`" in content:
424
+ parts = content.split("`")
425
+ content = parts[0] + "".join(
426
+ f"<code>{parts[i]}</code>" + parts[i + 1] if i + 1 < len(parts) else ""
427
+ for i in range(1, len(parts), 2)
428
+ )
429
+ html_parts.append(f"<li>{content}</li>")
430
+ elif stripped == "":
431
+ if in_list:
432
+ html_parts.append("</ul>")
433
+ in_list = False
434
+ if in_table:
435
+ html_parts.append("</table>")
436
+ in_table = False
437
+ html_parts.append("<br>")
438
+ else:
439
+ if in_list:
440
+ html_parts.append("</ul>")
441
+ in_list = False
442
+ if in_table:
443
+ html_parts.append("</table>")
444
+ in_table = False
445
+ content = stripped
446
+ content = content.replace("**", "<strong>", 1).replace("**", "</strong>", 1)
447
+ html_parts.append(f"<p>{content}</p>")
448
+
449
+ if in_list:
450
+ html_parts.append("</ul>")
451
+ if in_table:
452
+ html_parts.append("</table>")
453
+
454
+ return "\n".join(html_parts)
@@ -0,0 +1 @@
1
+ /* DevPulse static assets */
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DevPulse Report</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117;
10
+ --card: #161b22;
11
+ --border: #30363d;
12
+ --text: #c9d1d9;
13
+ --heading: #f0f6fc;
14
+ --accent: #58a6ff;
15
+ --green: #3fb950;
16
+ --yellow: #d29922;
17
+ --red: #f85149;
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ line-height: 1.6;
25
+ padding: 2rem;
26
+ max-width: 960px;
27
+ margin: 0 auto;
28
+ }
29
+ h1 { color: var(--heading); margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
30
+ h2 { color: var(--accent); margin-top: 1.5rem; margin-bottom: 0.5rem; }
31
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
32
+ th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
33
+ th { background: var(--card); color: var(--heading); }
34
+ td { background: var(--card); }
35
+ ul { padding-left: 1.5rem; }
36
+ li { margin: 0.25rem 0; }
37
+ strong { color: var(--heading); }
38
+ code { background: var(--card); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
39
+ .header { text-align: center; margin-bottom: 2rem; padding: 1rem; border: 1px solid var(--border); border-radius: 8px; background: var(--card); }
40
+ .header h1 { border: none; color: var(--accent); }
41
+ .metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0; }
42
+ .metric-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; text-align: center; }
43
+ .metric-card .value { font-size: 2rem; font-weight: bold; color: var(--heading); }
44
+ .metric-card .label { font-size: 0.85rem; color: var(--text); }
45
+ .badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.8rem; font-weight: 600; }
46
+ .badge-green { background: #1a3a2a; color: var(--green); }
47
+ .badge-yellow { background: #3a2a1a; color: var(--yellow); }
48
+ .badge-red { background: #3a1a1a; color: var(--red); }
49
+ .insight { background: var(--card); border-left: 3px solid var(--accent); padding: 0.75rem 1rem; margin: 0.5rem 0; border-radius: 0 4px 4px 0; }
50
+ .insight.warning { border-left-color: var(--yellow); }
51
+ .insight.danger { border-left-color: var(--red); }
52
+ .insight.success { border-left-color: var(--green); }
53
+ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); color: #484f58; font-size: 0.85rem; text-align: center; }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <div class="header">
58
+ <h1>DevPulse Report</h1>
59
+ <p>AI-Powered Developer Productivity Dashboard</p>
60
+ </div>
61
+ <div id="content"></div>
62
+ <footer>Generated by DevPulse v1.0.0</footer>
63
+ </body>
64
+ </html>
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/tests'],
5
+ testMatch: ['**/*.test.ts'],
6
+ moduleFileExtensions: ['ts', 'js', 'json'],
7
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "main": "index.js",
3
+ "types": "index.d.ts",
4
+ "scripts": {
5
+ "build": "tsc",
6
+ "test": "jest",
7
+ "lint": "eslint src --ext .ts"
8
+ },
9
+ "name": "@theihtisham/dev-pulse",
10
+ "author": "ihtisham",
11
+ "license": "MIT",
12
+ "description": "Developer productivity metrics dashboard",
13
+ "version": "1.0.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/theihtisham/dev-pulse.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/theihtisham/dev-pulse/issues"
20
+ },
21
+ "homepage": "https://github.com/theihtisham/dev-pulse#readme",
22
+ "keywords": [
23
+ "dev pulse",
24
+ "typescript",
25
+ "ai",
26
+ "developer-tools"
27
+ ],
28
+ "devDependencies": {
29
+ "typescript": "^5.3.0",
30
+ "@types/node": "^20.0.0",
31
+ "jest": "^29.7.0",
32
+ "ts-jest": "^29.1.0",
33
+ "@types/jest": "^29.5.0"
34
+ }
35
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,80 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ai-dev-pulse"
7
+ version = "1.0.0"
8
+ description = "AI-powered developer productivity dashboard with GitHub integration"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "theihtisham"},
14
+ ]
15
+ keywords = ["developer-tools", "productivity", "dashboard", "github", "metrics"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ dependencies = [
27
+ "fastapi>=0.110.0",
28
+ "uvicorn>=0.27.0",
29
+ "rich>=13.7.0",
30
+ "click>=8.1.0",
31
+ "requests>=2.31.0",
32
+ "jinja2>=3.1.3",
33
+ "schedule>=1.2.0",
34
+ "pydantic>=2.6.0",
35
+ "pydantic-settings>=2.1.0",
36
+ "python-dateutil>=2.8.2",
37
+ ]
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=8.0.0",
42
+ "pytest-cov>=4.1.0",
43
+ "pytest-asyncio>=0.23.0",
44
+ "httpx>=0.27.0",
45
+ "ruff>=0.2.0",
46
+ "mypy>=1.8.0",
47
+ ]
48
+
49
+ [project.scripts]
50
+ devpulse = "devpulse.cli.main:cli"
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/user/devpulse"
54
+ Repository = "https://github.com/user/devpulse"
55
+ Issues = "https://github.com/user/devpulse/issues"
56
+
57
+ [tool.setuptools.packages.find]
58
+ include = ["devpulse*"]
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
62
+ python_files = ["test_*.py"]
63
+ python_classes = ["Test*"]
64
+ python_functions = ["test_*"]
65
+ addopts = "-v --tb=short"
66
+ asyncio_mode = "auto"
67
+
68
+ [tool.ruff]
69
+ target-version = "py310"
70
+ line-length = 100
71
+
72
+ [tool.ruff.lint]
73
+ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"]
74
+ ignore = ["E501"]
75
+
76
+ [tool.mypy]
77
+ python_version = "3.10"
78
+ warn_return_any = true
79
+ warn_unused_configs = true
80
+ disallow_untyped_defs = true
@@ -0,0 +1,14 @@
1
+ fastapi>=0.110.0
2
+ uvicorn>=0.27.0
3
+ rich>=13.7.0
4
+ click>=8.1.0
5
+ requests>=2.31.0
6
+ jinja2>=3.1.3
7
+ schedule>=1.2.0
8
+ pydantic>=2.6.0
9
+ pydantic-settings>=2.1.0
10
+ python-dateutil>=2.8.2
11
+ pytest>=8.0.0
12
+ pytest-cov>=4.1.0
13
+ pytest-asyncio>=0.23.0
14
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ """Tests package for DevPulse."""