github-talent-mcp 0.1.0__tar.gz → 0.3.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.
- {github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info → github_talent_mcp-0.3.0}/PKG-INFO +55 -6
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/README.md +50 -1
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/pyproject.toml +5 -5
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/github_client.py +105 -9
- github_talent_mcp-0.3.0/src/github_talent_mcp/scoring.py +342 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/server.py +112 -0
- github_talent_mcp-0.3.0/src/github_talent_mcp/tools/bulk.py +136 -0
- github_talent_mcp-0.3.0/src/github_talent_mcp/tools/compare.py +99 -0
- github_talent_mcp-0.3.0/src/github_talent_mcp/tools/outreach.py +137 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/profile.py +16 -2
- github_talent_mcp-0.3.0/src/github_talent_mcp/tools/score_jd.py +64 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0/src/github_talent_mcp.egg-info}/PKG-INFO +55 -6
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/SOURCES.txt +9 -0
- github_talent_mcp-0.3.0/src/github_talent_mcp.egg-info/requires.txt +4 -0
- github_talent_mcp-0.3.0/tests/test_bulk.py +122 -0
- github_talent_mcp-0.3.0/tests/test_compare.py +94 -0
- github_talent_mcp-0.3.0/tests/test_outreach.py +104 -0
- github_talent_mcp-0.3.0/tests/test_rate_limit.py +93 -0
- github_talent_mcp-0.3.0/tests/test_score_jd.py +88 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/scoring.py +0 -169
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/requires.txt +0 -4
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/LICENSE +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/setup.cfg +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/__init__.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/__main__.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/models.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/__init__.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/contributors.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/rank.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/search.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/dependency_links.txt +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/entry_points.txt +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/top_level.txt +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/tests/test_scoring.py +0 -0
- {github_talent_mcp-0.1.0 → github_talent_mcp-0.3.0}/tests/test_search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-talent-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP server for searching, scoring, and ranking GitHub developers for technical recruiting
|
|
5
5
|
Author: Carolina Cherry
|
|
6
6
|
License: MIT
|
|
@@ -21,10 +21,10 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: mcp
|
|
25
|
-
Requires-Dist: httpx
|
|
26
|
-
Requires-Dist: pydantic
|
|
27
|
-
Requires-Dist: python-dotenv
|
|
24
|
+
Requires-Dist: mcp<2,>=1.0.0
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27.0
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.0.0
|
|
27
|
+
Requires-Dist: python-dotenv<2,>=1.0.0
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
# github-talent-mcp
|
|
@@ -33,12 +33,19 @@ Dynamic: license-file
|
|
|
33
33
|
[](https://www.python.org)
|
|
34
34
|
[](https://modelcontextprotocol.io)
|
|
35
35
|
[](https://claude.ai)
|
|
36
|
+
[](https://github.com/features/copilot)
|
|
36
37
|
[](https://docs.github.com/en/rest)
|
|
37
38
|
|
|
38
39
|
MCP server that searches, scores, and ranks GitHub developers for technical recruiting.
|
|
39
40
|
|
|
41
|
+
Works with **Claude** (Code & Desktop) and **GitHub Copilot** (CLI & desktop app) — any MCP client that speaks stdio.
|
|
42
|
+
|
|
40
43
|
## Demo
|
|
41
44
|
|
|
45
|
+
https://github.com/user-attachments/assets/b2dbe9e0-26ee-4849-861a-4b5cb268facc
|
|
46
|
+
|
|
47
|
+
Sourcing candidates for a real Anthropic JD, live in Claude Cowork.
|
|
48
|
+
|
|
42
49
|
https://github.com/user-attachments/assets/2dfd82b4-3eb5-4f2b-bc0a-2580b95043e4
|
|
43
50
|
|
|
44
51
|
### Profile deep dive
|
|
@@ -118,7 +125,7 @@ Then set the token as an environment variable. Either:
|
|
|
118
125
|
- Export it in your shell: `export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`
|
|
119
126
|
- Or keep it in the `.env` file — the server reads it via `python-dotenv` on startup
|
|
120
127
|
|
|
121
|
-
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see
|
|
128
|
+
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see 8 tools under `github-talent`.
|
|
122
129
|
|
|
123
130
|
#### Claude Desktop
|
|
124
131
|
|
|
@@ -141,6 +148,32 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
141
148
|
|
|
142
149
|
Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
143
150
|
|
|
151
|
+
#### GitHub Copilot (CLI & desktop app)
|
|
152
|
+
|
|
153
|
+
The [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) and the [GitHub Copilot app](https://github.com/features/ai/github-app) share one MCP config, so a single setup covers both.
|
|
154
|
+
|
|
155
|
+
Add it to `~/.copilot/mcp-config.json` (global), or commit a `.copilot/mcp-config.json` in your repo — the Copilot app picks that up automatically:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"github-talent": {
|
|
161
|
+
"type": "local",
|
|
162
|
+
"command": "uvx",
|
|
163
|
+
"args": ["github-talent-mcp"],
|
|
164
|
+
"env": {
|
|
165
|
+
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
|
166
|
+
},
|
|
167
|
+
"tools": ["*"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Export your token first (`export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`) — Copilot only inherits `PATH`, so the `${GITHUB_TOKEN}` reference reads it from your shell. If `uvx` isn't on your `PATH`, use its absolute path as `command`.
|
|
174
|
+
|
|
175
|
+
The **Copilot app** reads this same config and also lets you add servers under Settings → MCP. In a Copilot CLI session, run `/mcp add` to register interactively or `/mcp show` to verify — you should see 8 tools under `github-talent`.
|
|
176
|
+
|
|
144
177
|
## Try It
|
|
145
178
|
|
|
146
179
|
Once installed, paste these prompts to verify everything works:
|
|
@@ -157,6 +190,18 @@ Once installed, paste these prompts to verify everything works:
|
|
|
157
190
|
**Repo contributors:**
|
|
158
191
|
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
159
192
|
|
|
193
|
+
**JD scoring:**
|
|
194
|
+
> Score these candidates against this job description: [paste JD]. Candidates: tiangolo, karpathy, hwchase17
|
|
195
|
+
|
|
196
|
+
**Compare candidates:**
|
|
197
|
+
> Compare tiangolo and hwchase17 for a Senior Python AI Engineer role
|
|
198
|
+
|
|
199
|
+
**Bulk scoring:**
|
|
200
|
+
> Score these 10 GitHub usernames and give me a ranked table: [paste list]
|
|
201
|
+
|
|
202
|
+
**Outreach:**
|
|
203
|
+
> Generate a casual recruiter message for tiangolo about a Senior Python role at Acme. My name is Daniel.
|
|
204
|
+
|
|
160
205
|
## Tools
|
|
161
206
|
|
|
162
207
|
| Tool | Description |
|
|
@@ -164,6 +209,10 @@ Once installed, paste these prompts to verify everything works:
|
|
|
164
209
|
| `search_developers` | Search GitHub users by language, location, activity, followers. For topic-based sourcing, use `get_repo_contributors` on relevant repos instead. |
|
|
165
210
|
| `get_developer_profile` | Deep profile enrichment: languages, stars, commits + PRs, OSS contributions, license breakdown, profile README, and activity score with breakdown. |
|
|
166
211
|
| `rank_candidates` | Rank usernames against a job description. Returns sorted candidates with combined score, strengths, gaps, and reasoning. |
|
|
212
|
+
| `score_against_jd` | Score candidates against a JD with per-dimension breakdown (tech stack, experience level, OSS signal, leadership). Returns gaps and personalized interview questions. |
|
|
213
|
+
| `compare_candidates` | Side-by-side comparison of 2-5 candidates. Shows dimension winners and a recommendation. Optionally scored against a JD. |
|
|
214
|
+
| `bulk_score` | Score up to 100 GitHub usernames in one call. Returns a ranked markdown table or CSV. Supports optional JD matching. |
|
|
215
|
+
| `generate_outreach` | Generate personalized recruiter messages (short/medium/detailed) that reference the candidate's actual repos and contributions. Requires your company name and sender name. Casual or formal tone. |
|
|
167
216
|
| `get_repo_contributors` | Top contributors for any repo. Accepts `owner/repo` or full URL. The fastest way to source for a specific domain. |
|
|
168
217
|
|
|
169
218
|
## Scoring
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
[](https://www.python.org)
|
|
5
5
|
[](https://modelcontextprotocol.io)
|
|
6
6
|
[](https://claude.ai)
|
|
7
|
+
[](https://github.com/features/copilot)
|
|
7
8
|
[](https://docs.github.com/en/rest)
|
|
8
9
|
|
|
9
10
|
MCP server that searches, scores, and ranks GitHub developers for technical recruiting.
|
|
10
11
|
|
|
12
|
+
Works with **Claude** (Code & Desktop) and **GitHub Copilot** (CLI & desktop app) — any MCP client that speaks stdio.
|
|
13
|
+
|
|
11
14
|
## Demo
|
|
12
15
|
|
|
16
|
+
https://github.com/user-attachments/assets/b2dbe9e0-26ee-4849-861a-4b5cb268facc
|
|
17
|
+
|
|
18
|
+
Sourcing candidates for a real Anthropic JD, live in Claude Cowork.
|
|
19
|
+
|
|
13
20
|
https://github.com/user-attachments/assets/2dfd82b4-3eb5-4f2b-bc0a-2580b95043e4
|
|
14
21
|
|
|
15
22
|
### Profile deep dive
|
|
@@ -89,7 +96,7 @@ Then set the token as an environment variable. Either:
|
|
|
89
96
|
- Export it in your shell: `export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`
|
|
90
97
|
- Or keep it in the `.env` file — the server reads it via `python-dotenv` on startup
|
|
91
98
|
|
|
92
|
-
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see
|
|
99
|
+
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see 8 tools under `github-talent`.
|
|
93
100
|
|
|
94
101
|
#### Claude Desktop
|
|
95
102
|
|
|
@@ -112,6 +119,32 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
112
119
|
|
|
113
120
|
Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
114
121
|
|
|
122
|
+
#### GitHub Copilot (CLI & desktop app)
|
|
123
|
+
|
|
124
|
+
The [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) and the [GitHub Copilot app](https://github.com/features/ai/github-app) share one MCP config, so a single setup covers both.
|
|
125
|
+
|
|
126
|
+
Add it to `~/.copilot/mcp-config.json` (global), or commit a `.copilot/mcp-config.json` in your repo — the Copilot app picks that up automatically:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"github-talent": {
|
|
132
|
+
"type": "local",
|
|
133
|
+
"command": "uvx",
|
|
134
|
+
"args": ["github-talent-mcp"],
|
|
135
|
+
"env": {
|
|
136
|
+
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
|
137
|
+
},
|
|
138
|
+
"tools": ["*"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Export your token first (`export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`) — Copilot only inherits `PATH`, so the `${GITHUB_TOKEN}` reference reads it from your shell. If `uvx` isn't on your `PATH`, use its absolute path as `command`.
|
|
145
|
+
|
|
146
|
+
The **Copilot app** reads this same config and also lets you add servers under Settings → MCP. In a Copilot CLI session, run `/mcp add` to register interactively or `/mcp show` to verify — you should see 8 tools under `github-talent`.
|
|
147
|
+
|
|
115
148
|
## Try It
|
|
116
149
|
|
|
117
150
|
Once installed, paste these prompts to verify everything works:
|
|
@@ -128,6 +161,18 @@ Once installed, paste these prompts to verify everything works:
|
|
|
128
161
|
**Repo contributors:**
|
|
129
162
|
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
130
163
|
|
|
164
|
+
**JD scoring:**
|
|
165
|
+
> Score these candidates against this job description: [paste JD]. Candidates: tiangolo, karpathy, hwchase17
|
|
166
|
+
|
|
167
|
+
**Compare candidates:**
|
|
168
|
+
> Compare tiangolo and hwchase17 for a Senior Python AI Engineer role
|
|
169
|
+
|
|
170
|
+
**Bulk scoring:**
|
|
171
|
+
> Score these 10 GitHub usernames and give me a ranked table: [paste list]
|
|
172
|
+
|
|
173
|
+
**Outreach:**
|
|
174
|
+
> Generate a casual recruiter message for tiangolo about a Senior Python role at Acme. My name is Daniel.
|
|
175
|
+
|
|
131
176
|
## Tools
|
|
132
177
|
|
|
133
178
|
| Tool | Description |
|
|
@@ -135,6 +180,10 @@ Once installed, paste these prompts to verify everything works:
|
|
|
135
180
|
| `search_developers` | Search GitHub users by language, location, activity, followers. For topic-based sourcing, use `get_repo_contributors` on relevant repos instead. |
|
|
136
181
|
| `get_developer_profile` | Deep profile enrichment: languages, stars, commits + PRs, OSS contributions, license breakdown, profile README, and activity score with breakdown. |
|
|
137
182
|
| `rank_candidates` | Rank usernames against a job description. Returns sorted candidates with combined score, strengths, gaps, and reasoning. |
|
|
183
|
+
| `score_against_jd` | Score candidates against a JD with per-dimension breakdown (tech stack, experience level, OSS signal, leadership). Returns gaps and personalized interview questions. |
|
|
184
|
+
| `compare_candidates` | Side-by-side comparison of 2-5 candidates. Shows dimension winners and a recommendation. Optionally scored against a JD. |
|
|
185
|
+
| `bulk_score` | Score up to 100 GitHub usernames in one call. Returns a ranked markdown table or CSV. Supports optional JD matching. |
|
|
186
|
+
| `generate_outreach` | Generate personalized recruiter messages (short/medium/detailed) that reference the candidate's actual repos and contributions. Requires your company name and sender name. Casual or formal tone. |
|
|
138
187
|
| `get_repo_contributors` | Top contributors for any repo. Accepts `owner/repo` or full URL. The fastest way to source for a specific domain. |
|
|
139
188
|
|
|
140
189
|
## Scoring
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "github-talent-mcp"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "MCP server for searching, scoring, and ranking GitHub developers for technical recruiting"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -26,10 +26,10 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Software Development :: Libraries",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"mcp>=1.0.0",
|
|
30
|
-
"httpx>=0.27.0",
|
|
31
|
-
"pydantic>=2.0.0",
|
|
32
|
-
"python-dotenv>=1.0.0",
|
|
29
|
+
"mcp>=1.0.0,<2",
|
|
30
|
+
"httpx>=0.27.0,<1",
|
|
31
|
+
"pydantic>=2.0.0,<3",
|
|
32
|
+
"python-dotenv>=1.0.0,<2",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import time
|
|
@@ -54,6 +55,59 @@ class GitHubClient:
|
|
|
54
55
|
reset = resp.headers.get("X-RateLimit-Reset", "unknown")
|
|
55
56
|
log.warning(f"Rate limit low: {remaining} remaining, resets at {reset}")
|
|
56
57
|
|
|
58
|
+
# -- Request wrapper with rate-limit backoff --
|
|
59
|
+
|
|
60
|
+
async def _get(
|
|
61
|
+
self,
|
|
62
|
+
url: str,
|
|
63
|
+
*,
|
|
64
|
+
params: dict | None = None,
|
|
65
|
+
headers: dict | None = None,
|
|
66
|
+
max_retries: int = 3,
|
|
67
|
+
) -> httpx.Response:
|
|
68
|
+
"""GET with backoff on GitHub rate limits (primary + secondary) and 5xx.
|
|
69
|
+
|
|
70
|
+
Respects Retry-After and X-RateLimit-Reset so a broad sourcing run paces
|
|
71
|
+
itself instead of erroring out mid-job. Each wait is capped (60s) so an
|
|
72
|
+
exhausted hourly quota fails fast rather than hanging the session.
|
|
73
|
+
"""
|
|
74
|
+
resp = await self._client.get(url, params=params, headers=headers)
|
|
75
|
+
for attempt in range(max_retries):
|
|
76
|
+
if not self._should_retry(resp):
|
|
77
|
+
return resp
|
|
78
|
+
delay = self._retry_after_seconds(resp, attempt)
|
|
79
|
+
log.warning(
|
|
80
|
+
f"GitHub {resp.status_code} on {url} — backing off {delay:.0f}s "
|
|
81
|
+
f"(retry {attempt + 1}/{max_retries})"
|
|
82
|
+
)
|
|
83
|
+
await asyncio.sleep(delay)
|
|
84
|
+
resp = await self._client.get(url, params=params, headers=headers)
|
|
85
|
+
return resp
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _should_retry(resp: httpx.Response) -> bool:
|
|
89
|
+
if resp.status_code >= 500:
|
|
90
|
+
return True
|
|
91
|
+
if resp.status_code in (403, 429):
|
|
92
|
+
# Secondary limit sends Retry-After; primary limit zeroes Remaining.
|
|
93
|
+
if resp.headers.get("Retry-After"):
|
|
94
|
+
return True
|
|
95
|
+
if resp.headers.get("X-RateLimit-Remaining") == "0":
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _retry_after_seconds(resp: httpx.Response, attempt: int) -> float:
|
|
101
|
+
retry_after = resp.headers.get("Retry-After", "")
|
|
102
|
+
if retry_after.isdigit():
|
|
103
|
+
return min(float(retry_after), 60.0)
|
|
104
|
+
if resp.headers.get("X-RateLimit-Remaining") == "0":
|
|
105
|
+
reset = resp.headers.get("X-RateLimit-Reset", "")
|
|
106
|
+
if reset.isdigit():
|
|
107
|
+
wait = float(reset) - time.time()
|
|
108
|
+
return min(max(wait, 0.0), 60.0) + 1.0
|
|
109
|
+
return min(2.0 ** attempt, 30.0)
|
|
110
|
+
|
|
57
111
|
# -- API methods --
|
|
58
112
|
|
|
59
113
|
async def search_users(
|
|
@@ -71,7 +125,7 @@ class GitHubClient:
|
|
|
71
125
|
for lang in languages:
|
|
72
126
|
parts.append(f"language:{lang}")
|
|
73
127
|
if location:
|
|
74
|
-
parts.append(f
|
|
128
|
+
parts.append(f'location:"{location}"')
|
|
75
129
|
if min_followers is not None:
|
|
76
130
|
parts.append(f"followers:>={min_followers}")
|
|
77
131
|
if min_repos is not None:
|
|
@@ -81,7 +135,7 @@ class GitHubClient:
|
|
|
81
135
|
# Recent activity should be verified via get_developer_profile.
|
|
82
136
|
|
|
83
137
|
q = " ".join(parts)
|
|
84
|
-
resp = await self.
|
|
138
|
+
resp = await self._get(
|
|
85
139
|
"/search/users",
|
|
86
140
|
params={"q": q, "per_page": per_page, "page": page, "sort": "followers", "order": "desc"},
|
|
87
141
|
)
|
|
@@ -94,7 +148,7 @@ class GitHubClient:
|
|
|
94
148
|
cached = self._cache_get(cache_key)
|
|
95
149
|
if cached is not None:
|
|
96
150
|
return cached
|
|
97
|
-
resp = await self.
|
|
151
|
+
resp = await self._get(f"/repos/{owner}/{repo}")
|
|
98
152
|
self._check_rate_limit(resp)
|
|
99
153
|
resp.raise_for_status()
|
|
100
154
|
data = resp.json()
|
|
@@ -106,7 +160,7 @@ class GitHubClient:
|
|
|
106
160
|
cached = self._cache_get(cache_key)
|
|
107
161
|
if cached is not None:
|
|
108
162
|
return cached
|
|
109
|
-
resp = await self.
|
|
163
|
+
resp = await self._get(f"/users/{username}")
|
|
110
164
|
self._check_rate_limit(resp)
|
|
111
165
|
resp.raise_for_status()
|
|
112
166
|
data = resp.json()
|
|
@@ -118,7 +172,7 @@ class GitHubClient:
|
|
|
118
172
|
cached = self._cache_get(cache_key)
|
|
119
173
|
if cached is not None:
|
|
120
174
|
return cached
|
|
121
|
-
resp = await self.
|
|
175
|
+
resp = await self._get(
|
|
122
176
|
f"/users/{username}/repos",
|
|
123
177
|
params={"per_page": per_page, "sort": "pushed", "direction": "desc", "type": "owner"},
|
|
124
178
|
)
|
|
@@ -133,7 +187,7 @@ class GitHubClient:
|
|
|
133
187
|
cached = self._cache_get(cache_key)
|
|
134
188
|
if cached is not None:
|
|
135
189
|
return cached
|
|
136
|
-
resp = await self.
|
|
190
|
+
resp = await self._get(f"/repos/{owner}/{repo}/languages")
|
|
137
191
|
self._check_rate_limit(resp)
|
|
138
192
|
resp.raise_for_status()
|
|
139
193
|
data = resp.json()
|
|
@@ -147,7 +201,7 @@ class GitHubClient:
|
|
|
147
201
|
return cached
|
|
148
202
|
all_events: list[dict] = []
|
|
149
203
|
for page in range(1, max_pages + 1):
|
|
150
|
-
resp = await self.
|
|
204
|
+
resp = await self._get(
|
|
151
205
|
f"/users/{username}/events/public",
|
|
152
206
|
params={"per_page": 100, "page": page},
|
|
153
207
|
)
|
|
@@ -160,13 +214,55 @@ class GitHubClient:
|
|
|
160
214
|
self._cache_set(cache_key, all_events)
|
|
161
215
|
return all_events
|
|
162
216
|
|
|
217
|
+
async def search_commit_count(self, username: str, since_date: str) -> int:
|
|
218
|
+
"""Count commits by username since a date using the Search API.
|
|
219
|
+
|
|
220
|
+
More accurate than Events API for users whose commits don't
|
|
221
|
+
surface as PushEvents (e.g. Torvalds' kernel merges).
|
|
222
|
+
"""
|
|
223
|
+
cache_key = f"commit_count:{username}:{since_date}"
|
|
224
|
+
cached = self._cache_get(cache_key)
|
|
225
|
+
if cached is not None:
|
|
226
|
+
return cached
|
|
227
|
+
resp = await self._get(
|
|
228
|
+
"/search/commits",
|
|
229
|
+
params={
|
|
230
|
+
"q": f"author:{username} user:{username} committer-date:>={since_date}",
|
|
231
|
+
"per_page": 1,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
self._check_rate_limit(resp)
|
|
235
|
+
resp.raise_for_status()
|
|
236
|
+
count = resp.json().get("total_count", 0)
|
|
237
|
+
self._cache_set(cache_key, count)
|
|
238
|
+
return count
|
|
239
|
+
|
|
240
|
+
async def search_pr_count(self, username: str, since_date: str) -> int:
|
|
241
|
+
"""Count PRs opened by username since a date using the Search API."""
|
|
242
|
+
cache_key = f"pr_count:{username}:{since_date}"
|
|
243
|
+
cached = self._cache_get(cache_key)
|
|
244
|
+
if cached is not None:
|
|
245
|
+
return cached
|
|
246
|
+
resp = await self._get(
|
|
247
|
+
"/search/issues",
|
|
248
|
+
params={
|
|
249
|
+
"q": f"author:{username} type:pr created:>={since_date}",
|
|
250
|
+
"per_page": 1,
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
self._check_rate_limit(resp)
|
|
254
|
+
resp.raise_for_status()
|
|
255
|
+
count = resp.json().get("total_count", 0)
|
|
256
|
+
self._cache_set(cache_key, count)
|
|
257
|
+
return count
|
|
258
|
+
|
|
163
259
|
async def get_profile_readme(self, username: str) -> str | None:
|
|
164
260
|
cache_key = f"readme:{username}"
|
|
165
261
|
cached = self._cache_get(cache_key)
|
|
166
262
|
if cached is not None:
|
|
167
263
|
return cached
|
|
168
264
|
try:
|
|
169
|
-
resp = await self.
|
|
265
|
+
resp = await self._get(
|
|
170
266
|
f"/repos/{username}/{username}/readme",
|
|
171
267
|
headers={"Accept": "application/vnd.github.raw+json"},
|
|
172
268
|
)
|
|
@@ -182,7 +278,7 @@ class GitHubClient:
|
|
|
182
278
|
async def get_repo_contributors(
|
|
183
279
|
self, owner: str, repo: str, per_page: int = 30,
|
|
184
280
|
) -> list[dict]:
|
|
185
|
-
resp = await self.
|
|
281
|
+
resp = await self._get(
|
|
186
282
|
f"/repos/{owner}/{repo}/contributors",
|
|
187
283
|
params={"per_page": per_page},
|
|
188
284
|
)
|