V2 implementation step 1

This commit is contained in:
Jim Lancaster
2026-08-01 15:34:06 -05:00
parent 00ed176ac1
commit c46d1bd0bc
6 changed files with 212 additions and 136 deletions
+88 -48
View File
@@ -1,19 +1,29 @@
"""Tests for transcription.models — Document, Source, Job, Revision persistence and relationships."""
"""Tests for the V2 SQLModel persistence layer and relationships."""
from datetime import UTC
from datetime import datetime
from uuid import UUID
import pytest
from sqlalchemy.exc import IntegrityError
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentPersonRole
from transcription.db.models import Job
from transcription.db.models import JobSource
from transcription.db.models import JobSourceStatus
from transcription.db.models import JobStatus
from transcription.db.models import Revision
from transcription.db.models import Person
from transcription.db.models import Source
def _make_document(**overrides) -> Document:
defaults = {"name": "letter bundle"}
defaults = {
"name": "letter bundle",
"document_type": "letter",
"notes": "Family correspondence",
}
defaults.update(overrides)
return Document(**defaults)
@@ -26,6 +36,16 @@ def _persist_document(session) -> Document:
return document
def _persist_person(session, **overrides) -> Person:
defaults = {"full_name": "Ada Lovelace"}
defaults.update(overrides)
person = Person(**defaults)
session.add(person)
session.commit()
session.refresh(person)
return person
def _persist_job(session, document: Document) -> Job:
job = Job(document_id=document.id)
session.add(job)
@@ -34,13 +54,14 @@ def _persist_job(session, document: Document) -> Job:
return job
def _persist_source(session, document: Document, job: Job, **overrides) -> Source:
def _persist_source(session, document: Document, *, page_number: int = 1, **overrides) -> Source:
defaults = {
"document_id": document.id,
"job_id": job.id,
"page_number": page_number,
"upload_name": "letter.jpg",
"filename": "stored-letter.jpg",
"file_path": "/uploads/stored-letter.jpg",
"raw_transcription": "Original machine text",
}
defaults.update(overrides)
source = Source(**defaults)
@@ -50,6 +71,20 @@ def _persist_source(session, document: Document, job: Job, **overrides) -> Sourc
return source
def _persist_job_source(session, job: Job, source: Source, **overrides) -> JobSource:
defaults = {
"job_id": job.id,
"source_id": source.id,
"status": JobSourceStatus.PENDING,
}
defaults.update(overrides)
job_source = JobSource(**defaults)
session.add(job_source)
session.commit()
session.refresh(job_source)
return job_source
class TestDocumentModel:
def test_can_be_persisted(self, session):
document = _persist_document(session)
@@ -60,6 +95,8 @@ class TestDocumentModel:
def test_defaults_are_populated(self, session):
document = _persist_document(session)
assert isinstance(document.id, UUID)
assert document.created_at is not None
assert document.updated_at is not None
class TestJobModel:
@@ -78,7 +115,7 @@ class TestJobModel:
assert job.date_created is not None
assert job.date_updated is not None
def test_transitions_to_transcribed(self, session):
def test_transitions_to_completed(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
@@ -87,79 +124,82 @@ class TestJobModel:
session.commit()
session.refresh(job)
job.status = JobStatus.TRANSCRIBED
job.status = JobStatus.COMPLETED
session.add(job)
session.commit()
session.refresh(job)
assert job.status == JobStatus.TRANSCRIBED
assert job.status == JobStatus.COMPLETED
class TestSourceModel:
def test_can_be_created_for_document_and_job(self, session):
def test_can_be_created_for_document(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
source = _persist_source(session, document, job)
source = _persist_source(session, document)
fetched = session.get(Source, source.id)
assert fetched is not None
assert fetched.document_id == document.id
assert fetched.job_id == job.id
assert fetched.page_number == 1
assert fetched.date_uploaded is not None
class TestRevisionModel:
def test_revision_persists_for_source(self, session):
def test_revised_text_is_supported(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
source = _persist_source(session, document, job)
source = _persist_source(session, document, revised_text="Edited output")
revision = Revision(source_id=source.id, text="Edited revision text")
session.add(revision)
session.commit()
session.refresh(revision)
fetched = session.get(Revision, revision.id)
fetched = session.get(Source, source.id)
assert fetched is not None
assert fetched.text == "Edited revision text"
assert fetched.date_created is not None
assert fetched.revised_text == "Edited output"
def test_source_id_is_unique(self, session):
class TestPersonAndDocumentPersonModel:
def test_document_person_role_is_unique_per_document_person(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
source = _persist_source(session, document, job)
person = _persist_person(session)
first = Revision(source_id=source.id, text="First")
first = DocumentPerson(document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR)
session.add(first)
session.commit()
duplicate = Revision(source_id=source.id, text="Duplicate")
duplicate = DocumentPerson(document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR)
session.add(duplicate)
with pytest.raises(IntegrityError):
session.commit()
class TestRelationships:
def test_document_exposes_jobs_and_sources(self, session):
class TestJobSourceModel:
def test_job_source_persists_json_payloads(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
_persist_source(session, document, job)
source = _persist_source(session, document)
job_source = _persist_job_source(
session,
job,
source,
raw_transcription="Page transcript",
ai_metadata={"confidence": 0.91, "boxes": [{"x": 1, "y": 2}]},
raw_api_response={"provider": "test"},
)
fetched = session.get(JobSource, job_source.id)
assert fetched is not None
assert fetched.status == JobSourceStatus.PENDING
assert fetched.ai_metadata == {"confidence": 0.91, "boxes": [{"x": 1, "y": 2}]}
assert fetched.raw_api_response == {"provider": "test"}
class TestRelationships:
def test_document_exposes_jobs_sources_and_people(self, session):
document = _persist_document(session)
_persist_job(session, document)
_persist_source(session, document)
person = _persist_person(session)
link = DocumentPerson(document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR)
session.add(link)
session.commit()
session.refresh(document)
assert len(document.jobs) == 1
assert len(document.sources) == 1
def test_source_exposes_optional_single_revision(self, session):
document = _persist_document(session)
job = _persist_job(session, document)
source = _persist_source(session, document, job)
assert source.revision is None
revision = Revision(source_id=source.id, text="Edited")
session.add(revision)
session.commit()
session.refresh(source)
assert source.revision is not None
assert source.revision.text == "Edited"
assert len(document.document_people) == 1