Files
transcription/src/transcription/app.py
T
Jim LancasterandCopilot App 67feeb28af
Quality Gate / gate (push) Failing after 47s
Repair the pre-commit quality gate and clear the ruff backlog
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]>
2026-08-23 16:47:11 -05:00

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