ghscope 0.1.0__tar.gz

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 (45) hide show
  1. ghscope-0.1.0/PKG-INFO +186 -0
  2. ghscope-0.1.0/README.md +166 -0
  3. ghscope-0.1.0/ghscope/__init__.py +10 -0
  4. ghscope-0.1.0/ghscope/analytics/__init__.py +1 -0
  5. ghscope-0.1.0/ghscope/analytics/activity.py +39 -0
  6. ghscope-0.1.0/ghscope/analytics/aggregates.py +185 -0
  7. ghscope-0.1.0/ghscope/analytics/timelines.py +60 -0
  8. ghscope-0.1.0/ghscope/auth/__init__.py +1 -0
  9. ghscope-0.1.0/ghscope/auth/app.py +25 -0
  10. ghscope-0.1.0/ghscope/auth/token.py +38 -0
  11. ghscope-0.1.0/ghscope/cli.py +128 -0
  12. ghscope-0.1.0/ghscope/client/__init__.py +1 -0
  13. ghscope-0.1.0/ghscope/client/graphql.py +91 -0
  14. ghscope-0.1.0/ghscope/client/rest.py +63 -0
  15. ghscope-0.1.0/ghscope/collectors/__init__.py +1 -0
  16. ghscope-0.1.0/ghscope/collectors/commits.py +140 -0
  17. ghscope-0.1.0/ghscope/collectors/issues.py +143 -0
  18. ghscope-0.1.0/ghscope/collectors/pull_requests.py +169 -0
  19. ghscope-0.1.0/ghscope/collectors/repos.py +153 -0
  20. ghscope-0.1.0/ghscope/collectors/users.py +107 -0
  21. ghscope-0.1.0/ghscope/exporters/__init__.py +1 -0
  22. ghscope-0.1.0/ghscope/exporters/csv.py +49 -0
  23. ghscope-0.1.0/ghscope/exporters/html.py +35 -0
  24. ghscope-0.1.0/ghscope/exporters/json.py +57 -0
  25. ghscope-0.1.0/ghscope/exporters/markdown.py +35 -0
  26. ghscope-0.1.0/ghscope/exporters/pdf.py +18 -0
  27. ghscope-0.1.0/ghscope/models/__init__.py +9 -0
  28. ghscope-0.1.0/ghscope/models/commit.py +19 -0
  29. ghscope-0.1.0/ghscope/models/issue.py +19 -0
  30. ghscope-0.1.0/ghscope/models/pull_request.py +25 -0
  31. ghscope-0.1.0/ghscope/models/repo.py +18 -0
  32. ghscope-0.1.0/ghscope/models/user.py +16 -0
  33. ghscope-0.1.0/ghscope/report/builder.py +224 -0
  34. ghscope-0.1.0/ghscope/report/context.py +40 -0
  35. ghscope-0.1.0/ghscope.egg-info/PKG-INFO +186 -0
  36. ghscope-0.1.0/ghscope.egg-info/SOURCES.txt +43 -0
  37. ghscope-0.1.0/ghscope.egg-info/dependency_links.txt +1 -0
  38. ghscope-0.1.0/ghscope.egg-info/entry_points.txt +2 -0
  39. ghscope-0.1.0/ghscope.egg-info/requires.txt +13 -0
  40. ghscope-0.1.0/ghscope.egg-info/top_level.txt +1 -0
  41. ghscope-0.1.0/pyproject.toml +40 -0
  42. ghscope-0.1.0/setup.cfg +4 -0
  43. ghscope-0.1.0/tests/test_analytics.py +167 -0
  44. ghscope-0.1.0/tests/test_cli_smoke.py +12 -0
  45. ghscope-0.1.0/tests/test_models.py +93 -0
