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