@theihtisham/dev-pulse 1.0.0 → 1.1.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.
Files changed (40) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  5. package/.github/dependabot.yml +16 -0
  6. package/.github/workflows/ci.yml +33 -0
  7. package/CODE_OF_CONDUCT.md +27 -0
  8. package/Dockerfile +8 -0
  9. package/LICENSE +21 -21
  10. package/README.md +135 -39
  11. package/SECURITY.md +22 -0
  12. package/devpulse/__init__.py +4 -4
  13. package/devpulse/api/__init__.py +1 -1
  14. package/devpulse/api/app.py +371 -371
  15. package/devpulse/cli/__init__.py +1 -1
  16. package/devpulse/cli/dashboard.py +131 -131
  17. package/devpulse/cli/main.py +678 -678
  18. package/devpulse/cli/render.py +175 -175
  19. package/devpulse/core/__init__.py +34 -34
  20. package/devpulse/core/analytics.py +487 -487
  21. package/devpulse/core/config.py +77 -77
  22. package/devpulse/core/database.py +612 -612
  23. package/devpulse/core/github_client.py +281 -281
  24. package/devpulse/core/models.py +142 -142
  25. package/devpulse/core/report_generator.py +454 -454
  26. package/devpulse/static/.gitkeep +1 -1
  27. package/devpulse/templates/report.html +64 -64
  28. package/package.json +35 -35
  29. package/pyproject.toml +80 -80
  30. package/requirements.txt +14 -14
  31. package/tests/__init__.py +1 -1
  32. package/tests/conftest.py +208 -208
  33. package/tests/test_analytics.py +284 -284
  34. package/tests/test_api.py +313 -313
  35. package/tests/test_cli.py +204 -204
  36. package/tests/test_config.py +47 -47
  37. package/tests/test_database.py +255 -255
  38. package/tests/test_models.py +107 -107
  39. package/tests/test_report_generator.py +173 -173
  40. package/jest.config.js +0 -7