ghscope-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghscope
3
+ Version: 0.1.0
4
+ Summary: Extensive GitHub activity and contribution reporting
5
+ Author-email: Fuad Alizada <f.alizada@alievsspace.com>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pydantic>=2.7.0
10
+ Requires-Dist: jinja2>=3.1.0
11
+ Requires-Dist: click>=8.1.7
12
+ Requires-Dist: python-dateutil>=2.9.0
13
+ Requires-Dist: weasyprint>=62.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
16
+ Requires-Dist: mypy>=1.11.0; extra == "dev"
17
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
18
+ Requires-Dist: respx>=0.21.1; extra == "dev"
19
+ Requires-Dist: types-python-dateutil>=2.9.0; extra == "dev"
20
+
21
+ ## ghscope
22
+
23
+ `ghscope` generates auditable activity reports for GitHub organisations and users.
24
+
25
+ It pulls commits, pull requests, issues and reviews through the GitHub GraphQL API, then aggregates them into human‑readable reports in Markdown, HTML, PDF, JSON and CSV.
26
+
27
+ The HTML export is a self‑contained dashboard with a sidebar, filters and per‑entity breakdowns; PDFs are generated from the same template for a print‑ready view.
28
+
29
+ ---
30
+
31
+ ### Installation
32
+
33
+ `ghscope` targets Python 3.12+.
34
+
35
+ With `uv` (recommended):
36
+
37
+ ```bash
38
+ uv add ghscope
39
+ ```
40
+
41
+ With `pip`:
42
+
43
+ ```bash
44
+ pip install ghscope
45
+ ```
46
+
47
+ ---
48
+
49
+ ### Authentication
50
+
51
+ `ghscope` uses a GitHub fine‑grained personal access token.
52
+
53
+ The token must have:
54
+
55
+ - **Repository access** to the repos you want to analyse (ideally “All repositories” for the organisation).
56
+ - **Scopes/permissions** that allow reading:
57
+ - code
58
+ - pull requests
59
+ - issues
60
+ - organisation members (for org‑wide reports)
61
+
62
+ You can pass the token explicitly or via environment variables.
63
+
64
+ Checked in order:
65
+
66
+ 1. `--token` CLI flag
67
+ 2. `GH_FINE_GRAINED_TOKEN`
68
+ 3. `GITHUB_TOKEN`
69
+ 4. `GH_TOKEN`
70
+
71
+ ---
72
+
73
+ ### CLI usage
74
+
75
+ Once installed, a `ghscope` entrypoint is available on your PATH.
76
+
77
+ Basic help:
78
+
79
+ ```bash
80
+ ghscope --help
81
+ ghscope report --help
82
+ ```
83
+
84
+ Generate a Markdown report for an organisation over a date range:
85
+
86
+ ```bash
87
+ ghscope report \
88
+ --org Alievs-corp \
89
+ --since 2026-01-01 \
90
+ --until 2026-02-28 \
91
+ --format md \
92
+ --output ./report.md
93
+ ```
94
+
95
+ Generate an HTML dashboard:
96
+
97
+ ```bash
98
+ ghscope report \
99
+ --org Alievs-corp \
100
+ --since 2026-01-01 \
101
+ --until 2026-02-28 \
102
+ --format html \
103
+ --output ./report.html
104
+ ```
105
+
106
+ Generate a JSON export suitable for further processing:
107
+
108
+ ```bash
109
+ ghscope report \
110
+ --org Alievs-corp \
111
+ --since 2026-01-01 \
112
+ --until 2026-02-28 \
113
+ --format json \
114
+ --output ./report.json
115
+ ```
116
+
117
+ Generate a PDF (requires WeasyPrint and its system dependencies):
118
+
119
+ ```bash
120
+ ghscope report \
121
+ --org Alievs-corp \
122
+ --since 2026-01-01 \
123
+ --until 2026-02-28 \
124
+ --format pdf \
125
+ --output ./report.pdf
126
+ ```
127
+
128
+ You can also run reports for a single user:
129
+
130
+ ```bash
131
+ ghscope report \
132
+ --user martian56 \
133
+ --since 2026-01-01 \
134
+ --until 2026-02-28 \
135
+ --format html \
136
+ --output ./user-report.html
137
+ ```
138
+
139
+ `--org` and `--user` can be combined; the tool will analyse all repos visible to the token in that scope.
140
+
141
+ ---
142
+
143
+ ### HTML dashboard
144
+
145
+ The HTML output is a static, portable dashboard with:
146
+
147
+ - **Overview**: repository count, commit/PR/issue totals, lines changed, active/inactive contributors.
148
+ - **Contributors**: per‑user activity, sorted by total impact.
149
+ - **Repositories**: per‑repo summaries (commits, lines, PRs, issues, distinct contributors).
150
+ - **Activity timeline**: stacked bars per day (commits, PRs, issues).
151
+ - **Pull requests, commits, issues**: detailed tables with titles/messages, authors, counts and state.
152
+
153
+ There is a small filter bar at the top:
154
+
155
+ - **Focus on contributor**: pick a user to see only their work; all sections (overview, repos, timeline, PRs, commits, issues) realign to that view.
156
+ - **Filter by text**: search across titles and messages.
157
+
158
+ The page is fully static: no build step, no external assets, and it can be served from any static host or converted to PDF.
159
+
160
+ ---
161
+
162
+ ### Data exported
163
+
164
+ Depending on the format, `ghscope` exports:
165
+
166
+ - **Aggregates**
167
+ - per‑user counts (commits, lines added/deleted, PRs opened/merged, issues opened/closed/assigned, reviews given/received)
168
+ - per‑repo counts (commits, lines, PRs, issues, contributor count)
169
+ - active vs inactive contributors
170
+ - daily activity buckets
171
+
172
+ - **Raw entities**
173
+ - users
174
+ - repositories
175
+ - commits
176
+ - pull requests
177
+ - issues
178
+
179
+ The JSON export contains both the aggregates and the raw entities so you can reshape the data as needed.
180
+
181
+ ---
182
+
183
+ ### Notes
184
+
185
+ - The reports only include repositories and members that the token can see. If you expect more data than appears in a report, check the token’s repo and organisation permissions first.
186
+ - For large organisations, keep an eye on GitHub GraphQL rate limits. `ghscope` logs remaining calls and the reset time when you run with `--verbose`.
@@ -0,0 +1,166 @@
1
+ ## ghscope
2
+
3
+ `ghscope` generates auditable activity reports for GitHub organisations and users.
4
+
5
+ It pulls commits, pull requests, issues and reviews through the GitHub GraphQL API, then aggregates them into human‑readable reports in Markdown, HTML, PDF, JSON and CSV.
6
+
7
+ The HTML export is a self‑contained dashboard with a sidebar, filters and per‑entity breakdowns; PDFs are generated from the same template for a print‑ready view.
8
+
9
+ ---
10
+
11
+ ### Installation
12
+
13
+ `ghscope` targets Python 3.12+.
14
+
15
+ With `uv` (recommended):
16
+
17
+ ```bash
18
+ uv add ghscope
19
+ ```
20
+
21
+ With `pip`:
22
+
23
+ ```bash
24
+ pip install ghscope
25
+ ```
26
+
27
+ ---
28
+
29
+ ### Authentication
30
+
31
+ `ghscope` uses a GitHub fine‑grained personal access token.
32
+
33
+ The token must have:
34
+
35
+ - **Repository access** to the repos you want to analyse (ideally “All repositories” for the organisation).
36
+ - **Scopes/permissions** that allow reading:
37
+ - code
38
+ - pull requests
39
+ - issues
40
+ - organisation members (for org‑wide reports)
41
+
42
+ You can pass the token explicitly or via environment variables.
43
+
44
+ Checked in order:
45
+
46
+ 1. `--token` CLI flag
47
+ 2. `GH_FINE_GRAINED_TOKEN`
48
+ 3. `GITHUB_TOKEN`
49
+ 4. `GH_TOKEN`
50
+
51
+ ---
52
+
53
+ ### CLI usage
54
+
55
+ Once installed, a `ghscope` entrypoint is available on your PATH.
56
+
57
+ Basic help:
58
+
59
+ ```bash
60
+ ghscope --help
61
+ ghscope report --help
62
+ ```
63
+
64
+ Generate a Markdown report for an organisation over a date range:
65
+
66
+ ```bash
67
+ ghscope report \
68
+ --org Alievs-corp \
69
+ --since 2026-01-01 \
70
+ --until 2026-02-28 \
71
+ --format md \
72
+ --output ./report.md
73
+ ```
74
+
75
+ Generate an HTML dashboard:
76
+
77
+ ```bash
78
+ ghscope report \
79
+ --org Alievs-corp \
80
+ --since 2026-01-01 \
81
+ --until 2026-02-28 \
82
+ --format html \
83
+ --output ./report.html
84
+ ```
85
+
86
+ Generate a JSON export suitable for further processing:
87
+
88
+ ```bash
89
+ ghscope report \
90
+ --org Alievs-corp \
91
+ --since 2026-01-01 \
92
+ --until 2026-02-28 \
93
+ --format json \
94
+ --output ./report.json
95
+ ```
96
+
97
+ Generate a PDF (requires WeasyPrint and its system dependencies):
98
+
99
+ ```bash
100
+ ghscope report \
101
+ --org Alievs-corp \
102
+ --since 2026-01-01 \
103
+ --until 2026-02-28 \
104
+ --format pdf \
105
+ --output ./report.pdf
106
+ ```
107
+
108
+ You can also run reports for a single user:
109
+
110
+ ```bash
111
+ ghscope report \
112
+ --user martian56 \
113
+ --since 2026-01-01 \
114
+ --until 2026-02-28 \
115
+ --format html \
116
+ --output ./user-report.html
117
+ ```
118
+
119
+ `--org` and `--user` can be combined; the tool will analyse all repos visible to the token in that scope.
120
+
121
+ ---
122
+
123
+ ### HTML dashboard
124
+
125
+ The HTML output is a static, portable dashboard with:
126
+
127
+ - **Overview**: repository count, commit/PR/issue totals, lines changed, active/inactive contributors.
128
+ - **Contributors**: per‑user activity, sorted by total impact.
129
+ - **Repositories**: per‑repo summaries (commits, lines, PRs, issues, distinct contributors).
130
+ - **Activity timeline**: stacked bars per day (commits, PRs, issues).
131
+ - **Pull requests, commits, issues**: detailed tables with titles/messages, authors, counts and state.
132
+
133
+ There is a small filter bar at the top:
134
+
135
+ - **Focus on contributor**: pick a user to see only their work; all sections (overview, repos, timeline, PRs, commits, issues) realign to that view.
136
+ - **Filter by text**: search across titles and messages.
137
+
138
+ The page is fully static: no build step, no external assets, and it can be served from any static host or converted to PDF.
139
+
140
+ ---
141
+
142
+ ### Data exported
143
+
144
+ Depending on the format, `ghscope` exports:
145
+
146
+ - **Aggregates**
147
+ - per‑user counts (commits, lines added/deleted, PRs opened/merged, issues opened/closed/assigned, reviews given/received)
148
+ - per‑repo counts (commits, lines, PRs, issues, contributor count)
149
+ - active vs inactive contributors
150
+ - daily activity buckets
151
+
152
+ - **Raw entities**
153
+ - users
154
+ - repositories
155
+ - commits
156
+ - pull requests
157
+ - issues
158
+
159
+ The JSON export contains both the aggregates and the raw entities so you can reshape the data as needed.
160
+
161
+ ---
162
+
163
+ ### Notes
164
+
165
+ - The reports only include repositories and members that the token can see. If you expect more data than appears in a report, check the token’s repo and organisation permissions first.
166
+ - For large organisations, keep an eye on GitHub GraphQL rate limits. `ghscope` logs remaining calls and the reset time when you run with `--verbose`.
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ try:
8
+ __version__ = version("ghscope")
9
+ except PackageNotFoundError:
10
+ __version__ = "0.0.0"
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Iterable
6
+
7
+ from ghscope.analytics.aggregates import ActivityReport, aggregate_activity
8
+ from ghscope.analytics.timelines import Timelines, build_timelines
9
+ from ghscope.models import Commit, Issue, PullRequest, Repository, User
10
+
11
+
12
+ @dataclass
13
+ class AnalyticsResult:
14
+ activity: ActivityReport
15
+ timelines: Timelines
16
+
17
+
18
+ def compute_analytics(
19
+ users: Iterable[User],
20
+ repositories: Iterable[Repository],
21
+ commits: Iterable[Commit],
22
+ pull_requests: Iterable[PullRequest],
23
+ issues: Iterable[Issue],
24
+ since: datetime,
25
+ until: datetime,
26
+ ) -> AnalyticsResult:
27
+ activity = aggregate_activity(
28
+ users=users,
29
+ repositories=repositories,
30
+ commits=commits,
31
+ pull_requests=pull_requests,
32
+ issues=issues,
33
+ )
34
+ timelines = build_timelines(
35
+ commits=commits,
36
+ pull_requests=pull_requests,
37
+ issues=issues,
38
+ )
39
+ return AnalyticsResult(activity=activity, timelines=timelines)
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Iterable, List, Mapping, MutableMapping
6
+
7
+ from ghscope.models import Commit, Issue, PullRequest, Repository, User
8
+
9
+
10
+ @dataclass
11
+ class UserActivitySummary:
12
+ login: str
13
+ commits: int
14
+ lines_added: int
15
+ lines_deleted: int
16
+ pull_requests_opened: int
17
+ pull_requests_merged: int
18
+ issues_opened: int
19
+ issues_closed: int
20
+ issues_assigned: int
21
+ reviews_given: int
22
+ reviews_received: int
23
+
24
+
25
+ @dataclass
26
+ class RepositoryActivitySummary:
27
+ full_name: str
28
+ commits: int
29
+ lines_added: int
30
+ lines_deleted: int
31
+ pull_requests_opened: int
32
+ pull_requests_merged: int
33
+ issues_opened: int
34
+ issues_closed: int
35
+ contributors: int
36
+
37
+
38
+ @dataclass
39
+ class ActivityReport:
40
+ users: Mapping[str, UserActivitySummary]
41
+ repositories: Mapping[str, RepositoryActivitySummary]
42
+ active_contributors: List[str]
43
+ inactive_contributors: List[str]
44
+
45
+
46
+ def aggregate_activity(
47
+ users: Iterable[User],
48
+ repositories: Iterable[Repository],
49
+ commits: Iterable[Commit],
50
+ pull_requests: Iterable[PullRequest],
51
+ issues: Iterable[Issue],
52
+ active_threshold: int = 1,
53
+ ) -> ActivityReport:
54
+ """Aggregate per-user and per-repository activity."""
55
+ user_logins = {user.login for user in users}
56
+ repo_names = {repo.full_name for repo in repositories}
57
+
58
+ user_commits: MutableMapping[str, int] = Counter()
59
+ user_lines_added: MutableMapping[str, int] = Counter()
60
+ user_lines_deleted: MutableMapping[str, int] = Counter()
61
+ repo_commits: MutableMapping[str, int] = Counter()
62
+ repo_lines_added: MutableMapping[str, int] = Counter()
63
+ repo_lines_deleted: MutableMapping[str, int] = Counter()
64
+
65
+ for commit in commits:
66
+ login = commit.author_login
67
+ if login:
68
+ user_commits[login] += 1
69
+ user_lines_added[login] += commit.additions
70
+ user_lines_deleted[login] += commit.deletions
71
+ repo = commit.repository_full_name
72
+ repo_commits[repo] += 1
73
+ repo_lines_added[repo] += commit.additions
74
+ repo_lines_deleted[repo] += commit.deletions
75
+
76
+ user_pr_opened: MutableMapping[str, int] = Counter()
77
+ user_pr_merged: MutableMapping[str, int] = Counter()
78
+ user_reviews_given: MutableMapping[str, int] = Counter()
79
+ user_reviews_received: MutableMapping[str, int] = Counter()
80
+ repo_pr_opened: MutableMapping[str, int] = Counter()
81
+ repo_pr_merged: MutableMapping[str, int] = Counter()
82
+
83
+ for pr in pull_requests:
84
+ repo = pr.repository_full_name
85
+ repo_pr_opened[repo] += 1
86
+ if pr.is_merged:
87
+ repo_pr_merged[repo] += 1
88
+
89
+ if pr.author_login:
90
+ user_pr_opened[pr.author_login] += 1
91
+ if pr.is_merged:
92
+ user_pr_merged[pr.author_login] += 1
93
+ user_reviews_received[pr.author_login] += pr.review_count
94
+
95
+ for reviewer in pr.reviewer_logins:
96
+ user_reviews_given[reviewer] += 1
97
+
98
+ user_issues_opened: MutableMapping[str, int] = Counter()
99
+ user_issues_closed: MutableMapping[str, int] = Counter()
100
+ user_issues_assigned: MutableMapping[str, int] = Counter()
101
+ repo_issues_opened: MutableMapping[str, int] = Counter()
102
+ repo_issues_closed: MutableMapping[str, int] = Counter()
103
+
104
+ for issue in issues:
105
+ repo = issue.repository_full_name
106
+ repo_issues_opened[repo] += 1
107
+ if issue.closed_at is not None:
108
+ repo_issues_closed[repo] += 1
109
+
110
+ if issue.author_login:
111
+ user_issues_opened[issue.author_login] += 1
112
+ if issue.closed_at is not None:
113
+ user_issues_closed[issue.author_login] += 1
114
+
115
+ for assignee in issue.assignee_logins:
116
+ user_issues_assigned[assignee] += 1
117
+
118
+ user_summaries: Dict[str, UserActivitySummary] = {}
119
+ for login in sorted(user_logins):
120
+ user_summaries[login] = UserActivitySummary(
121
+ login=login,
122
+ commits=user_commits.get(login, 0),
123
+ lines_added=user_lines_added.get(login, 0),
124
+ lines_deleted=user_lines_deleted.get(login, 0),
125
+ pull_requests_opened=user_pr_opened.get(login, 0),
126
+ pull_requests_merged=user_pr_merged.get(login, 0),
127
+ issues_opened=user_issues_opened.get(login, 0),
128
+ issues_closed=user_issues_closed.get(login, 0),
129
+ issues_assigned=user_issues_assigned.get(login, 0),
130
+ reviews_given=user_reviews_given.get(login, 0),
131
+ reviews_received=user_reviews_received.get(login, 0),
132
+ )
133
+
134
+ repo_contributors: MutableMapping[str, set[str]] = defaultdict(set)
135
+ for commit in commits:
136
+ if commit.author_login:
137
+ repo_contributors[commit.repository_full_name].add(commit.author_login)
138
+ for pr in pull_requests:
139
+ if pr.author_login:
140
+ repo_contributors[pr.repository_full_name].add(pr.author_login)
141
+ for reviewer in pr.reviewer_logins:
142
+ repo_contributors[pr.repository_full_name].add(reviewer)
143
+ for issue in issues:
144
+ if issue.author_login:
145
+ repo_contributors[issue.repository_full_name].add(issue.author_login)
146
+ for assignee in issue.assignee_logins:
147
+ repo_contributors[issue.repository_full_name].add(assignee)
148
+
149
+ repo_summaries: Dict[str, RepositoryActivitySummary] = {}
150
+ for full_name in sorted(repo_names):
151
+ contributors = repo_contributors.get(full_name, set())
152
+ repo_summaries[full_name] = RepositoryActivitySummary(
153
+ full_name=full_name,
154
+ commits=repo_commits.get(full_name, 0),
155
+ lines_added=repo_lines_added.get(full_name, 0),
156
+ lines_deleted=repo_lines_deleted.get(full_name, 0),
157
+ pull_requests_opened=repo_pr_opened.get(full_name, 0),
158
+ pull_requests_merged=repo_pr_merged.get(full_name, 0),
159
+ issues_opened=repo_issues_opened.get(full_name, 0),
160
+ issues_closed=repo_issues_closed.get(full_name, 0),
161
+ contributors=len(contributors),
162
+ )
163
+
164
+ contributor_activity: MutableMapping[str, int] = Counter()
165
+ for summary in user_summaries.values():
166
+ contributor_activity[summary.login] = (
167
+ summary.commits
168
+ + summary.pull_requests_opened
169
+ + summary.issues_opened
170
+ + summary.reviews_given
171
+ )
172
+
173
+ active_contributors = sorted(
174
+ login for login, count in contributor_activity.items() if count >= active_threshold
175
+ )
176
+ inactive_contributors = sorted(
177
+ login for login, count in contributor_activity.items() if count < active_threshold
178
+ )
179
+
180
+ return ActivityReport(
181
+ users=user_summaries,
182
+ repositories=repo_summaries,
183
+ active_contributors=active_contributors,
184
+ inactive_contributors=inactive_contributors,
185
+ )
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from dataclasses import dataclass
5
+ from datetime import date
6
+ from typing import Iterable, List
7
+
8
+ from ghscope.models import Commit, Issue, PullRequest
9
+
10
+
11
+ @dataclass
12
+ class ActivityBucket:
13
+ day: date
14
+ commits: int
15
+ pull_requests: int
16
+ issues: int
17
+
18
+
19
+ @dataclass
20
+ class Timelines:
21
+ by_day: List[ActivityBucket]
22
+
23
+
24
+ def build_timelines(
25
+ commits: Iterable[Commit],
26
+ pull_requests: Iterable[PullRequest],
27
+ issues: Iterable[Issue],
28
+ ) -> Timelines:
29
+ commit_counts: dict[date, int] = Counter()
30
+ pr_counts: dict[date, int] = Counter()
31
+ issue_counts: dict[date, int] = Counter()
32
+
33
+ for commit in commits:
34
+ commit_date = commit.committed_at.date()
35
+ commit_counts[commit_date] = commit_counts.get(commit_date, 0) + 1
36
+
37
+ for pr in pull_requests:
38
+ pr_date = pr.created_at.date()
39
+ pr_counts[pr_date] = pr_counts.get(pr_date, 0) + 1
40
+
41
+ for issue in issues:
42
+ issue_date = issue.created_at.date()
43
+ issue_counts[issue_date] = issue_counts.get(issue_date, 0) + 1
44
+
45
+ all_days = sorted(
46
+ {*(commit_counts.keys()), *(pr_counts.keys()), *(issue_counts.keys())},
47
+ )
48
+
49
+ buckets: List[ActivityBucket] = []
50
+ for day in all_days:
51
+ buckets.append(
52
+ ActivityBucket(
53
+ day=day,
54
+ commits=commit_counts.get(day, 0),
55
+ pull_requests=pr_counts.get(day, 0),
56
+ issues=issue_counts.get(day, 0),
57
+ ),
58
+ )
59
+
60
+ return Timelines(by_day=buckets)
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Mapping
5
+
6
+ from .token import TokenConfig, resolve_token
7
+
8
+
9
+ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
10
+ GITHUB_REST_URL = "https://api.github.com"
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GitHubAuth:
15
+ """Authentication configuration for GitHub API access."""
16
+
17
+ token_config: TokenConfig
18
+
19
+ def headers(self) -> Mapping[str, str]:
20
+ token = resolve_token(self.token_config)
21
+ return {
22
+ "Authorization": f"Bearer {token}",
23
+ "Accept": "application/vnd.github+json",
24
+ "X-GitHub-Api-Version": "2022-11-28",
25
+ }