ruff linting

This commit is contained in:
John Lancaster
2026-06-26 18:19:09 -05:00
parent e90dbe4958
commit 6dc58a8d50
41 changed files with 265 additions and 192 deletions
+5 -2
View File
@@ -4,10 +4,13 @@ from __future__ import annotations
import logging import logging
from fastapi import FastAPI, Request from fastapi import FastAPI
from fastapi import Request
from fastapi.responses import JSONResponse 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 from transcription.security import AccessDeniedError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+9 -10
View File
@@ -5,17 +5,16 @@ from __future__ import annotations
from uuid import UUID from uuid import UUID
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel, Field from pydantic import BaseModel
from pydantic import Field
from transcription.services.library import ( from transcription.services.library import accept_revision
accept_revision, from transcription.services.library import add_revision
add_revision, from transcription.services.library import export_transcripts
export_transcripts, from transcription.services.library import get_job_detail
get_job_detail, from transcription.services.library import list_jobs
list_jobs, from transcription.services.library import list_revisions
list_revisions, from transcription.services.library import search_accepted_transcripts
search_accepted_transcripts,
)
router = APIRouter(prefix="/api", tags=["transcription"]) router = APIRouter(prefix="/api", tags=["transcription"])
+13 -12
View File
@@ -3,25 +3,27 @@
from __future__ import annotations from __future__ import annotations
from contextlib import asynccontextmanager 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 fastapi.responses import JSONResponse
from transcription.api.errors import register_error_handlers from transcription.api.errors import register_error_handlers
from transcription.api.health import router as health_router from transcription.api.health import router as health_router
from transcription.api.routes import router as transcription_router from transcription.api.routes import router as transcription_router
from transcription.config import get_settings, setup_logging from transcription.config import get_settings
from transcription.db import ( from transcription.config import setup_logging
create_all, from transcription.db import create_all
dispose_database_runtime, from transcription.db import dispose_database_runtime
initialize_database_runtime, from transcription.db import initialize_database_runtime
should_bootstrap_schema, from transcription.db import should_bootstrap_schema
validate_schema_compatibility, from transcription.db import validate_schema_compatibility
)
from transcription.errors import build_error_envelope from transcription.errors import build_error_envelope
from transcription.migrations import apply_pending_migrations 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.ui import register_pages
from transcription.worker import run_worker_loop from transcription.worker import run_worker_loop
@@ -104,4 +106,3 @@ def create_app() -> FastAPI:
app.include_router(health_router) app.include_router(health_router)
app.include_router(transcription_router) app.include_router(transcription_router)
return app return app
+2 -2
View File
@@ -5,7 +5,6 @@ once at startup. Provider-specific defaults (model names, base URLs)
are resolved by the provider adapters, not here. are resolved by the provider adapters, not here.
""" """
import logging
import logging.config import logging.config
from enum import StrEnum from enum import StrEnum
from functools import lru_cache from functools import lru_cache
@@ -13,7 +12,8 @@ from pathlib import Path
from typing import Literal from typing import Literal
from pydantic import model_validator from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict
class Provider(StrEnum): class Provider(StrEnum):
+5 -3
View File
@@ -10,10 +10,12 @@ from dataclasses import dataclass
from sqlalchemy import inspect from sqlalchemy import inspect
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlmodel import Session, SQLModel, create_engine from sqlmodel import Session
from sqlmodel import SQLModel
from transcription.config import Settings, get_settings from sqlmodel import create_engine
from transcription.config import Settings
from transcription.config import get_settings
@dataclass(frozen=True) @dataclass(frozen=True)
+3 -2
View File
@@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import UTC
from datetime import datetime
from enum import StrEnum from enum import StrEnum
from uuid import uuid4 from uuid import uuid4
@@ -64,7 +65,7 @@ def build_error_envelope(error: AppError) -> ErrorEnvelope:
category=error.category.value, category=error.category.value,
message=error.message, message=error.message,
suggestion=error.suggestion, suggestion=error.suggestion,
timestamp=datetime.now(timezone.utc).isoformat(), timestamp=datetime.now(UTC).isoformat(),
) )
+4 -3
View File
@@ -3,11 +3,12 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys
from transcription.config import get_settings from transcription.config import get_settings
from transcription.db import initialize_database_runtime, validate_schema_compatibility from transcription.db import initialize_database_runtime
from transcription.migrations import apply_pending_migrations, list_pending_migrations 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: def _build_parser() -> argparse.ArgumentParser:
+11 -7
View File
@@ -6,13 +6,16 @@ personal-scale deployments without introducing heavyweight migration tooling.
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import logging 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 import inspect
from sqlalchemy.engine import Connection, Engine from sqlalchemy import text
from sqlalchemy.engine import Connection
from sqlalchemy.engine import Engine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -60,7 +63,7 @@ def _record_revision(connection: Connection, revision: MigrationRevision) -> Non
{ {
"revision_id": revision.revision_id, "revision_id": revision.revision_id,
"description": revision.description, "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: def _apply_0002_create_transcriptrevision(connection: Connection) -> None:
"""Ensure transcriptrevision table exists.""" """Ensure transcriptrevision table exists."""
# Import models lazily so metadata is fully populated. # Import models lazily so metadata is fully populated.
from transcription.models import TranscriptRevision # noqa: F401
from sqlmodel import SQLModel from sqlmodel import SQLModel
from transcription.models import TranscriptRevision # noqa: F401
table = SQLModel.metadata.tables["transcriptrevision"] table = SQLModel.metadata.tables["transcriptrevision"]
table.create(bind=connection, checkfirst=True) table.create(bind=connection, checkfirst=True)
+12 -8
View File
@@ -1,11 +1,15 @@
"""SQLModel domain models for the transcription system.""" """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 enum import StrEnum
from typing import Optional 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): class JobStatus(StrEnum):
@@ -22,7 +26,7 @@ class Document(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True) id: UUID = Field(default_factory=uuid4, primary_key=True)
filename: str filename: str
file_path: 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 --- # --- relationships ---
jobs: list["Job"] = Relationship(back_populates="document") jobs: list["Job"] = Relationship(back_populates="document")
@@ -35,8 +39,8 @@ class Job(SQLModel, table=True):
document_id: UUID = Field(foreign_key="document.id") document_id: UUID = Field(foreign_key="document.id")
status: JobStatus = Field(default=JobStatus.QUEUED) status: JobStatus = Field(default=JobStatus.QUEUED)
retry_count: int = Field(default=0, ge=0) retry_count: int = Field(default=0, ge=0)
created_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(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
# --- relationships --- # --- relationships ---
document: Document = Relationship(back_populates="jobs") 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) job_id: UUID = Field(foreign_key="job.id", unique=True)
text: str | None = None text: str | None = None
error_detail: 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 --- # --- relationships ---
job: Job = Relationship(back_populates="transcript") job: Job = Relationship(back_populates="transcript")
@@ -66,7 +70,7 @@ class TranscriptRevision(SQLModel, table=True):
text: str text: str
source: str = Field(default="worker") source: str = Field(default="worker")
accepted: bool = Field(default=False) 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 --- # --- relationships ---
job: Job = Relationship(back_populates="revisions") job: Job = Relationship(back_populates="revisions")
+9 -9
View File
@@ -1,13 +1,13 @@
"""Provider exports and factory for transcription adapters.""" """Provider exports and factory for transcription adapters."""
from transcription.config import Provider, Settings, get_settings from transcription.config import Provider
from transcription.providers.base import ( from transcription.config import Settings
ProviderAuthError, from transcription.config import get_settings
ProviderError, from transcription.providers.base import ProviderAuthError
ProviderResponseError, from transcription.providers.base import ProviderError
TranscriptionProvider, from transcription.providers.base import ProviderResponseError
TranscriptionResult, from transcription.providers.base import TranscriptionProvider
) from transcription.providers.base import TranscriptionResult
from transcription.providers.openrouter import OpenRouterTranscriptionProvider from transcription.providers.openrouter import OpenRouterTranscriptionProvider
@@ -21,11 +21,11 @@ def get_transcription_provider(*, settings: Settings | None = None) -> Transcrip
__all__ = [ __all__ = [
"OpenRouterTranscriptionProvider",
"ProviderAuthError", "ProviderAuthError",
"ProviderError", "ProviderError",
"ProviderResponseError", "ProviderResponseError",
"TranscriptionProvider", "TranscriptionProvider",
"TranscriptionResult", "TranscriptionResult",
"OpenRouterTranscriptionProvider",
"get_transcription_provider", "get_transcription_provider",
] ]
+8 -12
View File
@@ -9,13 +9,12 @@ from typing import Any
from openrouter import OpenRouter from openrouter import OpenRouter
from transcription.config import Settings, get_settings from transcription.config import Settings
from transcription.providers.base import ( from transcription.config import get_settings
ProviderAuthError, from transcription.providers.base import ProviderAuthError
ProviderError, from transcription.providers.base import ProviderError
ProviderResponseError, from transcription.providers.base import ProviderResponseError
TranscriptionResult, from transcription.providers.base import TranscriptionResult
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ class OpenRouterTranscriptionProvider:
http_referer=request.http_referer, http_referer=request.http_referer,
x_open_router_title=request.x_open_router_title, x_open_router_title=request.x_open_router_title,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc:
message = str(exc).lower() message = str(exc).lower()
if "401" in message or "auth" in message or "api key" in message: if "401" in message or "auth" in message or "api key" in message:
raise ProviderAuthError("OpenRouter authentication failed") from exc raise ProviderAuthError("OpenRouter authentication failed") from exc
@@ -111,10 +110,7 @@ class OpenRouterTranscriptionProvider:
parts: list[str] = [] parts: list[str] = []
for item in content: for item in content:
text_part = None text_part = None
if isinstance(item, dict): text_part = item.get("text") if isinstance(item, dict) else self._get_optional_attr(item, "text")
text_part = item.get("text")
else:
text_part = self._get_optional_attr(item, "text")
if isinstance(text_part, str) and text_part.strip(): if isinstance(text_part, str) and text_part.strip():
parts.append(text_part.strip()) parts.append(text_part.strip())
+3 -2
View File
@@ -9,7 +9,8 @@ import secrets
from fastapi import Request from fastapi import Request
from transcription.config import Settings from transcription.config import Settings
from transcription.errors import AppError, ErrorCategory from transcription.errors import AppError
from transcription.errors import ErrorCategory
class AccessDeniedError(AppError): class AccessDeniedError(AppError):
@@ -22,7 +23,7 @@ class AccessDeniedError(AppError):
def is_protected_path(path: str) -> bool: def is_protected_path(path: str) -> bool:
"""Return True when a request path requires operator authentication.""" """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: def enforce_request_access(*, request: Request, settings: Settings) -> None:
+14 -19
View File
@@ -1,30 +1,25 @@
"""Service layer exports.""" """Service layer exports."""
from transcription.services.transcription import ( from transcription.services.transcription import DEFAULT_PROMPT_FILE
DEFAULT_PROMPT_FILE, from transcription.services.transcription import PromptLoadError
PromptLoadError, from transcription.services.transcription import TranscriptionError
TranscriptionError, from transcription.services.transcription import load_image_payload
load_image_payload, from transcription.services.transcription import load_prompt_text
load_prompt_text, from transcription.services.transcription import transcribe_document_image
transcribe_document_image, from transcription.services.upload import SUPPORTED_UPLOAD_EXTENSIONS
) from transcription.services.upload import UploadError
from transcription.services.upload import ( from transcription.services.upload import UploadJobResult
SUPPORTED_UPLOAD_EXTENSIONS, from transcription.services.upload import create_upload_job
UploadError,
UploadJobResult,
create_upload_job,
)
__all__ = [ __all__ = [
"DEFAULT_PROMPT_FILE", "DEFAULT_PROMPT_FILE",
"SUPPORTED_UPLOAD_EXTENSIONS",
"PromptLoadError", "PromptLoadError",
"TranscriptionError", "TranscriptionError",
"load_image_payload",
"load_prompt_text",
"transcribe_document_image",
"SUPPORTED_UPLOAD_EXTENSIONS",
"UploadError", "UploadError",
"UploadJobResult", "UploadJobResult",
"create_upload_job", "create_upload_job",
"load_image_payload",
"load_prompt_text",
"transcribe_document_image",
] ]
+13 -6
View File
@@ -3,14 +3,21 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import UTC
from datetime import datetime
from uuid import UUID 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.db import get_session
from transcription.errors import AppError, ErrorCategory from transcription.errors import AppError
from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision 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): class LibraryError(AppError):
@@ -132,7 +139,7 @@ def add_revision(
transcript.error_detail = None transcript.error_detail = None
session.add(transcript) session.add(transcript)
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
if accepted: if accepted:
job.status = JobStatus.COMPLETED job.status = JobStatus.COMPLETED
elif job.status == JobStatus.QUEUED: 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) job = session.get(Job, revision.job_id)
if job is not None: if job is not None:
job.status = JobStatus.COMPLETED job.status = JobStatus.COMPLETED
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
session.add(job) session.add(job)
session.commit() session.commit()
+10 -10
View File
@@ -6,16 +6,16 @@ import logging
import mimetypes import mimetypes
from pathlib import Path from pathlib import Path
from transcription.config import Settings, get_settings from transcription.config import Settings
from transcription.errors import AppError, ErrorCategory from transcription.config import get_settings
from transcription.providers import ( from transcription.errors import AppError
ProviderAuthError, from transcription.errors import ErrorCategory
ProviderError, from transcription.providers import ProviderAuthError
ProviderResponseError, from transcription.providers import ProviderError
TranscriptionProvider, from transcription.providers import ProviderResponseError
TranscriptionResult, from transcription.providers import TranscriptionProvider
get_transcription_provider, from transcription.providers import TranscriptionResult
) from transcription.providers import get_transcription_provider
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+10 -5
View File
@@ -5,14 +5,19 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from uuid import UUID, uuid4 from uuid import UUID
from uuid import uuid4
from sqlmodel import Session 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.db import get_session
from transcription.errors import AppError, ErrorCategory from transcription.errors import AppError
from transcription.models import Document, Job, JobStatus from transcription.errors import ErrorCategory
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,7 +78,7 @@ def create_upload_job(
original_filename=filename, original_filename=filename,
stored_path=stored_path, stored_path=stored_path,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc:
_best_effort_delete(stored_path) _best_effort_delete(stored_path)
raise UploadError( raise UploadError(
"Failed to create upload database records", "Failed to create upload database records",
-2
View File
@@ -12,5 +12,3 @@ def register_pages(app: FastAPI) -> None:
register_upload_page() register_upload_page()
register_jobs_page() register_jobs_page()
ui.run_with(app, mount_path="/ui", show_welcome_message=False) ui.run_with(app, mount_path="/ui", show_welcome_message=False)
+3 -1
View File
@@ -4,7 +4,9 @@ from __future__ import annotations
from nicegui import ui 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: def to_app_error(exc: Exception, *, operation: str) -> AppError:
+10 -9
View File
@@ -9,15 +9,16 @@ from nicegui import ui
from sqlmodel import select from sqlmodel import select
from transcription.db import get_session from transcription.db import get_session
from transcription.models import Document, Job, Transcript from transcription.models import Document
from transcription.services.library import ( from transcription.models import Job
accept_revision, from transcription.models import Transcript
add_revision, from transcription.services.library import accept_revision
export_transcripts, from transcription.services.library import add_revision
list_revisions, from transcription.services.library import export_transcripts
search_accepted_transcripts, from transcription.services.library import list_revisions
) from transcription.services.library import search_accepted_transcripts
from transcription.ui.error_presenter import show_error, summarize_error from transcription.ui.error_presenter import show_error
from transcription.ui.error_presenter import summarize_error
@dataclass(frozen=True) @dataclass(frozen=True)
+5 -2
View File
@@ -7,8 +7,11 @@ from dataclasses import dataclass
from nicegui import ui from nicegui import ui
from nicegui.events import UploadEventArguments from nicegui.events import UploadEventArguments
from transcription.services.upload import UploadError, UploadJobResult, create_upload_job from transcription.services.upload import UploadError
from transcription.ui.error_presenter import show_error, summarize_error 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 @dataclass
+19 -10
View File
@@ -4,17 +4,26 @@ from __future__ import annotations
import logging import logging
import time import time
from datetime import datetime, timezone from datetime import UTC
from datetime import datetime
from threading import Event from threading import Event
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy.engine import Engine 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.db import get_session
from transcription.errors import AppError, ErrorCategory, classify_unexpected_error, format_error_detail from transcription.errors import AppError
from transcription.models import Document, Job, JobStatus, Transcript 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.library import add_revision
from transcription.services.transcription import transcribe_document_image 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) logger.info("Picked queued job operation=worker.pick job_id=%s", job.id)
job.status = JobStatus.PROCESSING job.status = JobStatus.PROCESSING
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
session.add(job) session.add(job)
session.commit() session.commit()
session.refresh(job) session.refresh(job)
@@ -75,7 +84,7 @@ def _process_next_queued_job(*, session: Session) -> bool:
session=session, session=session,
) )
job.status = JobStatus.TRANSCRIBED job.status = JobStatus.TRANSCRIBED
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
session.add(job) session.add(job)
session.commit() session.commit()
logger.info( logger.info(
@@ -85,7 +94,7 @@ def _process_next_queued_job(*, session: Session) -> bool:
result.provider, result.provider,
revision.revision_number, 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") error = exc if isinstance(exc, AppError) else classify_unexpected_error(exc, operation="worker.process_job")
settings = _get_worker_settings() settings = _get_worker_settings()
if _should_retry(job=job, error=error, settings=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)) _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error))
job.retry_count += 1 job.retry_count += 1
job.status = JobStatus.QUEUED job.status = JobStatus.QUEUED
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
session.add(job) session.add(job)
session.commit() session.commit()
if settings.worker_retry_backoff_seconds > 0: 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: 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)) _upsert_transcript(session=session, job_id=job.id, text=None, error_detail=format_error_detail(error))
job.status = JobStatus.FAILED job.status = JobStatus.FAILED
job.updated_at = datetime.now(timezone.utc) job.updated_at = datetime.now(UTC)
session.add(job) session.add(job)
session.commit() session.commit()
+4 -3
View File
@@ -5,18 +5,19 @@ from __future__ import annotations
import base64 import base64
from types import SimpleNamespace from types import SimpleNamespace
import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest
from transcription.api.errors import register_error_handlers from transcription.api.errors import register_error_handlers
from transcription.errors import build_error_envelope 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: 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}" return f"Basic {token}"
+3 -2
View File
@@ -1,11 +1,12 @@
"""Tests for API error response envelope handlers.""" """Tests for API error response envelope handlers."""
import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest
from transcription.api.errors import register_error_handlers 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 @pytest.mark.integration
+7 -6
View File
@@ -1,12 +1,13 @@
"""Tests for Step 3 functional API routes.""" """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 types import SimpleNamespace
from uuid import uuid4 from uuid import uuid4
import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest
from transcription.api.errors import register_error_handlers from transcription.api.errors import register_error_handlers
from transcription.api.routes import router from transcription.api.routes import router
@@ -25,7 +26,7 @@ class TestFunctionalRoutes:
def test_get_jobs_returns_serialized_rows(self, monkeypatch): def test_get_jobs_returns_serialized_rows(self, monkeypatch):
"""GET /api/jobs returns normalized job rows.""" """GET /api/jobs returns normalized job rows."""
now = datetime.now(timezone.utc) now = datetime.now(UTC)
job = SimpleNamespace( job = SimpleNamespace(
id=uuid4(), id=uuid4(),
document_id=uuid4(), document_id=uuid4(),
@@ -53,7 +54,7 @@ class TestFunctionalRoutes:
text="edited text", text="edited text",
source="user", source="user",
accepted=False, accepted=False,
created_at=datetime.now(timezone.utc), created_at=datetime.now(UTC),
) )
monkeypatch.setattr("transcription.api.routes.add_revision", lambda **_kwargs: revision) monkeypatch.setattr("transcription.api.routes.add_revision", lambda **_kwargs: revision)
@@ -78,7 +79,7 @@ class TestFunctionalRoutes:
text="family archive", text="family archive",
source="user", source="user",
accepted=True, accepted=True,
created_at=datetime.now(timezone.utc), created_at=datetime.now(UTC),
) )
monkeypatch.setattr("transcription.api.routes.search_accepted_transcripts", lambda query: [result]) monkeypatch.setattr("transcription.api.routes.search_accepted_transcripts", lambda query: [result])
@@ -103,7 +104,7 @@ class TestFunctionalRoutes:
"accepted": True, "accepted": True,
"source": "user", "source": "user",
"text": "exported", "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) monkeypatch.setattr("transcription.api.routes.export_transcripts", lambda accepted_only=True: records)
+3 -1
View File
@@ -5,7 +5,9 @@ isolated, fast, and leave no artifacts on disk.
""" """
import pytest 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 from sqlmodel.pool import StaticPool
+3 -1
View File
@@ -6,7 +6,9 @@ import pytest
from sqlmodel import select from sqlmodel import select
from transcription.config import Settings 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.providers.base import TranscriptionResult
from transcription.services.upload import create_upload_job from transcription.services.upload import create_upload_job
from transcription.worker import process_next_queued_job from transcription.worker import process_next_queued_job
+4 -2
View File
@@ -5,8 +5,10 @@ from types import SimpleNamespace
import pytest import pytest
from transcription.config import Settings from transcription.config import Settings
from transcription.providers.base import ProviderError, ProviderResponseError from transcription.providers.base import ProviderError
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL, OpenRouterTranscriptionProvider from transcription.providers.base import ProviderResponseError
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL
from transcription.providers.openrouter import OpenRouterTranscriptionProvider
class _FakeChat: class _FakeChat:
+11 -3
View File
@@ -1,10 +1,18 @@
"""Tests for Step 3 library services (revisions, search, export).""" """Tests for Step 3 library services (revisions, search, export)."""
from sqlmodel import select
import pytest import pytest
from sqlmodel import select
from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision from transcription.models import Document
from transcription.services.library import accept_revision, add_revision, export_transcripts, list_revisions, search_accepted_transcripts 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: def _create_job(session) -> Job:
+7 -8
View File
@@ -5,14 +5,13 @@ from pathlib import Path
import pytest import pytest
from transcription.config import Settings from transcription.config import Settings
from transcription.providers.base import ProviderError, TranscriptionResult from transcription.providers.base import ProviderError
from transcription.services.transcription import ( from transcription.providers.base import TranscriptionResult
PromptLoadError, from transcription.services.transcription import PromptLoadError
TranscriptionError, from transcription.services.transcription import TranscriptionError
load_image_payload, from transcription.services.transcription import load_image_payload
load_prompt_text, from transcription.services.transcription import load_prompt_text
transcribe_document_image, from transcription.services.transcription import transcribe_document_image
)
class _FakeProvider: class _FakeProvider:
@@ -7,7 +7,6 @@ import pytest
from transcription.services.transcription import transcribe_document_image from transcription.services.transcription import transcribe_document_image
HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY")) HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY"))
REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real" REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real"
+5 -2
View File
@@ -5,8 +5,11 @@ from pathlib import Path
import pytest import pytest
from transcription.config import Settings from transcription.config import Settings
from transcription.models import Document, Job, JobStatus from transcription.models import Document
from transcription.services.upload import UploadError, create_upload_job 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 @pytest.mark.unit
+9 -3
View File
@@ -7,10 +7,16 @@ import pytest
from sqlmodel import select from sqlmodel import select
from transcription.config import Settings from transcription.config import Settings
from transcription.errors import AppError, ErrorCategory from transcription.errors import AppError
from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision 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.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: def _create_queued_job(session, *, filename: str = "doc.jpg", file_path: str = "uploads/doc.jpg") -> Job:
+2 -1
View File
@@ -5,7 +5,8 @@ from pathlib import Path
import pytest import pytest
from pydantic import ValidationError 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: def _make_settings(**overrides) -> Settings:
+9 -5
View File
@@ -1,7 +1,10 @@
"""Tests for transcription.db — schema bootstrap and session factory.""" """Tests for transcription.db — schema bootstrap and session factory."""
from sqlalchemy import inspect, text from sqlalchemy import inspect
from sqlmodel import Session, SQLModel, create_engine from sqlalchemy import text
from sqlmodel import Session
from sqlmodel import SQLModel
from sqlmodel import create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
@@ -21,9 +24,11 @@ class TestSchemaBootstrap:
"""After create_all(), core V1 tables exist.""" """After create_all(), core V1 tables exist."""
engine = _in_memory_engine() engine = _in_memory_engine()
# Ensure models are imported so metadata is populated # Ensure models are imported so metadata is populated
from transcription.models import Document, Job, Transcript, TranscriptRevision # noqa: F401
import transcription.db as db_module 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) db_module.create_all(engine=engine)
@@ -34,7 +39,6 @@ class TestSchemaBootstrap:
assert "transcript" in table_names assert "transcript" in table_names
assert "transcriptrevision" in table_names assert "transcriptrevision" in table_names
def test_validate_schema_compatibility_returns_no_issues_for_fresh_schema(self): def test_validate_schema_compatibility_returns_no_issues_for_fresh_schema(self):
"""validate_schema_compatibility reports no issues on fresh schema.""" """validate_schema_compatibility reports no issues on fresh schema."""
engine = _in_memory_engine() engine = _in_memory_engine()
+4 -1
View File
@@ -2,7 +2,10 @@
import pytest 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 @pytest.mark.unit
+4 -2
View File
@@ -1,10 +1,12 @@
"""Tests for transcription.migrations — explicit Step 4 migration safety behavior.""" """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 import create_engine
from sqlmodel.pool import StaticPool 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(): def _in_memory_engine():
+5 -1
View File
@@ -5,7 +5,11 @@ from uuid import UUID
import pytest import pytest
from sqlalchemy.exc import IntegrityError 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: def _make_document(**overrides) -> Document:
-1
View File
@@ -2,7 +2,6 @@
from pathlib import Path from pathlib import Path
PROMPT_PATH = Path("prompts/transcribe_document.md") PROMPT_PATH = Path("prompts/transcribe_document.md")
+5 -2
View File
@@ -4,8 +4,11 @@ from uuid import uuid4
import pytest import pytest
from transcription.models import Document, Job, Transcript from transcription.models import Document
from transcription.ui.jobs_page import fetch_job_detail, fetch_jobs 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 @pytest.mark.integration
+1 -1
View File
@@ -1,8 +1,8 @@
"""Tests for UI page registration wiring.""" """Tests for UI page registration wiring."""
import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import pytest
from transcription.ui import register_pages from transcription.ui import register_pages
+2 -1
View File
@@ -5,7 +5,8 @@ from uuid import uuid4
import pytest 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 from transcription.ui import upload_page