diff options
| author | Christian Cleberg <[email protected]> | 2026-04-09 19:45:45 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-09 19:45:45 -0500 |
| commit | acbff854f2da96bddcaede1385e7fefeba0fb34b (patch) | |
| tree | 48d4707cd3276d2825370f0793008a6ffcfff936 /src/srht_contrib/api/routes_contributions.py | |
| download | hutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.tar.gz hutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.tar.bz2 hutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.zip | |
initial commit
Diffstat (limited to 'src/srht_contrib/api/routes_contributions.py')
| -rw-r--r-- | src/srht_contrib/api/routes_contributions.py | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/src/srht_contrib/api/routes_contributions.py b/src/srht_contrib/api/routes_contributions.py new file mode 100644 index 0000000..0682aa6 --- /dev/null +++ b/src/srht_contrib/api/routes_contributions.py @@ -0,0 +1,78 @@ +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"], dependencies=[Depends(require_api_key)]) + + +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`.") + + [email protected]("/{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"), + 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) + return ContributionAggregator().build_calendar(db, canonical_actor, start, end) + + [email protected]("/{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"), + 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) + return ContributionAggregator().build_stats(db, canonical_actor, start, end) + + [email protected]("/poll", response_model=PollResponse) +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"]) |
