1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
from __future__ import annotations
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from srht_contrib.api.dependencies import get_actor_identity_resolver, get_db, get_poller, require_api_key
from srht_contrib.jobs.poller import PollerService
from srht_contrib.schemas import ContributionCalendarResponse, ContributionStatsResponse, PollResponse
from srht_contrib.services.aggregator import ContributionAggregator
from srht_contrib.services.srht_client import SourceHutClientError
from srht_contrib.utils.dates import parse_date, year_bounds
from srht_contrib.utils.identity import ActorIdentityResolver
router = APIRouter(prefix="/api/contributions", tags=["contributions"])
def _resolve_range(year: int | None, from_date: str | None, to_date: str | None) -> tuple[date, date]:
if year is not None and (from_date or to_date):
raise HTTPException(status_code=400, detail="Use either `year` or `from`/`to`, not both.")
try:
if year is not None:
return year_bounds(year)
if from_date and to_date:
start = parse_date(from_date)
end = parse_date(to_date)
if start > end:
raise HTTPException(status_code=400, detail="`from` must be on or before `to`.")
return start, end
except ValueError as exc:
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD.") from exc
raise HTTPException(status_code=400, detail="Provide `year` or both `from` and `to`.")
@router.get("/{actor}", response_model=ContributionCalendarResponse)
def get_contributions(
actor: str,
year: int | None = Query(default=None, ge=1970, le=3000),
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
poller: PollerService = Depends(get_poller),
db: Session = Depends(get_db),
actor_identity_resolver: ActorIdentityResolver = Depends(get_actor_identity_resolver),
) -> ContributionCalendarResponse:
start, end = _resolve_range(year, from_date, to_date)
canonical_actor = actor_identity_resolver.canonicalize(actor, db=db)
poller.track_actor_request(db, canonical_actor)
response = ContributionAggregator().build_calendar(db, canonical_actor, start, end)
db.commit()
return response
@router.get("/{actor}/stats", response_model=ContributionStatsResponse)
def get_contribution_stats(
actor: str,
year: int | None = Query(default=None, ge=1970, le=3000),
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
poller: PollerService = Depends(get_poller),
db: Session = Depends(get_db),
actor_identity_resolver: ActorIdentityResolver = Depends(get_actor_identity_resolver),
) -> ContributionStatsResponse:
start, end = _resolve_range(year, from_date, to_date)
canonical_actor = actor_identity_resolver.canonicalize(actor, db=db)
poller.track_actor_request(db, canonical_actor)
response = ContributionAggregator().build_stats(db, canonical_actor, start, end)
db.commit()
return response
@router.post("/poll", response_model=PollResponse, dependencies=[Depends(require_api_key)])
def trigger_manual_poll(
actor: str,
poller: PollerService = Depends(get_poller),
db: Session = Depends(get_db),
actor_identity_resolver: ActorIdentityResolver = Depends(get_actor_identity_resolver),
) -> PollResponse:
canonical_actor = actor_identity_resolver.canonicalize(actor, db=db)
try:
inserted = poller.poll_all(db, canonical_actor)
except SourceHutClientError as exc:
raise HTTPException(status_code=502, detail=f"SourceHut polling failed: {exc}") from exc
return PollResponse(actor=canonical_actor, inserted_events=inserted, services=["todo", "git"])
|