diff --git a/src/transcription/api/errors.py b/src/transcription/api/errors.py index 2d4990a..01bdd90 100644 --- a/src/transcription/api/errors.py +++ b/src/transcription/api/errors.py @@ -4,10 +4,13 @@ from __future__ import annotations import logging -from fastapi import FastAPI, Request +from fastapi import FastAPI +from fastapi import Request from fastapi.responses import JSONResponse -from transcription.errors import AppError, ErrorCategory, build_error_envelope +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.errors import build_error_envelope from transcription.security import AccessDeniedError logger = logging.getLogger(__name__) diff --git a/src/transcription/api/routes.py b/src/transcription/api/routes.py index 2a5cc77..60ba153 100644 --- a/src/transcription/api/routes.py +++ b/src/transcription/api/routes.py @@ -5,17 +5,16 @@ from __future__ import annotations from uuid import UUID from fastapi import APIRouter -from pydantic import BaseModel, Field +from pydantic import BaseModel +from pydantic import Field -from transcription.services.library import ( - accept_revision, - add_revision, - export_transcripts, - get_job_detail, - list_jobs, - list_revisions, - search_accepted_transcripts, -) +from transcription.services.library import accept_revision +from transcription.services.library import add_revision +from transcription.services.library import export_transcripts +from transcription.services.library import get_job_detail +from transcription.services.library import list_jobs +from transcription.services.library import list_revisions +from transcription.services.library import search_accepted_transcripts router = APIRouter(prefix="/api", tags=["transcription"]) diff --git a/src/transcription/app.py b/src/transcription/app.py index 84a9460..1ca49d3 100644 --- a/src/transcription/app.py +++ b/src/transcription/app.py @@ -3,25 +3,27 @@ from __future__ import annotations from contextlib import asynccontextmanager -from threading import Event, Thread +from threading import Event +from threading import Thread -from fastapi import FastAPI, Request +from fastapi import FastAPI +from fastapi import Request from fastapi.responses import JSONResponse from transcription.api.errors import register_error_handlers from transcription.api.health import router as health_router from transcription.api.routes import router as transcription_router -from transcription.config import get_settings, setup_logging -from transcription.db import ( - create_all, - dispose_database_runtime, - initialize_database_runtime, - should_bootstrap_schema, - validate_schema_compatibility, -) +from transcription.config import get_settings +from transcription.config import setup_logging +from transcription.db import create_all +from transcription.db import dispose_database_runtime +from transcription.db import initialize_database_runtime +from transcription.db import should_bootstrap_schema +from transcription.db import validate_schema_compatibility from transcription.errors import build_error_envelope from transcription.migrations import apply_pending_migrations -from transcription.security import AccessDeniedError, enforce_request_access +from transcription.security import AccessDeniedError +from transcription.security import enforce_request_access from transcription.ui import register_pages from transcription.worker import run_worker_loop @@ -104,4 +106,3 @@ def create_app() -> FastAPI: app.include_router(health_router) app.include_router(transcription_router) return app - diff --git a/src/transcription/config.py b/src/transcription/config.py index ea13748..d2179c0 100644 --- a/src/transcription/config.py +++ b/src/transcription/config.py @@ -5,7 +5,6 @@ once at startup. Provider-specific defaults (model names, base URLs) are resolved by the provider adapters, not here. """ -import logging import logging.config from enum import StrEnum from functools import lru_cache @@ -13,7 +12,8 @@ from pathlib import Path from typing import Literal from pydantic import model_validator -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic_settings import BaseSettings +from pydantic_settings import SettingsConfigDict class Provider(StrEnum): diff --git a/src/transcription/db.py b/src/transcription/db.py index 27fcd25..1c2fcb8 100644 --- a/src/transcription/db.py +++ b/src/transcription/db.py @@ -10,10 +10,12 @@ from dataclasses import dataclass from sqlalchemy import inspect from sqlalchemy.engine import Engine -from sqlmodel import Session, SQLModel, create_engine - -from transcription.config import Settings, get_settings +from sqlmodel import Session +from sqlmodel import SQLModel +from sqlmodel import create_engine +from transcription.config import Settings +from transcription.config import get_settings @dataclass(frozen=True) diff --git a/src/transcription/errors.py b/src/transcription/errors.py index 75d2f0f..66ce89a 100644 --- a/src/transcription/errors.py +++ b/src/transcription/errors.py @@ -3,7 +3,8 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC +from datetime import datetime from enum import StrEnum from uuid import uuid4 @@ -64,7 +65,7 @@ def build_error_envelope(error: AppError) -> ErrorEnvelope: category=error.category.value, message=error.message, suggestion=error.suggestion, - timestamp=datetime.now(timezone.utc).isoformat(), + timestamp=datetime.now(UTC).isoformat(), ) @@ -84,4 +85,4 @@ def format_error_detail(error: AppError) -> str: return ( f"[{error.category.value}] {error.message} | " f"suggestion={error.suggestion} | error_id={error.error_id}" - ) \ No newline at end of file + ) diff --git a/src/transcription/migration_runner.py b/src/transcription/migration_runner.py index 97adf2e..826d88c 100644 --- a/src/transcription/migration_runner.py +++ b/src/transcription/migration_runner.py @@ -3,11 +3,12 @@ from __future__ import annotations import argparse -import sys from transcription.config import get_settings -from transcription.db import initialize_database_runtime, validate_schema_compatibility -from transcription.migrations import apply_pending_migrations, list_pending_migrations +from transcription.db import initialize_database_runtime +from transcription.db import validate_schema_compatibility +from transcription.migrations import apply_pending_migrations +from transcription.migrations import list_pending_migrations def _build_parser() -> argparse.ArgumentParser: @@ -71,4 +72,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/src/transcription/migrations.py b/src/transcription/migrations.py index 9d06d17..40737b3 100644 --- a/src/transcription/migrations.py +++ b/src/transcription/migrations.py @@ -6,13 +6,16 @@ personal-scale deployments without introducing heavyweight migration tooling. from __future__ import annotations -from dataclasses import dataclass -from datetime import datetime, timezone import logging -from typing import Callable +from collections.abc import Callable +from dataclasses import dataclass +from datetime import UTC +from datetime import datetime -from sqlalchemy import inspect, text -from sqlalchemy.engine import Connection, Engine +from sqlalchemy import inspect +from sqlalchemy import text +from sqlalchemy.engine import Connection +from sqlalchemy.engine import Engine logger = logging.getLogger(__name__) @@ -60,7 +63,7 @@ def _record_revision(connection: Connection, revision: MigrationRevision) -> Non { "revision_id": revision.revision_id, "description": revision.description, - "applied_at": datetime.now(timezone.utc).isoformat(), + "applied_at": datetime.now(UTC).isoformat(), }, ) @@ -83,9 +86,10 @@ def _apply_0001_add_retry_count(connection: Connection) -> None: def _apply_0002_create_transcriptrevision(connection: Connection) -> None: """Ensure transcriptrevision table exists.""" # Import models lazily so metadata is fully populated. - from transcription.models import TranscriptRevision # noqa: F401 from sqlmodel import SQLModel + from transcription.models import TranscriptRevision # noqa: F401 + table = SQLModel.metadata.tables["transcriptrevision"] table.create(bind=connection, checkfirst=True) @@ -125,4 +129,4 @@ def apply_pending_migrations(*, engine: Engine) -> list[str]: applied_ids.append(revision.revision_id) logger.info("Applied migration revision=%s", revision.revision_id) - return applied_ids \ No newline at end of file + return applied_ids diff --git a/src/transcription/models.py b/src/transcription/models.py index 3a48c17..8ecf2cd 100644 --- a/src/transcription/models.py +++ b/src/transcription/models.py @@ -1,11 +1,15 @@ """SQLModel domain models for the transcription system.""" -from datetime import datetime, timezone +from datetime import UTC +from datetime import datetime from enum import StrEnum from typing import Optional -from uuid import UUID, uuid4 +from uuid import UUID +from uuid import uuid4 -from sqlmodel import Field, Relationship, SQLModel +from sqlmodel import Field +from sqlmodel import Relationship +from sqlmodel import SQLModel class JobStatus(StrEnum): @@ -22,7 +26,7 @@ class Document(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) filename: str file_path: str - uploaded_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + uploaded_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # --- relationships --- jobs: list["Job"] = Relationship(back_populates="document") @@ -35,8 +39,8 @@ class Job(SQLModel, table=True): document_id: UUID = Field(foreign_key="document.id") status: JobStatus = Field(default=JobStatus.QUEUED) retry_count: int = Field(default=0, ge=0) - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # --- relationships --- document: Document = Relationship(back_populates="jobs") @@ -51,7 +55,7 @@ class Transcript(SQLModel, table=True): job_id: UUID = Field(foreign_key="job.id", unique=True) text: str | None = None error_detail: str | None = None - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # --- relationships --- job: Job = Relationship(back_populates="transcript") @@ -66,7 +70,7 @@ class TranscriptRevision(SQLModel, table=True): text: str source: str = Field(default="worker") accepted: bool = Field(default=False) - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # --- relationships --- job: Job = Relationship(back_populates="revisions") diff --git a/src/transcription/providers/__init__.py b/src/transcription/providers/__init__.py index 6a5c042..318e40a 100644 --- a/src/transcription/providers/__init__.py +++ b/src/transcription/providers/__init__.py @@ -1,13 +1,13 @@ """Provider exports and factory for transcription adapters.""" -from transcription.config import Provider, Settings, get_settings -from transcription.providers.base import ( - ProviderAuthError, - ProviderError, - ProviderResponseError, - TranscriptionProvider, - TranscriptionResult, -) +from transcription.config import Provider +from transcription.config import Settings +from transcription.config import get_settings +from transcription.providers.base import ProviderAuthError +from transcription.providers.base import ProviderError +from transcription.providers.base import ProviderResponseError +from transcription.providers.base import TranscriptionProvider +from transcription.providers.base import TranscriptionResult from transcription.providers.openrouter import OpenRouterTranscriptionProvider @@ -21,11 +21,11 @@ def get_transcription_provider(*, settings: Settings | None = None) -> Transcrip __all__ = [ + "OpenRouterTranscriptionProvider", "ProviderAuthError", "ProviderError", "ProviderResponseError", "TranscriptionProvider", "TranscriptionResult", - "OpenRouterTranscriptionProvider", "get_transcription_provider", ] diff --git a/src/transcription/providers/openrouter.py b/src/transcription/providers/openrouter.py index a990b4b..e8ec702 100644 --- a/src/transcription/providers/openrouter.py +++ b/src/transcription/providers/openrouter.py @@ -9,13 +9,12 @@ from typing import Any from openrouter import OpenRouter -from transcription.config import Settings, get_settings -from transcription.providers.base import ( - ProviderAuthError, - ProviderError, - ProviderResponseError, - TranscriptionResult, -) +from transcription.config import Settings +from transcription.config import get_settings +from transcription.providers.base import ProviderAuthError +from transcription.providers.base import ProviderError +from transcription.providers.base import ProviderResponseError +from transcription.providers.base import TranscriptionResult logger = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class OpenRouterTranscriptionProvider: http_referer=request.http_referer, x_open_router_title=request.x_open_router_title, ) - except Exception as exc: # noqa: BLE001 + except Exception as exc: message = str(exc).lower() if "401" in message or "auth" in message or "api key" in message: raise ProviderAuthError("OpenRouter authentication failed") from exc @@ -111,10 +110,7 @@ class OpenRouterTranscriptionProvider: parts: list[str] = [] for item in content: text_part = None - if isinstance(item, dict): - text_part = item.get("text") - else: - text_part = self._get_optional_attr(item, "text") + text_part = item.get("text") if isinstance(item, dict) else self._get_optional_attr(item, "text") if isinstance(text_part, str) and text_part.strip(): parts.append(text_part.strip()) diff --git a/src/transcription/security.py b/src/transcription/security.py index f706cfe..35bb7ec 100644 --- a/src/transcription/security.py +++ b/src/transcription/security.py @@ -9,7 +9,8 @@ import secrets from fastapi import Request from transcription.config import Settings -from transcription.errors import AppError, ErrorCategory +from transcription.errors import AppError +from transcription.errors import ErrorCategory class AccessDeniedError(AppError): @@ -22,7 +23,7 @@ class AccessDeniedError(AppError): def is_protected_path(path: str) -> bool: """Return True when a request path requires operator authentication.""" - return path == "/ui" or path.startswith("/ui/") or path.startswith("/api") + return path == "/ui" or path.startswith(("/ui/", "/api")) def enforce_request_access(*, request: Request, settings: Settings) -> None: @@ -78,4 +79,4 @@ def _parse_basic_authorization_header(value: str | None) -> tuple[str, str]: suggestion="Provide HTTP Basic operator credentials and retry.", ) - return username, password \ No newline at end of file + return username, password diff --git a/src/transcription/services/__init__.py b/src/transcription/services/__init__.py index d1f3d7d..fb41a54 100644 --- a/src/transcription/services/__init__.py +++ b/src/transcription/services/__init__.py @@ -1,30 +1,25 @@ """Service layer exports.""" -from transcription.services.transcription import ( - DEFAULT_PROMPT_FILE, - PromptLoadError, - TranscriptionError, - load_image_payload, - load_prompt_text, - transcribe_document_image, -) -from transcription.services.upload import ( - SUPPORTED_UPLOAD_EXTENSIONS, - UploadError, - UploadJobResult, - create_upload_job, -) +from transcription.services.transcription import DEFAULT_PROMPT_FILE +from transcription.services.transcription import PromptLoadError +from transcription.services.transcription import TranscriptionError +from transcription.services.transcription import load_image_payload +from transcription.services.transcription import load_prompt_text +from transcription.services.transcription import transcribe_document_image +from transcription.services.upload import SUPPORTED_UPLOAD_EXTENSIONS +from transcription.services.upload import UploadError +from transcription.services.upload import UploadJobResult +from transcription.services.upload import create_upload_job __all__ = [ "DEFAULT_PROMPT_FILE", + "SUPPORTED_UPLOAD_EXTENSIONS", "PromptLoadError", "TranscriptionError", - "load_image_payload", - "load_prompt_text", - "transcribe_document_image", - "SUPPORTED_UPLOAD_EXTENSIONS", "UploadError", "UploadJobResult", "create_upload_job", + "load_image_payload", + "load_prompt_text", + "transcribe_document_image", ] - diff --git a/src/transcription/services/library.py b/src/transcription/services/library.py index 2743798..5753205 100644 --- a/src/transcription/services/library.py +++ b/src/transcription/services/library.py @@ -3,14 +3,21 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC +from datetime import datetime from uuid import UUID -from sqlmodel import Session, select +from sqlmodel import Session +from sqlmodel import select from transcription.db import get_session -from transcription.errors import AppError, ErrorCategory -from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript +from transcription.models import TranscriptRevision class LibraryError(AppError): @@ -132,7 +139,7 @@ def add_revision( transcript.error_detail = None session.add(transcript) - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) if accepted: job.status = JobStatus.COMPLETED elif job.status == JobStatus.QUEUED: @@ -174,7 +181,7 @@ def accept_revision(*, revision_id: UUID, session: Session | None = None) -> Tra job = session.get(Job, revision.job_id) if job is not None: job.status = JobStatus.COMPLETED - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) session.add(job) session.commit() diff --git a/src/transcription/services/transcription.py b/src/transcription/services/transcription.py index 964466a..0cb14f0 100644 --- a/src/transcription/services/transcription.py +++ b/src/transcription/services/transcription.py @@ -6,16 +6,16 @@ import logging import mimetypes from pathlib import Path -from transcription.config import Settings, get_settings -from transcription.errors import AppError, ErrorCategory -from transcription.providers import ( - ProviderAuthError, - ProviderError, - ProviderResponseError, - TranscriptionProvider, - TranscriptionResult, - get_transcription_provider, -) +from transcription.config import Settings +from transcription.config import get_settings +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.providers import ProviderAuthError +from transcription.providers import ProviderError +from transcription.providers import ProviderResponseError +from transcription.providers import TranscriptionProvider +from transcription.providers import TranscriptionResult +from transcription.providers import get_transcription_provider logger = logging.getLogger(__name__) diff --git a/src/transcription/services/upload.py b/src/transcription/services/upload.py index b571481..f740482 100644 --- a/src/transcription/services/upload.py +++ b/src/transcription/services/upload.py @@ -5,14 +5,19 @@ from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path -from uuid import UUID, uuid4 +from uuid import UUID +from uuid import uuid4 from sqlmodel import Session -from transcription.config import Settings, get_settings +from transcription.config import Settings +from transcription.config import get_settings from transcription.db import get_session -from transcription.errors import AppError, ErrorCategory -from transcription.models import Document, Job, JobStatus +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus logger = logging.getLogger(__name__) @@ -73,7 +78,7 @@ def create_upload_job( original_filename=filename, stored_path=stored_path, ) - except Exception as exc: # noqa: BLE001 + except Exception as exc: _best_effort_delete(stored_path) raise UploadError( "Failed to create upload database records", @@ -152,4 +157,4 @@ def _best_effort_delete(path: Path) -> None: if path.exists(): path.unlink() except OSError: - logger.warning("Failed to clean up upload file after DB error: %s", path) \ No newline at end of file + logger.warning("Failed to clean up upload file after DB error: %s", path) diff --git a/src/transcription/ui/__init__.py b/src/transcription/ui/__init__.py index b3487f3..016ed7a 100644 --- a/src/transcription/ui/__init__.py +++ b/src/transcription/ui/__init__.py @@ -12,5 +12,3 @@ def register_pages(app: FastAPI) -> None: register_upload_page() register_jobs_page() ui.run_with(app, mount_path="/ui", show_welcome_message=False) - - diff --git a/src/transcription/ui/error_presenter.py b/src/transcription/ui/error_presenter.py index f3edebd..06148a9 100644 --- a/src/transcription/ui/error_presenter.py +++ b/src/transcription/ui/error_presenter.py @@ -4,7 +4,9 @@ from __future__ import annotations from nicegui import ui -from transcription.errors import AppError, ErrorCategory, classify_unexpected_error +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.errors import classify_unexpected_error def to_app_error(exc: Exception, *, operation: str) -> AppError: @@ -37,4 +39,4 @@ def summarize_error(exc: Exception, *, operation: str) -> str: error = to_app_error(exc, operation=operation) if error.category == ErrorCategory.INTERNAL_UNEXPECTED: return f"Unexpected error (ref: {error.error_id})" - return f"{error.message} (ref: {error.error_id})" \ No newline at end of file + return f"{error.message} (ref: {error.error_id})" diff --git a/src/transcription/ui/jobs_page.py b/src/transcription/ui/jobs_page.py index 09b02e3..a7af74e 100644 --- a/src/transcription/ui/jobs_page.py +++ b/src/transcription/ui/jobs_page.py @@ -9,15 +9,16 @@ from nicegui import ui from sqlmodel import select from transcription.db import get_session -from transcription.models import Document, Job, Transcript -from transcription.services.library import ( - accept_revision, - add_revision, - export_transcripts, - list_revisions, - search_accepted_transcripts, -) -from transcription.ui.error_presenter import show_error, summarize_error +from transcription.models import Document +from transcription.models import Job +from transcription.models import Transcript +from transcription.services.library import accept_revision +from transcription.services.library import add_revision +from transcription.services.library import export_transcripts +from transcription.services.library import list_revisions +from transcription.services.library import search_accepted_transcripts +from transcription.ui.error_presenter import show_error +from transcription.ui.error_presenter import summarize_error @dataclass(frozen=True) diff --git a/src/transcription/ui/upload_page.py b/src/transcription/ui/upload_page.py index cdf6831..f0e00d1 100644 --- a/src/transcription/ui/upload_page.py +++ b/src/transcription/ui/upload_page.py @@ -7,8 +7,11 @@ from dataclasses import dataclass from nicegui import ui from nicegui.events import UploadEventArguments -from transcription.services.upload import UploadError, UploadJobResult, create_upload_job -from transcription.ui.error_presenter import show_error, summarize_error +from transcription.services.upload import UploadError +from transcription.services.upload import UploadJobResult +from transcription.services.upload import create_upload_job +from transcription.ui.error_presenter import show_error +from transcription.ui.error_presenter import summarize_error @dataclass diff --git a/src/transcription/worker.py b/src/transcription/worker.py index 1e102e7..833a65e 100644 --- a/src/transcription/worker.py +++ b/src/transcription/worker.py @@ -4,17 +4,26 @@ from __future__ import annotations import logging import time -from datetime import datetime, timezone +from datetime import UTC +from datetime import datetime from threading import Event from pydantic import ValidationError from sqlalchemy.engine import Engine -from sqlmodel import Session, select +from sqlmodel import Session +from sqlmodel import select -from transcription.config import Settings, get_settings +from transcription.config import Settings +from transcription.config import get_settings from transcription.db import get_session -from transcription.errors import AppError, ErrorCategory, classify_unexpected_error, format_error_detail -from transcription.models import Document, Job, JobStatus, Transcript +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.errors import classify_unexpected_error +from transcription.errors import format_error_detail +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript from transcription.services.library import add_revision from transcription.services.transcription import transcribe_document_image @@ -44,7 +53,7 @@ def _process_next_queued_job(*, session: Session) -> bool: logger.info("Picked queued job operation=worker.pick job_id=%s", job.id) job.status = JobStatus.PROCESSING - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) session.add(job) session.commit() session.refresh(job) @@ -75,7 +84,7 @@ def _process_next_queued_job(*, session: Session) -> bool: session=session, ) job.status = JobStatus.TRANSCRIBED - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) session.add(job) session.commit() logger.info( @@ -85,7 +94,7 @@ def _process_next_queued_job(*, session: Session) -> bool: result.provider, revision.revision_number, ) - except Exception as exc: # noqa: BLE001 + except Exception as exc: error = exc if isinstance(exc, AppError) else classify_unexpected_error(exc, operation="worker.process_job") settings = _get_worker_settings() if _should_retry(job=job, error=error, settings=settings): @@ -139,7 +148,7 @@ def _requeue_for_retry(*, session: Session, job: Job, error: AppError, settings: _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error)) job.retry_count += 1 job.status = JobStatus.QUEUED - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) session.add(job) session.commit() if settings.worker_retry_backoff_seconds > 0: @@ -149,7 +158,7 @@ def _requeue_for_retry(*, session: Session, job: Job, error: AppError, settings: def _finalize_failed_job(*, session: Session, job: Job, error: AppError) -> None: _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error)) job.status = JobStatus.FAILED - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) session.add(job) session.commit() diff --git a/tests/api/test_access_control.py b/tests/api/test_access_control.py index 6409bdc..a54dae0 100644 --- a/tests/api/test_access_control.py +++ b/tests/api/test_access_control.py @@ -5,18 +5,19 @@ from __future__ import annotations import base64 from types import SimpleNamespace +import pytest from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient -import pytest from transcription.api.errors import register_error_handlers from transcription.errors import build_error_envelope -from transcription.security import AccessDeniedError, enforce_request_access +from transcription.security import AccessDeniedError +from transcription.security import enforce_request_access def _basic_header(username: str, password: str) -> str: - token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") + token = base64.b64encode(f"{username}:{password}".encode()).decode("ascii") return f"Basic {token}" @@ -125,4 +126,4 @@ class TestAccessControl: response = client.get("/healthz") assert response.status_code == 200 - assert response.json() == {"status": "ok"} \ No newline at end of file + assert response.json() == {"status": "ok"} diff --git a/tests/api/test_error_responses.py b/tests/api/test_error_responses.py index 03535a8..be33da4 100644 --- a/tests/api/test_error_responses.py +++ b/tests/api/test_error_responses.py @@ -1,11 +1,12 @@ """Tests for API error response envelope handlers.""" +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -import pytest from transcription.api.errors import register_error_handlers -from transcription.errors import AppError, ErrorCategory +from transcription.errors import AppError +from transcription.errors import ErrorCategory @pytest.mark.integration diff --git a/tests/api/test_routes.py b/tests/api/test_routes.py index 4ef8348..a3b9a06 100644 --- a/tests/api/test_routes.py +++ b/tests/api/test_routes.py @@ -1,12 +1,13 @@ """Tests for Step 3 functional API routes.""" -from datetime import datetime, timezone +from datetime import UTC +from datetime import datetime from types import SimpleNamespace from uuid import uuid4 +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -import pytest from transcription.api.errors import register_error_handlers from transcription.api.routes import router @@ -25,7 +26,7 @@ class TestFunctionalRoutes: def test_get_jobs_returns_serialized_rows(self, monkeypatch): """GET /api/jobs returns normalized job rows.""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) job = SimpleNamespace( id=uuid4(), document_id=uuid4(), @@ -53,7 +54,7 @@ class TestFunctionalRoutes: text="edited text", source="user", accepted=False, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) monkeypatch.setattr("transcription.api.routes.add_revision", lambda **_kwargs: revision) @@ -78,7 +79,7 @@ class TestFunctionalRoutes: text="family archive", source="user", accepted=True, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) monkeypatch.setattr("transcription.api.routes.search_accepted_transcripts", lambda query: [result]) @@ -103,7 +104,7 @@ class TestFunctionalRoutes: "accepted": True, "source": "user", "text": "exported", - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), } ] monkeypatch.setattr("transcription.api.routes.export_transcripts", lambda accepted_only=True: records) diff --git a/tests/conftest.py b/tests/conftest.py index 5a37231..49cdd1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,9 @@ isolated, fast, and leave no artifacts on disk. """ import pytest -from sqlmodel import Session, SQLModel, create_engine +from sqlmodel import Session +from sqlmodel import SQLModel +from sqlmodel import create_engine from sqlmodel.pool import StaticPool diff --git a/tests/integration/test_pipeline_flow.py b/tests/integration/test_pipeline_flow.py index 7f81ed2..9f401f3 100644 --- a/tests/integration/test_pipeline_flow.py +++ b/tests/integration/test_pipeline_flow.py @@ -6,7 +6,9 @@ import pytest from sqlmodel import select from transcription.config import Settings -from transcription.models import Job, JobStatus, Transcript +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript from transcription.providers.base import TranscriptionResult from transcription.services.upload import create_upload_job from transcription.worker import process_next_queued_job diff --git a/tests/providers/test_openrouter.py b/tests/providers/test_openrouter.py index 027daa2..994676e 100644 --- a/tests/providers/test_openrouter.py +++ b/tests/providers/test_openrouter.py @@ -5,8 +5,10 @@ from types import SimpleNamespace import pytest from transcription.config import Settings -from transcription.providers.base import ProviderError, ProviderResponseError -from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL, OpenRouterTranscriptionProvider +from transcription.providers.base import ProviderError +from transcription.providers.base import ProviderResponseError +from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL +from transcription.providers.openrouter import OpenRouterTranscriptionProvider class _FakeChat: diff --git a/tests/services/test_library.py b/tests/services/test_library.py index 8d2a4a7..571e799 100644 --- a/tests/services/test_library.py +++ b/tests/services/test_library.py @@ -1,10 +1,18 @@ """Tests for Step 3 library services (revisions, search, export).""" -from sqlmodel import select import pytest +from sqlmodel import select -from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision -from transcription.services.library import accept_revision, add_revision, export_transcripts, list_revisions, search_accepted_transcripts +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript +from transcription.models import TranscriptRevision +from transcription.services.library import accept_revision +from transcription.services.library import add_revision +from transcription.services.library import export_transcripts +from transcription.services.library import list_revisions +from transcription.services.library import search_accepted_transcripts def _create_job(session) -> Job: diff --git a/tests/services/test_transcription.py b/tests/services/test_transcription.py index c98059a..ba5fd0b 100644 --- a/tests/services/test_transcription.py +++ b/tests/services/test_transcription.py @@ -5,14 +5,13 @@ from pathlib import Path import pytest from transcription.config import Settings -from transcription.providers.base import ProviderError, TranscriptionResult -from transcription.services.transcription import ( - PromptLoadError, - TranscriptionError, - load_image_payload, - load_prompt_text, - transcribe_document_image, -) +from transcription.providers.base import ProviderError +from transcription.providers.base import TranscriptionResult +from transcription.services.transcription import PromptLoadError +from transcription.services.transcription import TranscriptionError +from transcription.services.transcription import load_image_payload +from transcription.services.transcription import load_prompt_text +from transcription.services.transcription import transcribe_document_image class _FakeProvider: diff --git a/tests/services/test_transcription_external.py b/tests/services/test_transcription_external.py index 7cb3aed..9838912 100644 --- a/tests/services/test_transcription_external.py +++ b/tests/services/test_transcription_external.py @@ -7,7 +7,6 @@ import pytest from transcription.services.transcription import transcribe_document_image - HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY")) REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real" @@ -67,4 +66,4 @@ class TestRealImageExternalTranscription: f"{result.text}\n" ) artifact_path.write_text(artifact_text, encoding="utf-8") - assert artifact_path.exists() \ No newline at end of file + assert artifact_path.exists() diff --git a/tests/services/test_upload.py b/tests/services/test_upload.py index 2bc7d7c..dea60c6 100644 --- a/tests/services/test_upload.py +++ b/tests/services/test_upload.py @@ -5,8 +5,11 @@ from pathlib import Path import pytest from transcription.config import Settings -from transcription.models import Document, Job, JobStatus -from transcription.services.upload import UploadError, create_upload_job +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.services.upload import UploadError +from transcription.services.upload import create_upload_job @pytest.mark.unit diff --git a/tests/services/test_worker.py b/tests/services/test_worker.py index 2c723a9..d853d4b 100644 --- a/tests/services/test_worker.py +++ b/tests/services/test_worker.py @@ -7,10 +7,16 @@ import pytest from sqlmodel import select from transcription.config import Settings -from transcription.errors import AppError, ErrorCategory -from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript +from transcription.models import TranscriptRevision from transcription.providers.base import TranscriptionResult -from transcription.worker import process_next_queued_job, run_worker_loop +from transcription.worker import process_next_queued_job +from transcription.worker import run_worker_loop def _create_queued_job(session, *, filename: str = "doc.jpg", file_path: str = "uploads/doc.jpg") -> Job: diff --git a/tests/test_config.py b/tests/test_config.py index 360f79e..a0dfee7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,8 @@ from pathlib import Path import pytest from pydantic import ValidationError -from transcription.config import Provider, Settings +from transcription.config import Provider +from transcription.config import Settings def _make_settings(**overrides) -> Settings: diff --git a/tests/test_db.py b/tests/test_db.py index dbea161..fd5eda8 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,7 +1,10 @@ """Tests for transcription.db — schema bootstrap and session factory.""" -from sqlalchemy import inspect, text -from sqlmodel import Session, SQLModel, create_engine +from sqlalchemy import inspect +from sqlalchemy import text +from sqlmodel import Session +from sqlmodel import SQLModel +from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -21,9 +24,11 @@ class TestSchemaBootstrap: """After create_all(), core V1 tables exist.""" engine = _in_memory_engine() # Ensure models are imported so metadata is populated - from transcription.models import Document, Job, Transcript, TranscriptRevision # noqa: F401 - import transcription.db as db_module + from transcription.models import Document # noqa: F401 + from transcription.models import Job # noqa: F401 + from transcription.models import Transcript # noqa: F401 + from transcription.models import TranscriptRevision # noqa: F401 db_module.create_all(engine=engine) @@ -34,7 +39,6 @@ class TestSchemaBootstrap: assert "transcript" in table_names assert "transcriptrevision" in table_names - def test_validate_schema_compatibility_returns_no_issues_for_fresh_schema(self): """validate_schema_compatibility reports no issues on fresh schema.""" engine = _in_memory_engine() diff --git a/tests/test_errors.py b/tests/test_errors.py index 785226b..ea03624 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -2,7 +2,10 @@ import pytest -from transcription.errors import AppError, ErrorCategory, classify_unexpected_error, new_error_id +from transcription.errors import AppError +from transcription.errors import ErrorCategory +from transcription.errors import classify_unexpected_error +from transcription.errors import new_error_id @pytest.mark.unit diff --git a/tests/test_migrations.py b/tests/test_migrations.py index e12d2c6..4945b8c 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -1,10 +1,12 @@ """Tests for transcription.migrations — explicit Step 4 migration safety behavior.""" -from sqlalchemy import inspect, text +from sqlalchemy import inspect +from sqlalchemy import text from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from transcription.migrations import apply_pending_migrations, list_pending_migrations +from transcription.migrations import apply_pending_migrations +from transcription.migrations import list_pending_migrations def _in_memory_engine(): @@ -103,4 +105,4 @@ class TestMigrations: second_apply = apply_pending_migrations(engine=engine) assert len(first_apply) == 2 - assert second_apply == [] \ No newline at end of file + assert second_apply == [] diff --git a/tests/test_models.py b/tests/test_models.py index 45474ae..ec3d20a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,11 @@ from uuid import UUID import pytest from sqlalchemy.exc import IntegrityError -from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision +from transcription.models import Document +from transcription.models import Job +from transcription.models import JobStatus +from transcription.models import Transcript +from transcription.models import TranscriptRevision def _make_document(**overrides) -> Document: diff --git a/tests/test_prompts.py b/tests/test_prompts.py index fdf2231..1b27ed3 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -2,7 +2,6 @@ from pathlib import Path - PROMPT_PATH = Path("prompts/transcribe_document.md") diff --git a/tests/ui/test_jobs_page.py b/tests/ui/test_jobs_page.py index 622af6b..17a7f6c 100644 --- a/tests/ui/test_jobs_page.py +++ b/tests/ui/test_jobs_page.py @@ -4,8 +4,11 @@ from uuid import uuid4 import pytest -from transcription.models import Document, Job, Transcript -from transcription.ui.jobs_page import fetch_job_detail, fetch_jobs +from transcription.models import Document +from transcription.models import Job +from transcription.models import Transcript +from transcription.ui.jobs_page import fetch_job_detail +from transcription.ui.jobs_page import fetch_jobs @pytest.mark.integration diff --git a/tests/ui/test_pages_registration.py b/tests/ui/test_pages_registration.py index 6dbcacb..73ae134 100644 --- a/tests/ui/test_pages_registration.py +++ b/tests/ui/test_pages_registration.py @@ -1,8 +1,8 @@ """Tests for UI page registration wiring.""" +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -import pytest from transcription.ui import register_pages diff --git a/tests/ui/test_upload_page.py b/tests/ui/test_upload_page.py index 18105a0..50c2e2f 100644 --- a/tests/ui/test_upload_page.py +++ b/tests/ui/test_upload_page.py @@ -5,7 +5,8 @@ from uuid import uuid4 import pytest -from transcription.services.upload import UploadError, UploadJobResult +from transcription.services.upload import UploadError +from transcription.services.upload import UploadJobResult from transcription.ui import upload_page