github-talent-mcp 0.2.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.2.0/src/github_talent_mcp.egg-info → github_talent_mcp-0.3.0}/PKG-INFO +6 -9
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/README.md +5 -8
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/pyproject.toml +1 -1
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/github_client.py +64 -10
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0/src/github_talent_mcp.egg-info}/PKG-INFO +6 -9
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/SOURCES.txt +1 -0
- github_talent_mcp-0.3.0/tests/test_rate_limit.py +93 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/LICENSE +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/setup.cfg +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/__init__.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/__main__.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/models.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/scoring.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/server.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/__init__.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/bulk.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/compare.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/contributors.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/outreach.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/profile.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/rank.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/score_jd.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/search.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/dependency_links.txt +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/entry_points.txt +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/requires.txt +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/top_level.txt +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/tests/test_bulk.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/tests/test_compare.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/tests/test_outreach.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/tests/test_score_jd.py +0 -0
- {github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/tests/test_scoring.py +0 -0
- {github_talent_mcp-0.2.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
|
|
@@ -33,10 +33,13 @@ 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
|
|
|
42
45
|
https://github.com/user-attachments/assets/b2dbe9e0-26ee-4849-861a-4b5cb268facc
|
|
@@ -149,13 +152,7 @@ Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
|
149
152
|
|
|
150
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.
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
copilot mcp add github-talent -e GITHUB_TOKEN=$GITHUB_TOKEN -- uvx github-talent-mcp
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Or add it manually to `~/.copilot/mcp-config.json`:
|
|
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:
|
|
159
156
|
|
|
160
157
|
```json
|
|
161
158
|
{
|
|
@@ -175,7 +172,7 @@ Or add it manually to `~/.copilot/mcp-config.json`:
|
|
|
175
172
|
|
|
176
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`.
|
|
177
174
|
|
|
178
|
-
The **Copilot app**
|
|
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`.
|
|
179
176
|
|
|
180
177
|
## Try It
|
|
181
178
|
|
|
@@ -4,10 +4,13 @@
|
|
|
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
|
|
|
13
16
|
https://github.com/user-attachments/assets/b2dbe9e0-26ee-4849-861a-4b5cb268facc
|
|
@@ -120,13 +123,7 @@ Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
|
120
123
|
|
|
121
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.
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
copilot mcp add github-talent -e GITHUB_TOKEN=$GITHUB_TOKEN -- uvx github-talent-mcp
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Or add it manually to `~/.copilot/mcp-config.json`:
|
|
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:
|
|
130
127
|
|
|
131
128
|
```json
|
|
132
129
|
{
|
|
@@ -146,7 +143,7 @@ Or add it manually to `~/.copilot/mcp-config.json`:
|
|
|
146
143
|
|
|
147
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`.
|
|
148
145
|
|
|
149
|
-
The **Copilot app**
|
|
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`.
|
|
150
147
|
|
|
151
148
|
## Try It
|
|
152
149
|
|
|
@@ -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"}
|
|
@@ -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(
|
|
@@ -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
|
)
|
|
@@ -170,7 +224,7 @@ class GitHubClient:
|
|
|
170
224
|
cached = self._cache_get(cache_key)
|
|
171
225
|
if cached is not None:
|
|
172
226
|
return cached
|
|
173
|
-
resp = await self.
|
|
227
|
+
resp = await self._get(
|
|
174
228
|
"/search/commits",
|
|
175
229
|
params={
|
|
176
230
|
"q": f"author:{username} user:{username} committer-date:>={since_date}",
|
|
@@ -189,7 +243,7 @@ class GitHubClient:
|
|
|
189
243
|
cached = self._cache_get(cache_key)
|
|
190
244
|
if cached is not None:
|
|
191
245
|
return cached
|
|
192
|
-
resp = await self.
|
|
246
|
+
resp = await self._get(
|
|
193
247
|
"/search/issues",
|
|
194
248
|
params={
|
|
195
249
|
"q": f"author:{username} type:pr created:>={since_date}",
|
|
@@ -208,7 +262,7 @@ class GitHubClient:
|
|
|
208
262
|
if cached is not None:
|
|
209
263
|
return cached
|
|
210
264
|
try:
|
|
211
|
-
resp = await self.
|
|
265
|
+
resp = await self._get(
|
|
212
266
|
f"/repos/{username}/{username}/readme",
|
|
213
267
|
headers={"Accept": "application/vnd.github.raw+json"},
|
|
214
268
|
)
|
|
@@ -224,7 +278,7 @@ class GitHubClient:
|
|
|
224
278
|
async def get_repo_contributors(
|
|
225
279
|
self, owner: str, repo: str, per_page: int = 30,
|
|
226
280
|
) -> list[dict]:
|
|
227
|
-
resp = await self.
|
|
281
|
+
resp = await self._get(
|
|
228
282
|
f"/repos/{owner}/{repo}/contributors",
|
|
229
283
|
params={"per_page": per_page},
|
|
230
284
|
)
|
|
@@ -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
|
|
@@ -33,10 +33,13 @@ 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
|
|
|
42
45
|
https://github.com/user-attachments/assets/b2dbe9e0-26ee-4849-861a-4b5cb268facc
|
|
@@ -149,13 +152,7 @@ Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
|
149
152
|
|
|
150
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.
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
copilot mcp add github-talent -e GITHUB_TOKEN=$GITHUB_TOKEN -- uvx github-talent-mcp
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Or add it manually to `~/.copilot/mcp-config.json`:
|
|
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:
|
|
159
156
|
|
|
160
157
|
```json
|
|
161
158
|
{
|
|
@@ -175,7 +172,7 @@ Or add it manually to `~/.copilot/mcp-config.json`:
|
|
|
175
172
|
|
|
176
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`.
|
|
177
174
|
|
|
178
|
-
The **Copilot app**
|
|
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`.
|
|
179
176
|
|
|
180
177
|
## Try It
|
|
181
178
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from github_talent_mcp.github_client import GitHubClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _resp(status: int, headers: dict | None = None) -> httpx.Response:
|
|
12
|
+
return httpx.Response(
|
|
13
|
+
status,
|
|
14
|
+
headers=headers or {},
|
|
15
|
+
request=httpx.Request("GET", "https://api.github.com/x"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _client_with(responses: list[httpx.Response]) -> GitHubClient:
|
|
20
|
+
client = GitHubClient.__new__(GitHubClient)
|
|
21
|
+
client._client = type("FakeHTTP", (), {})()
|
|
22
|
+
client._client.get = AsyncMock(side_effect=list(responses))
|
|
23
|
+
return client
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture(autouse=True)
|
|
27
|
+
def no_sleep(monkeypatch):
|
|
28
|
+
"""Capture backoff durations without actually waiting."""
|
|
29
|
+
slept: list[float] = []
|
|
30
|
+
|
|
31
|
+
async def fake_sleep(delay):
|
|
32
|
+
slept.append(delay)
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
|
|
35
|
+
return slept
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_retries_on_primary_rate_limit(no_sleep):
|
|
40
|
+
reset = str(int(time.time()) + 2)
|
|
41
|
+
client = _client_with([
|
|
42
|
+
_resp(403, {"X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset}),
|
|
43
|
+
_resp(200),
|
|
44
|
+
])
|
|
45
|
+
resp = await client._get("/x")
|
|
46
|
+
assert resp.status_code == 200
|
|
47
|
+
assert client._client.get.await_count == 2
|
|
48
|
+
assert no_sleep and no_sleep[0] <= 61.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_retries_on_secondary_rate_limit_retry_after(no_sleep):
|
|
53
|
+
client = _client_with([
|
|
54
|
+
_resp(429, {"Retry-After": "3"}),
|
|
55
|
+
_resp(200),
|
|
56
|
+
])
|
|
57
|
+
resp = await client._get("/x")
|
|
58
|
+
assert resp.status_code == 200
|
|
59
|
+
assert no_sleep == [3.0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_retries_on_5xx(no_sleep):
|
|
64
|
+
client = _client_with([_resp(502), _resp(200)])
|
|
65
|
+
resp = await client._get("/x")
|
|
66
|
+
assert resp.status_code == 200
|
|
67
|
+
assert client._client.get.await_count == 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_no_retry_on_404(no_sleep):
|
|
72
|
+
client = _client_with([_resp(404)])
|
|
73
|
+
resp = await client._get("/x")
|
|
74
|
+
assert resp.status_code == 404
|
|
75
|
+
assert client._client.get.await_count == 1
|
|
76
|
+
assert no_sleep == []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_no_retry_on_403_with_quota_remaining(no_sleep):
|
|
81
|
+
# A 403 that is NOT a rate limit (e.g. permissions) should not retry.
|
|
82
|
+
client = _client_with([_resp(403, {"X-RateLimit-Remaining": "4999"})])
|
|
83
|
+
resp = await client._get("/x")
|
|
84
|
+
assert resp.status_code == 403
|
|
85
|
+
assert client._client.get.await_count == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_gives_up_after_max_retries(no_sleep):
|
|
90
|
+
client = _client_with([_resp(403, {"Retry-After": "1"})] * 10)
|
|
91
|
+
resp = await client._get("/x", max_retries=3)
|
|
92
|
+
assert resp.status_code == 403
|
|
93
|
+
assert client._client.get.await_count == 4 # initial + 3 retries
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp/tools/contributors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/requires.txt
RENAMED
|
File without changes
|
{github_talent_mcp-0.2.0 → github_talent_mcp-0.3.0}/src/github_talent_mcp.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|