aboutsummaryrefslogtreecommitdiff
path: root/src/srht_contrib/main.py
blob: d8284d60e6ddd3db921c434e36ae29c4adef68e7 (plain) (blame)
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from __future__ import annotations

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import logging

from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import FastAPI
from sqlalchemy import Engine
from sqlalchemy.orm import Session, sessionmaker

from srht_contrib.api.routes_contributions import router as contributions_router
from srht_contrib.api.routes_health import router as health_router
from srht_contrib.api.routes_repositories import router as repositories_router
from srht_contrib.config import Settings, get_settings
from srht_contrib.db import make_engine, validate_db
from srht_contrib.jobs.poller import PollerService
from srht_contrib.logging import configure_logging
from srht_contrib.services.git import GitIngestionService
from srht_contrib.services.srht_client import SourceHutGraphQLClient
from srht_contrib.services.todo import TodoIngestionService
from srht_contrib.utils.identity import ActorIdentityResolver


logger = logging.getLogger(__name__)


def build_poller(settings: Settings) -> PollerService:
    todo_client = SourceHutGraphQLClient(settings.todo_srht_endpoint, settings.srht_token, request_delay=settings.srht_request_delay_seconds)
    git_client = SourceHutGraphQLClient(settings.git_srht_endpoint, settings.srht_token, request_delay=settings.srht_request_delay_seconds)
    todo_service = TodoIngestionService(todo_client, settings)
    git_service = GitIngestionService(git_client, settings)
    return PollerService(todo_service=todo_service, git_service=git_service, settings=settings)


def create_app(
    settings: Settings | None = None,
    *,
    engine: Engine | None = None,
    session_factory: sessionmaker[Session] | None = None,
    poller: PollerService | None = None,
) -> FastAPI:
    app_settings = settings or get_settings()
    configure_logging()
    app_engine = engine or make_engine(app_settings)
    app_session_factory = session_factory or sessionmaker(
        bind=app_engine,
        autoflush=False,
        autocommit=False,
        expire_on_commit=False,
    )
    actor_identity_resolver = ActorIdentityResolver(app_settings.actor_aliases_json)

    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
        validate_db(app_engine)
        app_poller = poller or build_poller(app_settings)
        app.state.poller = app_poller
        app.state.settings = app_settings
        app.state.engine = app_engine
        app.state.session_factory = app_session_factory
        app.state.actor_identity_resolver = actor_identity_resolver
        scheduler: BackgroundScheduler | None = None
        if app_settings.enable_scheduler:
            scheduler = BackgroundScheduler()
            scheduler.add_job(
                _scheduled_poll,
                "interval",
                seconds=app_settings.poll_interval_seconds,
                args=[app],
                id="srht-poller",
                replace_existing=True,
            )
            scheduler.start()
            _run_startup_poll(app)
        app.state.scheduler = scheduler
        try:
            yield
        finally:
            if scheduler is not None:
                scheduler.shutdown(wait=False)
            app_poller.todo_service.client.close()
            app_poller.git_service.client.close()

    app = FastAPI(title=app_settings.app_name, lifespan=lifespan)

    app.include_router(health_router)
    app.include_router(contributions_router)
    app.include_router(repositories_router)
    return app


def _scheduled_poll(app: FastAPI) -> None:
    settings: Settings = app.state.settings
    poller: PollerService = app.state.poller
    session_factory: sessionmaker[Session] = app.state.session_factory
    db = session_factory()
    try:
        poller.poll_tracked_actors(db, settings.default_actor)
    finally:
        db.close()


def _run_startup_poll(app: FastAPI) -> None:
    try:
        _scheduled_poll(app)
    except Exception:
        logger.exception("Initial scheduled poll failed during application startup")


app = create_app()