generated from john/python-template
Quality Gate / gate (push) Failing after 47s
The pre-commit hooks declared `language: system` with bare `ruff`/`ty` entries, but both are uv-managed dev dependencies and are not on PATH, so every commit failed with `Executable 'ruff' not found`. Route both through `uv run`; keep ruff blocking and make ty advisory (verbose) until its 18 whole-project diagnostics are cleared. With the gate working, clear `ruff check .` to zero: - 18 auto-fixes (import sorting, blank lines, `max()` simplification, `with` merging, unused imports). - Real defects: `SourceNavigation` annotated but never imported in sources_page; two naive `datetime.now()` calls in migration.py now use `datetime.now(UTC)`. - Dead parameters removed: `source_has_photo_table` (computed, passed, never read), `_serialize_value(key=...)`, and unused `request` on two NiceGUI page handlers where the framework injects it optionally. - Mechanical line-length wrapping and one `startswith` tuple collapse. - `# noqa: PLR0915` / `# noqa: PLR1702` on five long UI/migration functions, following the convention already used in jobs_page and settings_page, rather than refactoring during stabilization. Full suite green (377 tests, `-m "not external"`). Co-authored-by: Copilot App <[email protected]>
109 lines
4.1 KiB
Python
109 lines
4.1 KiB
Python
"""Application factory and lifespan wiring for the transcription app."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import AsyncExitStack
|
|
from contextlib import asynccontextmanager
|
|
from datetime import UTC
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi import status
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from .api.documents_api import router as documents_router
|
|
from .api.errors import register_error_handlers
|
|
from .api.health import router as health_router
|
|
from .api.print_api import router as print_router
|
|
from .config import Settings
|
|
from .config import configure_logging
|
|
from .config import get_settings
|
|
from .db import create_all
|
|
from .db import dispose_database_runtime
|
|
from .db import initialize_database_runtime
|
|
from .db import reconcile_canonical_media_paths
|
|
from .db import reconcile_legacy_job_source_columns
|
|
from .services import ServiceBundle
|
|
from .ui import register_pages
|
|
from .worker import worker_consumer_lifespan
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def _lifespan(app: FastAPI):
|
|
settings = getattr(app.state, "settings", None) or get_settings()
|
|
configure_logging(settings)
|
|
app.state.settings = settings
|
|
app.state.runtime = initialize_database_runtime(settings=settings)
|
|
session_factory = app.state.runtime.session_factory
|
|
app.state.services = ServiceBundle.from_session_factory(session_factory, settings=settings)
|
|
|
|
if settings.should_bootstrap_schema:
|
|
await create_all(engine=app.state.runtime.engine)
|
|
await reconcile_legacy_job_source_columns(engine=app.state.runtime.engine)
|
|
await reconcile_canonical_media_paths(engine=app.state.runtime.engine)
|
|
|
|
settings.upload_dir.mkdir(parents=True, exist_ok=True)
|
|
settings.prompt_dir.mkdir(parents=True, exist_ok=True)
|
|
settings.log_dir.mkdir(parents=True, exist_ok=True)
|
|
settings.database_backup_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
await _recover_stale_processing_jobs(app)
|
|
|
|
async with AsyncExitStack() as stack:
|
|
stack.push_async_callback(dispose_database_runtime)
|
|
stop_event, worker_notifier, worker_health = await stack.enter_async_context(
|
|
worker_consumer_lifespan(
|
|
session_factory=app.state.runtime.session_factory,
|
|
poll_interval_seconds=1.0,
|
|
)
|
|
)
|
|
app.state.worker_stop_event = stop_event
|
|
app.state.worker_notifier = worker_notifier
|
|
app.state.worker_health = worker_health
|
|
yield
|
|
|
|
|
|
async def _recover_stale_processing_jobs(app: FastAPI) -> None:
|
|
"""Re-queue stale processing jobs at startup.
|
|
|
|
Any job left in PROCESSING longer than the configured provider timeout is
|
|
assumed orphaned and moved back to QUEUED before the worker starts.
|
|
"""
|
|
settings = app.state.settings
|
|
stale_before = datetime.now(UTC) - timedelta(seconds=settings.worker_provider_timeout_seconds)
|
|
recovered = await app.state.services.jobs.requeue_stale_processing_jobs(stale_before=stale_before)
|
|
if recovered > 0:
|
|
logger.warning("Recovered %s stale processing job(s) at startup", recovered)
|
|
|
|
|
|
def create_app(settings: Settings | None = None) -> FastAPI:
|
|
"""Create and configure the FastAPI application."""
|
|
app = FastAPI(title="Transcription", lifespan=_lifespan)
|
|
active_settings = settings or get_settings()
|
|
app.state.settings = active_settings
|
|
app.mount(
|
|
"/uploads",
|
|
StaticFiles(directory=active_settings.upload_dir, check_dir=False),
|
|
name="uploads",
|
|
)
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def root_redirect() -> RedirectResponse:
|
|
return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
|
|
|
@app.get("/ui", include_in_schema=False)
|
|
async def ui_redirect() -> RedirectResponse:
|
|
return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
|
|
|
register_error_handlers(app)
|
|
app.include_router(health_router)
|
|
app.include_router(documents_router)
|
|
app.include_router(print_router)
|
|
register_pages(app)
|
|
return app
|