summaryrefslogtreecommitdiff
path: root/src/srht_contrib/api/routes_contributions.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/srht_contrib/api/routes_contributions.py')
-rw-r--r--src/srht_contrib/api/routes_contributions.py78
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"])