Fix cross-year GitHub activity ranges
This commit is contained in:
+61
-11
@@ -11,6 +11,15 @@ class GitHubSourceError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _iter_year_ranges(from_date: date, to_date: date) -> list[tuple[date, date]]:
|
||||
ranges: list[tuple[date, date]] = []
|
||||
for year in range(from_date.year, to_date.year + 1):
|
||||
range_start = max(from_date, date(year, 1, 1))
|
||||
range_end = min(to_date, date(year, 12, 31))
|
||||
ranges.append((range_start, range_end))
|
||||
return ranges
|
||||
|
||||
|
||||
def _extract_attr(tag: str, attr: str) -> str | None:
|
||||
match = re.search(rf'{attr}="([^"]+)"', tag)
|
||||
return match.group(1) if match else None
|
||||
@@ -42,7 +51,7 @@ def _parse_public_contributions_html(html: str, from_date: date, to_date: date)
|
||||
return normalized
|
||||
|
||||
|
||||
async def _fetch_github_activity_public(
|
||||
async def _fetch_github_activity_public_range(
|
||||
username: str,
|
||||
from_date: date,
|
||||
to_date: date,
|
||||
@@ -66,21 +75,32 @@ async def _fetch_github_activity_public(
|
||||
return _parse_public_contributions_html(response.text, from_date, to_date)
|
||||
|
||||
|
||||
async def fetch_github_activity(
|
||||
async def _fetch_github_activity_public(
|
||||
username: str,
|
||||
token: str | None,
|
||||
from_date: date,
|
||||
to_date: date,
|
||||
timeout_seconds: float = 20.0,
|
||||
timeout_seconds: float,
|
||||
) -> dict[str, int]:
|
||||
if not token:
|
||||
return await _fetch_github_activity_public(
|
||||
normalized: dict[str, int] = {}
|
||||
for range_start, range_end in _iter_year_ranges(from_date, to_date):
|
||||
normalized.update(
|
||||
await _fetch_github_activity_public_range(
|
||||
username=username,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
from_date=range_start,
|
||||
to_date=range_end,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
async def _fetch_github_activity_graphql_range(
|
||||
username: str,
|
||||
token: str,
|
||||
from_date: date,
|
||||
to_date: date,
|
||||
timeout_seconds: float,
|
||||
) -> dict[str, int]:
|
||||
query = """
|
||||
query($login: String!, $from: DateTime!, $to: DateTime!) {
|
||||
user(login: $login) {
|
||||
@@ -103,9 +123,10 @@ async def fetch_github_activity(
|
||||
"to": datetime.combine(to_date, datetime.max.time(), tzinfo=timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
headers: dict[str, str] = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"bearer {token}"
|
||||
headers: dict[str, str] = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"bearer {token}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_seconds) as client:
|
||||
response = await client.post(
|
||||
@@ -140,3 +161,32 @@ async def fetch_github_activity(
|
||||
normalized[date_key] = int(day.get("contributionCount", 0))
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
async def fetch_github_activity(
|
||||
username: str,
|
||||
token: str | None,
|
||||
from_date: date,
|
||||
to_date: date,
|
||||
timeout_seconds: float = 20.0,
|
||||
) -> dict[str, int]:
|
||||
if not token:
|
||||
return await _fetch_github_activity_public(
|
||||
username=username,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
|
||||
normalized: dict[str, int] = {}
|
||||
for range_start, range_end in _iter_year_ranges(from_date, to_date):
|
||||
normalized.update(
|
||||
await _fetch_github_activity_graphql_range(
|
||||
username=username,
|
||||
token=token,
|
||||
from_date=range_start,
|
||||
to_date=range_end,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
)
|
||||
return normalized
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from app.sources import github as github_source
|
||||
|
||||
|
||||
def test_iter_year_ranges_splits_cross_year_window() -> None:
|
||||
ranges = github_source._iter_year_ranges(date(2025, 6, 1), date(2026, 6, 1))
|
||||
|
||||
assert ranges == [
|
||||
(date(2025, 6, 1), date(2025, 12, 31)),
|
||||
(date(2026, 1, 1), date(2026, 6, 1)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_github_activity_without_token_merges_year_slices(monkeypatch) -> None:
|
||||
requested_ranges: list[tuple[date, date]] = []
|
||||
|
||||
async def fake_fetch_public_range(username, from_date, to_date, timeout_seconds):
|
||||
requested_ranges.append((from_date, to_date))
|
||||
return {
|
||||
from_date.isoformat(): 1,
|
||||
to_date.isoformat(): 2,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
github_source,
|
||||
"_fetch_github_activity_public_range",
|
||||
fake_fetch_public_range,
|
||||
)
|
||||
|
||||
activity = await github_source.fetch_github_activity(
|
||||
username="octocat",
|
||||
token=None,
|
||||
from_date=date(2025, 6, 1),
|
||||
to_date=date(2026, 6, 1),
|
||||
timeout_seconds=1.0,
|
||||
)
|
||||
|
||||
assert requested_ranges == [
|
||||
(date(2025, 6, 1), date(2025, 12, 31)),
|
||||
(date(2026, 1, 1), date(2026, 6, 1)),
|
||||
]
|
||||
assert activity == {
|
||||
"2025-06-01": 1,
|
||||
"2025-12-31": 2,
|
||||
"2026-01-01": 1,
|
||||
"2026-06-01": 2,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_github_activity_with_token_merges_year_slices(monkeypatch) -> None:
|
||||
requested_ranges: list[tuple[date, date, str]] = []
|
||||
|
||||
async def fake_fetch_graphql_range(username, token, from_date, to_date, timeout_seconds):
|
||||
requested_ranges.append((from_date, to_date, token))
|
||||
return {
|
||||
from_date.isoformat(): 3,
|
||||
to_date.isoformat(): 4,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
github_source,
|
||||
"_fetch_github_activity_graphql_range",
|
||||
fake_fetch_graphql_range,
|
||||
)
|
||||
|
||||
activity = await github_source.fetch_github_activity(
|
||||
username="octocat",
|
||||
token="secret",
|
||||
from_date=date(2025, 6, 1),
|
||||
to_date=date(2026, 6, 1),
|
||||
timeout_seconds=1.0,
|
||||
)
|
||||
|
||||
assert requested_ranges == [
|
||||
(date(2025, 6, 1), date(2025, 12, 31), "secret"),
|
||||
(date(2026, 1, 1), date(2026, 6, 1), "secret"),
|
||||
]
|
||||
assert activity == {
|
||||
"2025-06-01": 3,
|
||||
"2025-12-31": 4,
|
||||
"2026-01-01": 3,
|
||||
"2026-06-01": 4,
|
||||
}
|
||||
Reference in New Issue
Block a user