Improve activity response metadata
This commit is contained in:
@@ -36,6 +36,8 @@ Supported query params:
|
||||
- `theme=dark|light`
|
||||
- `source=all|github|gitea`
|
||||
|
||||
Responses also include an `X-Activity-Stale: true|false` header so image consumers can detect when stale cached data was used after an upstream fetch failure.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
|
||||
+39
-7
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
@@ -128,8 +129,14 @@ async def collect_merged_activity(
|
||||
github_stale = False
|
||||
gitea_stale = False
|
||||
|
||||
fetch_names: list[str] = []
|
||||
fetch_tasks: list[asyncio.Task[tuple[dict[str, int], bool]]] = []
|
||||
|
||||
if options.source in ("all", "github"):
|
||||
github_data, github_stale = await _fetch_with_cache(
|
||||
fetch_names.append("github")
|
||||
fetch_tasks.append(
|
||||
asyncio.create_task(
|
||||
_fetch_with_cache(
|
||||
cache,
|
||||
gh_key,
|
||||
lambda: fetch_github_activity(
|
||||
@@ -138,10 +145,15 @@ async def collect_merged_activity(
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if options.source in ("all", "gitea"):
|
||||
gitea_data, gitea_stale = await _fetch_with_cache(
|
||||
fetch_names.append("gitea")
|
||||
fetch_tasks.append(
|
||||
asyncio.create_task(
|
||||
_fetch_with_cache(
|
||||
cache,
|
||||
gt_key,
|
||||
lambda: fetch_gitea_activity(
|
||||
@@ -151,7 +163,15 @@ async def collect_merged_activity(
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
for name, (data, stale) in zip(fetch_names, await asyncio.gather(*fetch_tasks), strict=True):
|
||||
if name == "github":
|
||||
github_data, github_stale = data, stale
|
||||
else:
|
||||
gitea_data, gitea_stale = data, stale
|
||||
|
||||
merged = merge_activity(github_data, gitea_data, dates=_date_keys(from_date, to_date))
|
||||
merged = filter_activity_source(merged, options.source)
|
||||
@@ -169,6 +189,10 @@ def _daily_totals(merged: dict[str, dict[str, int]]) -> dict[str, int]:
|
||||
return {day: int(payload.get("total", 0)) for day, payload in merged.items()}
|
||||
|
||||
|
||||
def _activity_headers(result: ActivityResult) -> dict[str, str]:
|
||||
return {"X-Activity-Stale": "true" if result.stale else "false"}
|
||||
|
||||
|
||||
def _image_cache_key(prefix: str, options: QueryOptions, result: ActivityResult) -> str:
|
||||
digest = hashlib.sha1(
|
||||
json.dumps(result.merged, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
@@ -203,7 +227,7 @@ async def activity_json(
|
||||
"stale": result.stale,
|
||||
"activity": result.merged,
|
||||
}
|
||||
return JSONResponse(content=payload)
|
||||
return JSONResponse(content=payload, headers=_activity_headers(result))
|
||||
|
||||
|
||||
@app.get("/activity.svg")
|
||||
@@ -222,7 +246,11 @@ async def activity_svg(
|
||||
if cache is not None:
|
||||
cached = cache.get_json(cache_key)
|
||||
if cached is not None:
|
||||
return Response(content=str(cached.value), media_type="image/svg+xml")
|
||||
return Response(
|
||||
content=str(cached.value),
|
||||
media_type="image/svg+xml",
|
||||
headers=_activity_headers(result),
|
||||
)
|
||||
|
||||
daily_totals = _daily_totals(result.merged)
|
||||
total = sum(daily_totals.values())
|
||||
@@ -236,7 +264,7 @@ async def activity_svg(
|
||||
)
|
||||
if cache is not None:
|
||||
cache.set_json(cache_key, svg)
|
||||
return Response(content=svg, media_type="image/svg+xml")
|
||||
return Response(content=svg, media_type="image/svg+xml", headers=_activity_headers(result))
|
||||
|
||||
|
||||
@app.get("/activity.png")
|
||||
@@ -256,7 +284,11 @@ async def activity_png(
|
||||
cached = cache.get_json(cache_key)
|
||||
if cached is not None:
|
||||
png_data = bytes.fromhex(str(cached.value))
|
||||
return Response(content=png_data, media_type="image/png")
|
||||
return Response(
|
||||
content=png_data,
|
||||
media_type="image/png",
|
||||
headers=_activity_headers(result),
|
||||
)
|
||||
|
||||
daily_totals = _daily_totals(result.merged)
|
||||
total = sum(daily_totals.values())
|
||||
@@ -275,4 +307,4 @@ async def activity_png(
|
||||
raise HTTPException(status_code=500, detail="PNG rendering failed") from exc
|
||||
if cache is not None:
|
||||
cache.set_json(cache_key, png_data.hex())
|
||||
return Response(content=png_data, media_type="image/png")
|
||||
return Response(content=png_data, media_type="image/png", headers=_activity_headers(result))
|
||||
|
||||
@@ -8,6 +8,31 @@ from app.main import ActivityResult, app
|
||||
from app.settings import get_settings
|
||||
|
||||
|
||||
def test_activity_json_reports_stale_header(monkeypatch) -> None:
|
||||
monkeypatch.setenv("GITHUB_USERNAME", "octocat")
|
||||
monkeypatch.setenv("GITEA_BASE_URL", "https://gitea.example.com")
|
||||
monkeypatch.setenv("GITEA_USERNAME", "octocat")
|
||||
get_settings.cache_clear()
|
||||
|
||||
async def fake_collect_merged_activity(settings, cache, options):
|
||||
return ActivityResult(
|
||||
merged={"2026-01-01": {"github": 1, "gitea": 2, "total": 3}},
|
||||
stale=True,
|
||||
from_date=date(2026, 1, 1),
|
||||
to_date=date(2026, 1, 1),
|
||||
days_count=1,
|
||||
)
|
||||
|
||||
monkeypatch.setattr("app.main.collect_merged_activity", fake_collect_merged_activity)
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/activity.json")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-activity-stale"] == "true"
|
||||
assert response.json()["stale"] is True
|
||||
|
||||
|
||||
def test_activity_svg_returns_svg_content_type(monkeypatch) -> None:
|
||||
monkeypatch.setenv("GITHUB_USERNAME", "octocat")
|
||||
monkeypatch.setenv("GITEA_BASE_URL", "https://gitea.example.com")
|
||||
@@ -35,4 +60,18 @@ def test_activity_svg_returns_svg_content_type(monkeypatch) -> None:
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("image/svg+xml")
|
||||
assert response.headers["x-activity-stale"] == "false"
|
||||
assert "<svg" in response.text
|
||||
|
||||
|
||||
def test_activity_json_rejects_year_and_days_together(monkeypatch) -> None:
|
||||
monkeypatch.setenv("GITHUB_USERNAME", "octocat")
|
||||
monkeypatch.setenv("GITEA_BASE_URL", "https://gitea.example.com")
|
||||
monkeypatch.setenv("GITEA_USERNAME", "octocat")
|
||||
get_settings.cache_clear()
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/activity.json?year=2026&days=30")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "Provide either year or days, not both"}
|
||||
|
||||
Reference in New Issue
Block a user