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/main.py · raw

  1from __future__ import annotations
  2
  3from collections.abc import AsyncIterator
  4from contextlib import asynccontextmanager
  5import logging
  6
  7from apscheduler.schedulers.background import BackgroundScheduler
  8from fastapi import FastAPI
  9from sqlalchemy import Engine
 10from sqlalchemy.orm import Session, sessionmaker
 11
 12from srht_contrib.api.routes_contributions import router as contributions_router
 13from srht_contrib.api.routes_health import router as health_router
 14from srht_contrib.api.routes_repositories import router as repositories_router
 15from srht_contrib.config import Settings, get_settings
 16from srht_contrib.db import make_engine, validate_db
 17from srht_contrib.jobs.poller import PollerService
 18from srht_contrib.logging import configure_logging
 19from srht_contrib.services.git import GitIngestionService
 20from srht_contrib.services.srht_client import SourceHutGraphQLClient
 21from srht_contrib.services.todo import TodoIngestionService
 22from srht_contrib.utils.identity import ActorIdentityResolver
 23
 24
 25logger = logging.getLogger(__name__)
 26
 27
 28def build_poller(settings: Settings) -> PollerService:
 29    todo_client = SourceHutGraphQLClient(settings.todo_srht_endpoint, settings.srht_token, request_delay=settings.srht_request_delay_seconds)
 30    git_client = SourceHutGraphQLClient(settings.git_srht_endpoint, settings.srht_token, request_delay=settings.srht_request_delay_seconds)
 31    todo_service = TodoIngestionService(todo_client, settings)
 32    git_service = GitIngestionService(git_client, settings)
 33    return PollerService(todo_service=todo_service, git_service=git_service, settings=settings)
 34
 35
 36def create_app(
 37    settings: Settings | None = None,
 38    *,
 39    engine: Engine | None = None,
 40    session_factory: sessionmaker[Session] | None = None,
 41    poller: PollerService | None = None,
 42) -> FastAPI:
 43    app_settings = settings or get_settings()
 44    configure_logging()
 45    app_engine = engine or make_engine(app_settings)
 46    app_session_factory = session_factory or sessionmaker(
 47        bind=app_engine,
 48        autoflush=False,
 49        autocommit=False,
 50        expire_on_commit=False,
 51    )
 52    actor_identity_resolver = ActorIdentityResolver(app_settings.actor_aliases_json)
 53
 54    @asynccontextmanager
 55    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
 56        validate_db(app_engine)
 57        app_poller = poller or build_poller(app_settings)
 58        app.state.poller = app_poller
 59        app.state.settings = app_settings
 60        app.state.engine = app_engine
 61        app.state.session_factory = app_session_factory
 62        app.state.actor_identity_resolver = actor_identity_resolver
 63        scheduler: BackgroundScheduler | None = None
 64        if app_settings.enable_scheduler:
 65            scheduler = BackgroundScheduler()
 66            scheduler.add_job(
 67                _scheduled_poll,
 68                "interval",
 69                seconds=app_settings.poll_interval_seconds,
 70                args=[app],
 71                id="srht-poller",
 72                replace_existing=True,
 73            )
 74            scheduler.start()
 75            _run_startup_poll(app)
 76        app.state.scheduler = scheduler
 77        try:
 78            yield
 79        finally:
 80            if scheduler is not None:
 81                scheduler.shutdown(wait=False)
 82            app_poller.todo_service.client.close()
 83            app_poller.git_service.client.close()
 84
 85    app = FastAPI(title=app_settings.app_name, lifespan=lifespan)
 86
 87    app.include_router(health_router)
 88    app.include_router(contributions_router)
 89    app.include_router(repositories_router)
 90    return app
 91
 92
 93def _scheduled_poll(app: FastAPI) -> None:
 94    settings: Settings = app.state.settings
 95    poller: PollerService = app.state.poller
 96    session_factory: sessionmaker[Session] = app.state.session_factory
 97    db = session_factory()
 98    try:
 99        poller.poll_tracked_actors(db, settings.default_actor)
100    finally:
101        db.close()
102
103
104def _run_startup_poll(app: FastAPI) -> None:
105    try:
106        _scheduled_poll(app)
107    except Exception:
108        logger.exception("Initial scheduled poll failed during application startup")
109
110
111app = create_app()