generated from john/python-template
ruff linting
This commit is contained in:
@@ -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__)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,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(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -84,4 +85,4 @@ def format_error_detail(error: AppError) -> str:
|
|||||||
return (
|
return (
|
||||||
f"[{error.category.value}] {error.message} | "
|
f"[{error.category.value}] {error.message} | "
|
||||||
f"suggestion={error.suggestion} | error_id={error.error_id}"
|
f"suggestion={error.suggestion} | error_id={error.error_id}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -71,4 +72,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -125,4 +129,4 @@ def apply_pending_migrations(*, engine: Engine) -> list[str]:
|
|||||||
applied_ids.append(revision.revision_id)
|
applied_ids.append(revision.revision_id)
|
||||||
logger.info("Applied migration revision=%s", revision.revision_id)
|
logger.info("Applied migration revision=%s", revision.revision_id)
|
||||||
|
|
||||||
return applied_ids
|
return applied_ids
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -78,4 +79,4 @@ def _parse_basic_authorization_header(value: str | None) -> tuple[str, str]:
|
|||||||
suggestion="Provide HTTP Basic operator credentials and retry.",
|
suggestion="Provide HTTP Basic operator credentials and retry.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return username, password
|
return username, password
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -152,4 +157,4 @@ def _best_effort_delete(path: Path) -> None:
|
|||||||
if path.exists():
|
if path.exists():
|
||||||
path.unlink()
|
path.unlink()
|
||||||
except OSError:
|
except OSError:
|
||||||
logger.warning("Failed to clean up upload file after DB error: %s", path)
|
logger.warning("Failed to clean up upload file after DB error: %s", path)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -37,4 +39,4 @@ def summarize_error(exc: Exception, *, operation: str) -> str:
|
|||||||
error = to_app_error(exc, operation=operation)
|
error = to_app_error(exc, operation=operation)
|
||||||
if error.category == ErrorCategory.INTERNAL_UNEXPECTED:
|
if error.category == ErrorCategory.INTERNAL_UNEXPECTED:
|
||||||
return f"Unexpected error (ref: {error.error_id})"
|
return f"Unexpected error (ref: {error.error_id})"
|
||||||
return f"{error.message} (ref: {error.error_id})"
|
return f"{error.message} (ref: {error.error_id})"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|
||||||
|
|
||||||
@@ -125,4 +126,4 @@ class TestAccessControl:
|
|||||||
response = client.get("/healthz")
|
response = client.get("/healthz")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"status": "ok"}
|
assert response.json() == {"status": "ok"}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -67,4 +66,4 @@ class TestRealImageExternalTranscription:
|
|||||||
f"{result.text}\n"
|
f"{result.text}\n"
|
||||||
)
|
)
|
||||||
artifact_path.write_text(artifact_text, encoding="utf-8")
|
artifact_path.write_text(artifact_text, encoding="utf-8")
|
||||||
assert artifact_path.exists()
|
assert artifact_path.exists()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
@@ -103,4 +105,4 @@ class TestMigrations:
|
|||||||
second_apply = apply_pending_migrations(engine=engine)
|
second_apply = apply_pending_migrations(engine=engine)
|
||||||
|
|
||||||
assert len(first_apply) == 2
|
assert len(first_apply) == 2
|
||||||
assert second_apply == []
|
assert second_apply == []
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user