@@ -1,107 +1,107 @@
1
- """Tests for Pydantic models."""
2
-
3
- import pytest
4
-
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
-
20
- class TestCommit:
21
- def test_create_commit(self):
22
- c = Commit(sha="abc123", repo="org/repo", author="Alice", author_date="2026-04-01T10:00:00Z")
23
- assert c.sha == "abc123"
24
- assert c.additions == 0
25
-
26
- def test_commit_defaults(self):
27
- c = Commit(sha="abc", repo="r", author="a", author_date="d")
28
- assert c.message == ""
29
- assert c.url == ""
30
-
31
-
32
- class TestPullRequest:
33
- def test_create_pr(self):
34
- pr = PullRequest(number=1, repo="org/repo")
35
- assert pr.state == "open"
36
- assert pr.merged_at is None
37
-
38
- def test_pr_with_merge(self):
39
- pr = PullRequest(number=1, repo="org/repo", state="merged", merged_at="2026-04-02")
40
- assert pr.state == "merged"
41
-
42
-
43
- class TestIssue:
44
- def test_create_issue(self):
45
- issue = Issue(number=10, repo="org/repo")
46
- assert issue.labels == []
47
- assert issue.state == "open"
48
-
49
-
50
- class TestDeveloperMetrics:
51
- def test_create_metrics(self):
52
- m = DeveloperMetrics(author="Alice")
53
- assert m.commits_count == 0
54
- assert m.avg_pr_merge_time_hours == 0.0
55
-
56
- def test_metrics_serialization(self):
57
- m = DeveloperMetrics(author="Bob", commits_count=5, active_days=3)
58
- d = m.model_dump()
59
- assert d["author"] == "Bob"
60
- assert d["commits_count"] == 5
61
-
62
-
63
- class TestTeamHealth:
64
- def test_create_team_health(self):
65
- th = TeamHealth()
66
- assert th.overall_score == 0.0
67
- assert th.recommendations == []
68
-
69
- def test_team_health_with_burnout(self):
70
- th = TeamHealth(
71
- overall_score=75.5,
72
- burnout_risk={"Alice": 0.3, "Bob": 0.7},
73
- velocity_trend="stable",
74
- )
75
- assert th.burnout_risk["Bob"] == 0.7
76
-
77
-
78
- class TestGoal:
79
- def test_create_goal(self):
80
- g = Goal(title="Test", target_value=10, metric="commits")
81
- assert g.id is None
82
- assert g.status == "active"
83
-
84
- def test_goal_with_id(self):
85
- g = Goal(id=1, title="Test", target_value=10, metric="commits")
86
- assert g.id == 1
87
-
88
-
89
- class TestSprintData:
90
- def test_create_sprint(self):
91
- s = SprintData(name="Sprint 1")
92
- assert s.total_points == 0
93
- assert s.scope_creep_pct == 0
94
-
95
-
96
- class TestDailyReport:
97
- def test_create_daily_report(self):
98
- r = DailyReport(date="2026-04-01", author="Alice")
99
- assert r.commits == 0
100
- assert r.summary == ""
101
-
102
-
103
- class TestWeeklyReport:
104
- def test_create_weekly_report(self):
105
- r = WeeklyReport(week_start="2026-04-01", week_end="2026-04-07")
106
- assert r.total_commits == 0
107
- assert r.insights == []
1
+ """Tests for Pydantic models."""
2
+
3
+ import pytest
4
+
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
+
20
+ class TestCommit:
21
+ def test_create_commit(self):
22
+ c = Commit(sha="abc123", repo="org/repo", author="Alice", author_date="2026-04-01T10:00:00Z")
23
+ assert c.sha == "abc123"
24
+ assert c.additions == 0
25
+
26
+ def test_commit_defaults(self):
27
+ c = Commit(sha="abc", repo="r", author="a", author_date="d")
28
+ assert c.message == ""
29
+ assert c.url == ""
30
+
31
+
32
+ class TestPullRequest:
33
+ def test_create_pr(self):
34
+ pr = PullRequest(number=1, repo="org/repo")
35
+ assert pr.state == "open"
36
+ assert pr.merged_at is None
37
+
38
+ def test_pr_with_merge(self):
39
+ pr = PullRequest(number=1, repo="org/repo", state="merged", merged_at="2026-04-02")
40
+ assert pr.state == "merged"
41
+
42
+
43
+ class TestIssue:
44
+ def test_create_issue(self):
45
+ issue = Issue(number=10, repo="org/repo")
46
+ assert issue.labels == []
47
+ assert issue.state == "open"
48
+
49
+
50
+ class TestDeveloperMetrics:
51
+ def test_create_metrics(self):
52
+ m = DeveloperMetrics(author="Alice")
53
+ assert m.commits_count == 0
54
+ assert m.avg_pr_merge_time_hours == 0.0
55
+
56
+ def test_metrics_serialization(self):
57
+ m = DeveloperMetrics(author="Bob", commits_count=5, active_days=3)
58
+ d = m.model_dump()
59
+ assert d["author"] == "Bob"
60
+ assert d["commits_count"] == 5
61
+
62
+
63
+ class TestTeamHealth:
64
+ def test_create_team_health(self):
65
+ th = TeamHealth()
66
+ assert th.overall_score == 0.0
67
+ assert th.recommendations == []
68
+
69
+ def test_team_health_with_burnout(self):
70
+ th = TeamHealth(
71
+ overall_score=75.5,
72
+ burnout_risk={"Alice": 0.3, "Bob": 0.7},
73
+ velocity_trend="stable",
74
+ )
75
+ assert th.burnout_risk["Bob"] == 0.7
76
+
77
+
78
+ class TestGoal:
79
+ def test_create_goal(self):
80
+ g = Goal(title="Test", target_value=10, metric="commits")
81
+ assert g.id is None
82
+ assert g.status == "active"
83
+
84
+ def test_goal_with_id(self):
85
+ g = Goal(id=1, title="Test", target_value=10, metric="commits")
86
+ assert g.id == 1
87
+
88
+
89
+ class TestSprintData:
90
+ def test_create_sprint(self):
91
+ s = SprintData(name="Sprint 1")
92
+ assert s.total_points == 0
93
+ assert s.scope_creep_pct == 0
94
+
95
+
96
+ class TestDailyReport:
97
+ def test_create_daily_report(self):
98
+ r = DailyReport(date="2026-04-01", author="Alice")
99
+ assert r.commits == 0
100
+ assert r.summary == ""
101
+
102
+
103
+ class TestWeeklyReport:
104
+ def test_create_weekly_report(self):
105
+ r = WeeklyReport(week_start="2026-04-01", week_end="2026-04-07")
106
+ assert r.total_commits == 0
107
+ assert r.insights == []
@@ -1,173 +1,173 @@
1
- """Tests for the ReportGenerator."""
2
-
3
- import json
4
- import os
5
-
6
- import pytest
7
-
8
- from devpulse.core.report_generator import ReportGenerator, _md_to_html
9
-
10
-
11
- class TestDailyReport:
12
- """Test daily report generation."""
13
-
14
- def test_daily_report_basic(self, populated_db):
15
- gen = ReportGenerator(db=populated_db)
16
- report = gen.daily_report(target_date="2026-04-01")
17
-
18
- assert report["date"] == "2026-04-01"
19
- assert report["commits"] == 2 # Alice's 2 commits on April 1
20
- assert report["prs_opened"] >= 0
21
- assert "summary" in report
22
-
23
- def test_daily_report_with_author(self, populated_db):
24
- gen = ReportGenerator(db=populated_db)
25
- report = gen.daily_report(target_date="2026-04-01", author="Alice")
26
-
27
- assert report["author"] == "Alice"
28
- assert report["commits"] == 2
29
-
30
- def test_daily_report_no_activity(self, populated_db):
31
- gen = ReportGenerator(db=populated_db)
32
- report = gen.daily_report(target_date="2020-01-01")
33
-
34
- assert report["commits"] == 0
35
- assert report["summary"] # Should still have a summary
36
-
37
-
38
- class TestWeeklyReport:
39
- """Test weekly report generation."""
40
-
41
- def test_weekly_report_basic(self, populated_db):
42
- gen = ReportGenerator(db=populated_db)
43
- report = gen.weekly_report(week_start="2026-03-30")
44
-
45
- assert report["week_start"] == "2026-03-30"
46
- assert report["week_end"] == "2026-04-05"
47
- assert report["total_commits"] >= 0
48
- assert isinstance(report["top_contributors"], list)
49
-
50
- def test_weekly_report_has_insights(self, populated_db):
51
- gen = ReportGenerator(db=populated_db)
52
- report = gen.weekly_report(week_start="2026-03-30")
53
-
54
- assert isinstance(report["insights"], list)
55
- assert isinstance(report["recommendations"], list)
56
-
57
-
58
- class TestMonthlyReport:
59
- """Test monthly report generation."""
60
-
61
- def test_monthly_report_basic(self, populated_db):
62
- gen = ReportGenerator(db=populated_db)
63
- report = gen.monthly_report(year=2026, month=4)
64
-
65
- assert report["period"] == "2026-04"
66
- assert report["total_commits"] >= 0
67
- assert "team_health_score" in report
68
- assert "velocity_trend" in report
69
-
70
- def test_monthly_report_default_period(self, populated_db):
71
- gen = ReportGenerator(db=populated_db)
72
- report = gen.monthly_report()
73
-
74
- assert len(report["period"]) == 7 # YYYY-MM format
75
-
76
-
77
- class TestOutputFormats:
78
- """Test report output format conversions."""
79
-
80
- def test_to_markdown_daily(self, populated_db):
81
- gen = ReportGenerator(db=populated_db)
82
- data = gen.daily_report(target_date="2026-04-01")
83
- md = gen.to_markdown(data, "daily")
84
-
85
- assert "# Daily Report" in md
86
- assert "2026-04-01" in md
87
- assert "## Metrics" in md
88
-
89
- def test_to_markdown_weekly(self, populated_db):
90
- gen = ReportGenerator(db=populated_db)
91
- data = gen.weekly_report(week_start="2026-03-30")
92
- md = gen.to_markdown(data, "weekly")
93
-
94
- assert "# Weekly Report" in md
95
- assert "Top Contributors" in md
96
-
97
- def test_to_markdown_monthly(self, populated_db):
98
- gen = ReportGenerator(db=populated_db)
99
- data = gen.monthly_report(year=2026, month=4)
100
- md = gen.to_markdown(data, "monthly")
101
-
102
- assert "# Monthly Report" in md
103
- assert "Team Health Score" in md
104
-
105
- def test_to_json(self, populated_db):
106
- gen = ReportGenerator(db=populated_db)
107
- data = gen.daily_report(target_date="2026-04-01")
108
- j = gen.to_json(data)
109
-
110
- parsed = json.loads(j)
111
- assert parsed["date"] == "2026-04-01"
112
-
113
- def test_to_html(self, populated_db):
114
- gen = ReportGenerator(db=populated_db)
115
- data = gen.daily_report(target_date="2026-04-01")
116
- html = gen.to_html(data, "daily")
117
-
118
- assert "<!DOCTYPE html>" in html
119
- assert "DevPulse" in html
120
- assert "<table>" in html or "<h1>" in html
121
-
122
-
123
- class TestMdToHtml:
124
- """Test the markdown-to-HTML converter."""
125
-
126
- def test_headers(self):
127
- html = _md_to_html("# Title\n## Subtitle")
128
- assert "<h1>Title</h1>" in html
129
- assert "<h2>Subtitle</h2>" in html
130
-
131
- def test_list_items(self):
132
- html = _md_to_html("- Item 1\n- Item 2")
133
- assert "<ul>" in html
134
- assert "<li>" in html
135
- assert "Item 1" in html
136
-
137
- def test_bold_text(self):
138
- html = _md_to_html("- **Bold** text")
139
- assert "<strong>Bold</strong>" in html
140
-
141
-
142
- class TestSaveReport:
143
- """Test report file saving."""
144
-
145
- def test_save_markdown_report(self, populated_db):
146
- gen = ReportGenerator(db=populated_db)
147
- data = gen.daily_report(target_date="2026-04-01")
148
- path = gen.save_report(data, "daily", "markdown")
149
-
150
- assert os.path.exists(path)
151
- assert path.endswith(".md")
152
- with open(path) as f:
153
- content = f.read()
154
- assert "Daily Report" in content
155
-
156
- def test_save_json_report(self, populated_db):
157
- gen = ReportGenerator(db=populated_db)
158
- data = gen.daily_report(target_date="2026-04-01")
159
- path = gen.save_report(data, "daily", "json")
160
-
161
- assert os.path.exists(path)
162
- assert path.endswith(".json")
163
-
164
- def test_save_html_report(self, populated_db):
165
- gen = ReportGenerator(db=populated_db)
166
- data = gen.daily_report(target_date="2026-04-01")
167
- path = gen.save_report(data, "daily", "html")
168
-
169
- assert os.path.exists(path)
170
- assert path.endswith(".html")
171
- with open(path) as f:
172
- content = f.read()
173
- assert "<!DOCTYPE html>" in content
1
+ """Tests for the ReportGenerator."""
2
+
3
+ import json
4
+ import os
5
+
6
+ import pytest
7
+
8
+ from devpulse.core.report_generator import ReportGenerator, _md_to_html
9
+
10
+
11
+ class TestDailyReport:
12
+ """Test daily report generation."""
13
+
14
+ def test_daily_report_basic(self, populated_db):
15
+ gen = ReportGenerator(db=populated_db)
16
+ report = gen.daily_report(target_date="2026-04-01")
17
+
18
+ assert report["date"] == "2026-04-01"
19
+ assert report["commits"] == 2 # Alice's 2 commits on April 1
20
+ assert report["prs_opened"] >= 0
21
+ assert "summary" in report
22
+
23
+ def test_daily_report_with_author(self, populated_db):
24
+ gen = ReportGenerator(db=populated_db)
25
+ report = gen.daily_report(target_date="2026-04-01", author="Alice")
26
+
27
+ assert report["author"] == "Alice"
28
+ assert report["commits"] == 2
29
+
30
+ def test_daily_report_no_activity(self, populated_db):
31
+ gen = ReportGenerator(db=populated_db)
32
+ report = gen.daily_report(target_date="2020-01-01")
33
+
34
+ assert report["commits"] == 0
35
+ assert report["summary"] # Should still have a summary
36
+
37
+
38
+ class TestWeeklyReport:
39
+ """Test weekly report generation."""
40
+
41
+ def test_weekly_report_basic(self, populated_db):
42
+ gen = ReportGenerator(db=populated_db)
43
+ report = gen.weekly_report(week_start="2026-03-30")
44
+
45
+ assert report["week_start"] == "2026-03-30"
46
+ assert report["week_end"] == "2026-04-05"
47
+ assert report["total_commits"] >= 0
48
+ assert isinstance(report["top_contributors"], list)
49
+
50
+ def test_weekly_report_has_insights(self, populated_db):
51
+ gen = ReportGenerator(db=populated_db)
52
+ report = gen.weekly_report(week_start="2026-03-30")
53
+
54
+ assert isinstance(report["insights"], list)
55
+ assert isinstance(report["recommendations"], list)
56
+
57
+
58
+ class TestMonthlyReport:
59
+ """Test monthly report generation."""
60
+
61
+ def test_monthly_report_basic(self, populated_db):
62
+ gen = ReportGenerator(db=populated_db)
63
+ report = gen.monthly_report(year=2026, month=4)
64
+
65
+ assert report["period"] == "2026-04"
66
+ assert report["total_commits"] >= 0
67
+ assert "team_health_score" in report
68
+ assert "velocity_trend" in report
69
+
70
+ def test_monthly_report_default_period(self, populated_db):
71
+ gen = ReportGenerator(db=populated_db)
72
+ report = gen.monthly_report()
73
+
74
+ assert len(report["period"]) == 7 # YYYY-MM format
75
+
76
+
77
+ class TestOutputFormats:
78
+ """Test report output format conversions."""
79
+
80
+ def test_to_markdown_daily(self, populated_db):
81
+ gen = ReportGenerator(db=populated_db)
82
+ data = gen.daily_report(target_date="2026-04-01")
83
+ md = gen.to_markdown(data, "daily")
84
+
85
+ assert "# Daily Report" in md
86
+ assert "2026-04-01" in md
87
+ assert "## Metrics" in md
88
+
89
+ def test_to_markdown_weekly(self, populated_db):
90
+ gen = ReportGenerator(db=populated_db)
91
+ data = gen.weekly_report(week_start="2026-03-30")
92
+ md = gen.to_markdown(data, "weekly")
93
+
94
+ assert "# Weekly Report" in md
95
+ assert "Top Contributors" in md
96
+
97
+ def test_to_markdown_monthly(self, populated_db):
98
+ gen = ReportGenerator(db=populated_db)
99
+ data = gen.monthly_report(year=2026, month=4)
100
+ md = gen.to_markdown(data, "monthly")
101
+
102
+ assert "# Monthly Report" in md
103
+ assert "Team Health Score" in md
104
+
105
+ def test_to_json(self, populated_db):
106
+ gen = ReportGenerator(db=populated_db)
107
+ data = gen.daily_report(target_date="2026-04-01")
108
+ j = gen.to_json(data)
109
+
110
+ parsed = json.loads(j)
111
+ assert parsed["date"] == "2026-04-01"
112
+
113
+ def test_to_html(self, populated_db):
114
+ gen = ReportGenerator(db=populated_db)
115
+ data = gen.daily_report(target_date="2026-04-01")
116
+ html = gen.to_html(data, "daily")
117
+
118
+ assert "<!DOCTYPE html>" in html
119
+ assert "DevPulse" in html
120
+ assert "<table>" in html or "<h1>" in html
121
+
122
+
123
+ class TestMdToHtml:
124
+ """Test the markdown-to-HTML converter."""
125
+
126
+ def test_headers(self):
127
+ html = _md_to_html("# Title\n## Subtitle")
128
+ assert "<h1>Title</h1>" in html
129
+ assert "<h2>Subtitle</h2>" in html
130
+
131
+ def test_list_items(self):
132
+ html = _md_to_html("- Item 1\n- Item 2")
133
+ assert "<ul>" in html
134
+ assert "<li>" in html
135
+ assert "Item 1" in html
136
+
137
+ def test_bold_text(self):
138
+ html = _md_to_html("- **Bold** text")
139
+ assert "<strong>Bold</strong>" in html
140
+
141
+
142
+ class TestSaveReport:
143
+ """Test report file saving."""
144
+
145
+ def test_save_markdown_report(self, populated_db):
146
+ gen = ReportGenerator(db=populated_db)
147
+ data = gen.daily_report(target_date="2026-04-01")
148
+ path = gen.save_report(data, "daily", "markdown")
149
+
150
+ assert os.path.exists(path)
151
+ assert path.endswith(".md")
152
+ with open(path) as f:
153
+ content = f.read()
154
+ assert "Daily Report" in content
155
+
156
+ def test_save_json_report(self, populated_db):
157
+ gen = ReportGenerator(db=populated_db)
158
+ data = gen.daily_report(target_date="2026-04-01")
159
+ path = gen.save_report(data, "daily", "json")
160
+
161
+ assert os.path.exists(path)
162
+ assert path.endswith(".json")
163
+
164
+ def test_save_html_report(self, populated_db):
165
+ gen = ReportGenerator(db=populated_db)
166
+ data = gen.daily_report(target_date="2026-04-01")
167
+ path = gen.save_report(data, "daily", "html")
168
+
169
+ assert os.path.exists(path)
170
+ assert path.endswith(".html")
171
+ with open(path) as f:
172
+ content = f.read()
173
+ assert "<!DOCTYPE html>" in content
package/jest.config.js DELETED
@@ -1,7 +0,0 @@
1
- module.exports = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- roots: ['<rootDir>/tests'],
5
- testMatch: ['**/*.test.ts'],
6
- moduleFileExtensions: ['ts', 'js', 'json'],
7
- };