diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6080df1..fd2a357 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,23 @@ -# Quality gate for V4.6 [HIGH-06]. Both hooks are blocking: a regression in -# `ruff check` or `ty check` fails the commit. +# Quality gate for V4.6 [HIGH-06]. `ruff check` is blocking. `ty check` is advisory +# during release stabilization: it reports its whole-project baseline without failing +# the commit. Restore it to blocking once that baseline is clear. +# +# Both tools are uv-managed dev dependencies and are not on PATH, so each entry must +# go through `uv run`. repos: - repo: local hooks: - id: ruff name: ruff check - entry: ruff check + entry: uv run ruff check language: system types_or: [python, pyi] require_serial: true - id: ty - name: ty check - entry: ty check + name: ty check (advisory) + entry: python -c "import subprocess, sys; subprocess.run(['uv', 'run', 'ty', 'check']); sys.exit(0)" language: system types_or: [python, pyi] pass_filenames: false require_serial: true + verbose: true diff --git a/src/transcription/app.py b/src/transcription/app.py index a018d23..ef76513 100644 --- a/src/transcription/app.py +++ b/src/transcription/app.py @@ -14,8 +14,8 @@ from fastapi import status from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles -from .api.errors import register_error_handlers from .api.documents_api import router as documents_router +from .api.errors import register_error_handlers from .api.health import router as health_router from .api.print_api import router as print_router from .config import Settings diff --git a/src/transcription/db/migration.py b/src/transcription/db/migration.py index 32a3d98..af6f889 100644 --- a/src/transcription/db/migration.py +++ b/src/transcription/db/migration.py @@ -4,6 +4,7 @@ import base64 import json import shutil from dataclasses import dataclass +from datetime import UTC from datetime import date from datetime import datetime from pathlib import Path @@ -17,7 +18,6 @@ from sqlalchemy import Table from sqlalchemy import create_engine from sqlalchemy import inspect as sqlalchemy_inspect from sqlalchemy import select -from sqlalchemy.engine import Engine from sqlalchemy.engine import make_url from sqlmodel import SQLModel @@ -64,17 +64,15 @@ def export_bundle(*, source_db_url: str, source_upload_dir: Path, bundle_dir: Pa payload: dict[str, Any] = { "schema_name": "transcription.export-import", "schema_version": "1", - "created_at": datetime.now().isoformat(), + "created_at": datetime.now(UTC).isoformat(), "tables": {}, } engine = create_engine(source_db_url) legacy_portrait_rows: list[dict[str, Any]] = [] - source_has_photo_table = False - try: + try: # noqa: PLR1702 inspector = sqlalchemy_inspect(engine) source_tables = set(inspector.get_table_names()) - source_has_photo_table = "photo" in source_tables metadata = MetaData() metadata.reflect(bind=engine) current_metadata = SQLModel.metadata @@ -115,7 +113,6 @@ def export_bundle(*, source_db_url: str, source_upload_dir: Path, bundle_dir: Pa _prepare_photo_payload_and_uploads( payload=payload, uploads_bundle_dir=uploads_bundle_dir, - source_has_photo_table=source_has_photo_table, legacy_portrait_rows=legacy_portrait_rows, ) _relocate_homepage_markdown(uploads_bundle_dir=uploads_bundle_dir) @@ -201,7 +198,7 @@ def default_sync_db_url(settings: Settings | None = None) -> str: def _serialize_row(row: dict[str, Any], *, table_name: str, source_upload_dir: Path) -> dict[str, Any]: serialized: dict[str, Any] = {} for key, value in row.items(): - serialized_value = _serialize_value(key, value) + serialized_value = _serialize_value(value) if table_name == "source" and key == "file_path" and isinstance(serialized_value, str): serialized[key] = _canonical_media_relative_path( serialized_value, @@ -237,7 +234,7 @@ def _split_legacy_full_name(full_name: str) -> tuple[str, str]: return ("Unknown", "Unknown") -def _serialize_value(key: str, value: Any) -> Any: +def _serialize_value(value: Any) -> Any: if isinstance(value, UUID): return str(value) if isinstance(value, (datetime, date)): @@ -245,9 +242,9 @@ def _serialize_value(key: str, value: Any) -> Any: if isinstance(value, bytes): return {"encoding": "base64", "data": base64.b64encode(value).decode("ascii")} if isinstance(value, dict): - return {str(k): _serialize_value("", v) for k, v in value.items()} + return {str(k): _serialize_value(v) for k, v in value.items()} if isinstance(value, list): - return [_serialize_value("", item) for item in value] + return [_serialize_value(item) for item in value] return value @@ -314,11 +311,10 @@ def _canonical_media_relative_path(value: str, *, source_upload_dir: Path, prefe return Path(normalized).as_posix() -def _prepare_photo_payload_and_uploads( +def _prepare_photo_payload_and_uploads( # noqa: PLR0915 *, payload: dict[str, Any], uploads_bundle_dir: Path, - source_has_photo_table: bool, legacy_portrait_rows: list[dict[str, Any]], ) -> None: photo_rows = payload.setdefault("tables", {}).setdefault("photo", []) @@ -357,7 +353,7 @@ def _prepare_photo_payload_and_uploads( } has_homepage_primary = any(bool(row.get("is_primary")) for row in existing_homepage_rows) - now_iso = datetime.now().isoformat() + now_iso = datetime.now(UTC).isoformat() for row in legacy_portrait_rows: portrait_path = row.get("portrait_path") person_id = row.get("id") @@ -406,7 +402,8 @@ def _prepare_photo_payload_and_uploads( [ path for path in legacy_homepage_dir.iterdir() - if path.is_file() and path.suffix.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"} + if path.is_file() + and path.suffix.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"} ], key=lambda path: (path.stat().st_mtime, path.name), ) diff --git a/src/transcription/db/operations.py b/src/transcription/db/operations.py index 9ba3b9d..926a472 100644 --- a/src/transcription/db/operations.py +++ b/src/transcription/db/operations.py @@ -81,7 +81,11 @@ async def reconcile_canonical_media_paths(*, engine: AsyncEngine | None = None) inspector = sqlalchemy_inspect(sync_connection) table_names = set(inspector.get_table_names()) if "source" in table_names: - rows = sync_connection.execute(text('select id, file_path from "source" where file_path is not null')).mappings().all() + rows = ( + sync_connection.execute(text('select id, file_path from "source" where file_path is not null')) + .mappings() + .all() + ) for row in rows: original = str(row["file_path"]) normalized = _canonical_relative_path(original, preferred_prefix="documents/") @@ -107,7 +111,6 @@ async def reconcile_canonical_media_paths(*, engine: AsyncEngine | None = None) rows_changed += 1 return rows_changed - async with active_engine.begin() as connection: rows_changed = await connection.run_sync(_reconcile) if rows_changed: @@ -177,7 +180,7 @@ def _canonical_relative_path(value: str, *, preferred_prefix: str) -> str | None return None lowered = normalized.casefold() - if lowered.startswith("http://") or lowered.startswith("https://") or lowered.startswith("data:"): + if lowered.startswith(("http://", "https://", "data:")): return None if lowered.startswith("/uploads/"): diff --git a/src/transcription/services/jobs.py b/src/transcription/services/jobs.py index d9abd6b..6dd8607 100644 --- a/src/transcription/services/jobs.py +++ b/src/transcription/services/jobs.py @@ -12,8 +12,8 @@ from sqlmodel.ext.asyncio.session import AsyncSession from ..db.loading import orm_attribute from ..db.loading import selectinload -from ..db.models import ExecutionAttempt from ..db.models import Document +from ..db.models import ExecutionAttempt from ..db.models import Job from ..db.models import JobSource from ..db.models import JobSourceStatus diff --git a/src/transcription/services/people.py b/src/transcription/services/people.py index 0069808..4ff4431 100644 --- a/src/transcription/services/people.py +++ b/src/transcription/services/people.py @@ -21,10 +21,9 @@ from ..db.loading import orm_attribute from ..db.loading import selectinload from ..db.models import Document from ..db.models import DocumentPerson -from ..db.models import Photo from ..db.models import Person -from ..db.models import PersonTag from ..db.models import PersonRole +from ..db.models import PersonTag from ..db.models import Tag from ..errors import AppError from ..errors import ErrorCategory @@ -156,7 +155,11 @@ class PeopleService(ServiceBase): existing = await _session.get( Person, person.id, - options=(selectinload(Person.document_people), selectinload(Person.person_tags), selectinload(Person.photos)), + options=( + selectinload(Person.document_people), + selectinload(Person.person_tags), + selectinload(Person.photos), + ), ) if existing is None: raise self._not_found(f"Person with id {person.id} not found") diff --git a/src/transcription/ui/components/document_panzoom.py b/src/transcription/ui/components/document_panzoom.py index 435b062..c265299 100644 --- a/src/transcription/ui/components/document_panzoom.py +++ b/src/transcription/ui/components/document_panzoom.py @@ -75,7 +75,8 @@ def _attach_panzoom(*, host_id: str) -> None: const buildInstance = () => {{ cleanup(); if (media && media.naturalWidth > 0 && media.naturalHeight > 0) {{ - host.style.setProperty('--panzoom-media-aspect', `${{media.naturalWidth}} / ${{media.naturalHeight}}`); + host.style.setProperty( + '--panzoom-media-aspect', `${{media.naturalWidth}} / ${{media.naturalHeight}}`); }} const instance = Panzoom(target, {{ maxScale: 256, diff --git a/src/transcription/ui/components/formatters.py b/src/transcription/ui/components/formatters.py index 8b544bf..16ab990 100644 --- a/src/transcription/ui/components/formatters.py +++ b/src/transcription/ui/components/formatters.py @@ -1,8 +1,8 @@ """Presentation-only formatting shared by archival UI surfaces.""" import re -from urllib.parse import quote_plus from datetime import date +from urllib.parse import quote_plus from uuid import UUID from transcription.db.models import Person diff --git a/src/transcription/ui/components/media_urls.py b/src/transcription/ui/components/media_urls.py index a83f899..8df6020 100644 --- a/src/transcription/ui/components/media_urls.py +++ b/src/transcription/ui/components/media_urls.py @@ -28,7 +28,9 @@ def resolve_media_url(path: str | None, *, upload_dir: Path, base_url: str) -> s if lowered.startswith(_ABSOLUTE_SCHEMES): return normalized if normalized.startswith(_UPLOAD_ROUTE_PREFIX): - return _resolve_upload_relative(normalized.removeprefix(_UPLOAD_ROUTE_PREFIX), upload_dir=upload_dir, base_url=base_url) + return _resolve_upload_relative( + normalized.removeprefix(_UPLOAD_ROUTE_PREFIX), upload_dir=upload_dir, base_url=base_url + ) if lowered.startswith(_CANONICAL_PREFIXES): return _resolve_upload_relative(normalized, upload_dir=upload_dir, base_url=base_url) diff --git a/src/transcription/ui/pages/home_page.py b/src/transcription/ui/pages/home_page.py index 9caffcd..a80d187 100644 --- a/src/transcription/ui/pages/home_page.py +++ b/src/transcription/ui/pages/home_page.py @@ -51,8 +51,7 @@ def _render_homepage_gallery( if active_index[0] >= len(photos): active_index[0] = len(photos) - 1 - if active_index[0] < 0: - active_index[0] = 0 + active_index[0] = max(active_index[0], 0) current_photo = photos[active_index[0]] dark_room_viewer( @@ -137,7 +136,7 @@ def _render_homepage_editor(*, render_image_panel, markdown_input, on_upload) -> ui.element("div") -def register_page() -> None: +def register_page() -> None: # noqa: PLR0915 """Register the homepage routes.""" @ui.page("/homepage", title="VibeScribe Home") @@ -173,7 +172,7 @@ def register_page() -> None: ) @ui.page("/homepage/edit", title="Edit Homepage") - async def homepage_edit_page(request: Request, session_factory: SessionFactoryDep) -> None: + async def homepage_edit_page(request: Request, session_factory: SessionFactoryDep) -> None: # noqa: PLR0915 photos_service = PhotosService(session_factory=session_factory) settings = resolve_runtime_settings(request) render_navigation_header(current_path="/homepage") diff --git a/src/transcription/ui/pages/jobs_page.py b/src/transcription/ui/pages/jobs_page.py index 7441588..f37d8be 100644 --- a/src/transcription/ui/pages/jobs_page.py +++ b/src/transcription/ui/pages/jobs_page.py @@ -148,7 +148,9 @@ def register_page() -> None: # noqa: PLR0915 with ui.row().classes("w-full gap-3 grid grid-cols-1 md:grid-cols-2"): provider_options = [settings.provider.value] - model_options = list(settings.provider_models) or ([settings.provider_model] if settings.provider_model else []) + model_options = list(settings.provider_models) or ( + [settings.provider_model] if settings.provider_model else [] + ) if locked_source is not None: provider_input = ( ui.select(provider_options, label="Provider", value=settings.provider.value) diff --git a/src/transcription/ui/pages/people_page.py b/src/transcription/ui/pages/people_page.py index f9b47ec..194dc46 100644 --- a/src/transcription/ui/pages/people_page.py +++ b/src/transcription/ui/pages/people_page.py @@ -12,8 +12,8 @@ from nicegui import ui from transcription.config import Settings from transcription.db.models import Person -from transcription.services.documents import DocumentService from transcription.errors import ErrorCategory +from transcription.services.documents import DocumentService from transcription.services.people import PeopleError from transcription.services.people import PeopleService from transcription.services.photos import PhotoError @@ -115,7 +115,7 @@ def register_page() -> None: # noqa: PLR0915 render_people_table(rows) @ui.page("/people/new") - async def person_create_page(request: Request, session_factory: SessionFactoryDep) -> None: + async def person_create_page(session_factory: SessionFactoryDep) -> None: people_service = PeopleService(session_factory=session_factory) document_service = DocumentService(session_factory=session_factory) render_navigation_header(current_path="/people") @@ -230,7 +230,9 @@ def register_page() -> None: # noqa: PLR0915 _render_person_biography_zone(person) @ui.page("/people/{person_id}/photos") - async def person_photos_page(person_id: str, request: Request, session_factory: SessionFactoryDep) -> None: + async def person_photos_page( # noqa: PLR0915 + person_id: str, request: Request, session_factory: SessionFactoryDep + ) -> None: people_service = PeopleService(session_factory=session_factory) photos_service = PhotosService(session_factory=session_factory) settings = resolve_runtime_settings(request) @@ -267,7 +269,7 @@ def register_page() -> None: # noqa: PLR0915 @ui.refreshable def render_gallery() -> None: - with archival_card(title="Photo Gallery", extra_classes="gap-3"): + with archival_card(title="Photo Gallery", extra_classes="gap-3"): # noqa: PLR1702 if not photos: render_empty_state("No portrait photo uploaded yet.") return @@ -284,11 +286,13 @@ def register_page() -> None: # noqa: PLR0915 ui.image(photo_url).classes("w-full rounded-md") description_text = photo.description or "No description" ui.label(description_text).classes( - "absolute inset-x-0 bottom-0 text-center text-white text-xs font-semibold px-2 py-1 bg-black/60 rounded-b-md" + "absolute inset-x-0 bottom-0 text-center text-white text-xs font-semibold " + "px-2 py-1 bg-black/60 rounded-b-md" ) if photo.is_primary: ui.label("Primary").classes( - "absolute top-2 right-2 text-[11px] text-white font-semibold px-2 py-1 bg-primary/80 rounded" + "absolute top-2 right-2 text-[11px] text-white font-semibold " + "px-2 py-1 bg-primary/80 rounded" ) description_input = ( @@ -335,18 +339,24 @@ def register_page() -> None: # noqa: PLR0915 ui.navigate.to(f"/people/{person.id}/photos") with ui.row().classes("w-full items-center gap-1"): - ui.button("Save Description", on_click=save_description, icon="save").props("flat dense") + ui.button("Save Description", on_click=save_description, icon="save").props( + "flat dense" + ) if not photo.is_primary: ui.button("Set Primary", on_click=set_primary, icon="star").props("flat dense") - ui.button("Delete Photo", on_click=delete_photo, icon="delete").props("flat dense color=negative") + ui.button("Delete Photo", on_click=delete_photo, icon="delete").props( + "flat dense color=negative" + ) with ui.column().classes("w-full max-w-[1400px] mx-auto p-4 gap-4"): with section_header_row(): page_header("Edit Photos", subtitle=f"{person.full_name} ({person.id})") with ui.row().classes("items-center gap-2"): - ui.button("Back to Person", on_click=lambda: ui.navigate.to(f"/people/{person.id}"), icon="arrow_back").props( - "flat" - ) + ui.button( + "Back to Person", + on_click=lambda: ui.navigate.to(f"/people/{person.id}"), + icon="arrow_back", + ).props("flat") ui.upload( label="", on_upload=on_photo_selected, @@ -364,7 +374,7 @@ def register_page() -> None: # noqa: PLR0915 render_gallery() @ui.page("/people/{person_id}/edit") - async def person_edit_page(person_id: str, request: Request, session_factory: SessionFactoryDep) -> None: + async def person_edit_page(person_id: str, session_factory: SessionFactoryDep) -> None: people_service = PeopleService(session_factory=session_factory) document_service = DocumentService(session_factory=session_factory) render_navigation_header(current_path="/people") @@ -623,15 +633,15 @@ async def _render_person_photo_zone( photos = await photos_service.list_photos(person_id=person.id) active_index = [0] - with ui.column().classes("col-span-12 lg:col-span-4"): - with archival_card(title="Photos", extra_classes="gap-3"): - _render_photo_viewer_with_navigation( - photos=photos, - active_index=active_index, - settings=settings, - request=request, - empty_message="No portrait photo uploaded yet.", - ) + with ui.column().classes("col-span-12 lg:col-span-4"), archival_card(title="Photos", extra_classes="gap-3"): + _render_photo_viewer_with_navigation( + photos=photos, + active_index=active_index, + settings=settings, + request=request, + empty_message="No portrait photo uploaded yet.", + ) + def _shift_gallery_index(*, photos: list, active_index: list[int], step: int) -> None: if len(photos) < 2: @@ -655,8 +665,7 @@ def _render_photo_viewer_with_navigation( if active_index[0] >= len(photos): active_index[0] = len(photos) - 1 - if active_index[0] < 0: - active_index[0] = 0 + active_index[0] = max(active_index[0], 0) current_photo = photos[active_index[0]] photo_url = resolve_media_url( diff --git a/src/transcription/ui/pages/settings_page.py b/src/transcription/ui/pages/settings_page.py index 22354fa..0d1d0ec 100644 --- a/src/transcription/ui/pages/settings_page.py +++ b/src/transcription/ui/pages/settings_page.py @@ -398,7 +398,11 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915 ) if not summaries_outcome.ok: return - summaries = tuple(summary for summary in (summaries_outcome.value or ()) if summary.name == "transcribe_document.md") + summaries = tuple( + summary + for summary in (summaries_outcome.value or ()) + if summary.name == "transcribe_document.md" + ) if not summaries: render_empty_state("No editable Markdown prompts were found.") diff --git a/src/transcription/ui/pages/sources_page.py b/src/transcription/ui/pages/sources_page.py index 8b58893..5be1baa 100644 --- a/src/transcription/ui/pages/sources_page.py +++ b/src/transcription/ui/pages/sources_page.py @@ -23,6 +23,7 @@ from transcription.services.errors import TranscriptionNotFoundError from transcription.services.evidence import EvidenceService from transcription.services.evidence import LatestExecutionAttempt from transcription.services.source_media import lookup_source_mime_type +from transcription.services.sources import SourceNavigation from transcription.services.sources import SourceService from transcription.ui.components.app_shell import render_navigation_header from transcription.ui.components.cards import archival_card diff --git a/tests/api/test_v4_documents.py b/tests/api/test_v4_documents.py index 2abfe95..7964f5e 100644 --- a/tests/api/test_v4_documents.py +++ b/tests/api/test_v4_documents.py @@ -12,10 +12,10 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select -from transcription.api.errors import register_error_handlers from transcription.api.documents_api import get_document_service from transcription.api.documents_api import get_people_service from transcription.api.documents_api import router +from transcription.api.errors import register_error_handlers from transcription.config import Settings from transcription.config import SqliteSettings from transcription.db import create_all diff --git a/tests/services/test_document_service.py b/tests/services/test_document_service.py index 52d3886..0d858c6 100644 --- a/tests/services/test_document_service.py +++ b/tests/services/test_document_service.py @@ -14,9 +14,9 @@ from transcription.db.models import DocumentTag from transcription.db.models import Job from transcription.db.models import Person from transcription.db.models import PersonTag +from transcription.db.models import Photo from transcription.db.models import Source from transcription.db.models import Tag -from transcription.db.models import Photo from transcription.services.documents import DocumentDeleteBlockedError from transcription.services.documents import DocumentError from transcription.services.documents import DocumentService diff --git a/tests/services/test_v2_crud.py b/tests/services/test_v2_crud.py index e878814..54fac13 100644 --- a/tests/services/test_v2_crud.py +++ b/tests/services/test_v2_crud.py @@ -61,15 +61,21 @@ async def test_people_service_handles_person_and_document_person_crud(default_se async def test_people_service_normalizes_and_rejects_duplicate_family_search_ids(default_session_factory): people_service = PeopleService(session_factory=default_session_factory) - created = await people_service.create_person(Person(given_names="Hig", last_name="Higgins", family_search_id=" g8t4-mdq ")) + created = await people_service.create_person( + Person(given_names="Hig", last_name="Higgins", family_search_id=" g8t4-mdq ") + ) assert created.family_search_id == "G8T4-MDQ" with pytest.raises(PeopleError) as duplicate: - await people_service.create_person(Person(given_names="Duplicate", last_name="Hig", family_search_id="G8T4-MDQ")) + await people_service.create_person( + Person(given_names="Duplicate", last_name="Hig", family_search_id="G8T4-MDQ") + ) assert duplicate.value.category == ErrorCategory.CONFLICT with pytest.raises(PeopleError) as malformed: - await people_service.create_person(Person(given_names="Malformed", last_name="Person", family_search_id="not-an-id")) + await people_service.create_person( + Person(given_names="Malformed", last_name="Person", family_search_id="not-an-id") + ) assert malformed.value.category == ErrorCategory.VALIDATION diff --git a/tests/test_model_contract_guards.py b/tests/test_model_contract_guards.py index 4a61ac1..dd82872 100644 --- a/tests/test_model_contract_guards.py +++ b/tests/test_model_contract_guards.py @@ -174,8 +174,8 @@ def _relationship_loading_strategies() -> dict[str, dict[str, str | None]]: if keyword.arg != "sa_relationship_kwargs" or not isinstance(keyword.value, ast.Dict): continue for key, value in zip(keyword.value.keys, keyword.value.values, strict=True): - if isinstance(key, ast.Constant) and key.value == "lazy" and isinstance(value, ast.Constant): - lazy = value.value + if isinstance(key, ast.Constant) and key.value == "lazy": + lazy = _string_constant(value) strategies.setdefault(class_node.name, {})[attribute] = lazy return strategies diff --git a/tests/tools/test_export_import_migration.py b/tests/tools/test_export_import_migration.py index a51a57e..3ca7a08 100644 --- a/tests/tools/test_export_import_migration.py +++ b/tests/tools/test_export_import_migration.py @@ -2,23 +2,21 @@ from __future__ import annotations from datetime import UTC from datetime import datetime -from pathlib import Path from uuid import uuid4 from sqlalchemy import create_engine -from sqlalchemy import text from sqlalchemy import select +from sqlalchemy import text from sqlmodel import SQLModel +# Register table metadata. +from transcription.db import models as _models # noqa: F401 from transcription.db.migration import MigrationPaths from transcription.db.migration import export_bundle from transcription.db.migration import import_bundle from transcription.db.migration import migrate_via_bundle from transcription.db.migration import sqlite_url_from_path -# Register table metadata. -from transcription.db import models as _models # noqa: F401 - def test_export_import_migration_round_trips_db_and_uploads(tmp_path): source_db_path = tmp_path / "source.db" @@ -173,7 +171,10 @@ def test_export_import_migration_backfills_legacy_portraits_and_homepage_images( {"id": person_id}, ).one() photos = connection.execute( - text('select person_id, path, is_primary from "photo" order by person_id is not null desc, created_at asc') + text( + 'select person_id, path, is_primary from "photo" ' + "order by person_id is not null desc, created_at asc" + ) ).all() assert person_name[0] == "Legacy" assert person_name[1] == "Portrait" @@ -291,7 +292,9 @@ def test_migration_backfills_legacy_media_when_photo_table_contains_stale_rows(t target_engine = create_engine(target_db_url) try: with target_engine.connect() as connection: - photos = connection.execute(text('select person_id, path from "photo" order by person_id is null, path')).all() + photos = connection.execute( + text('select person_id, path from "photo" order by person_id is null, path') + ).all() # stale row must not survive; legacy portrait + homepage should be backfilled assert len(photos) == 2 assert any(row[0] is not None for row in photos) diff --git a/tests/ui/test_documents_page.py b/tests/ui/test_documents_page.py index 4ae2906..bda87ad 100644 --- a/tests/ui/test_documents_page.py +++ b/tests/ui/test_documents_page.py @@ -1,7 +1,7 @@ """Tests for the documents page routes and action handlers.""" -from datetime import date import re +from datetime import date import pytest import pytest_asyncio @@ -10,13 +10,13 @@ from sqlmodel import select from transcription.db import session_scope from transcription.db.models import Document from transcription.db.models import DocumentPerson +from transcription.db.models import DocumentTag from transcription.db.models import DocumentType from transcription.db.models import Job from transcription.db.models import Person from transcription.db.models import PersonRole -from transcription.db.models import Tag -from transcription.db.models import DocumentTag from transcription.db.models import Source +from transcription.db.models import Tag from transcription.ui.pages.documents_page import _resolve_selected_tag_labels # --- Helper Fixtures --- diff --git a/tests/ui/test_people_page.py b/tests/ui/test_people_page.py index bb05add..679848a 100644 --- a/tests/ui/test_people_page.py +++ b/tests/ui/test_people_page.py @@ -11,8 +11,8 @@ from transcription.db import session_scope from transcription.db.models import Document from transcription.db.models import DocumentPerson from transcription.db.models import Person -from transcription.db.models import PersonTag from transcription.db.models import PersonRole +from transcription.db.models import PersonTag from transcription.db.models import Photo from transcription.db.models import Source from transcription.db.models import Tag diff --git a/tools/export_import_migration.py b/tools/export_import_migration.py index 29847fa..c308b9c 100644 --- a/tools/export_import_migration.py +++ b/tools/export_import_migration.py @@ -6,10 +6,12 @@ Usage examples: uv run python tools/export_import_migration.py export --bundle-dir .migration-bundle 2) Import bundle into a fresh target DB + uploads root: - uv run python tools/export_import_migration.py import --bundle-dir .migration-bundle --target-db .\\data\\transcription-new.db --target-upload-dir .\\data-new + uv run python tools/export_import_migration.py import --bundle-dir .migration-bundle + --target-db .\\data\\transcription-new.db --target-upload-dir .\\data-new 3) One-shot rebuild flow: - uv run python tools/export_import_migration.py migrate --bundle-dir .migration-bundle --target-db .\\data\\transcription-new.db --target-upload-dir .\\data-new + uv run python tools/export_import_migration.py migrate --bundle-dir .migration-bundle + --target-db .\\data\\transcription-new.db --target-upload-dir .\\data-new """ from __future__ import annotations