generated from john/python-template
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
You are working in the `transcription` repository (Python 3.12+, FastAPI, NiceGUI, SQLModel/SQLAlchemy, Pydantic V2, asyncio). Follow `.github/instructions/ui.instructions.md` and `.github/instructions/services.instructions.md` for any code you touch, and keep `docs/ver4/*` as canonical authority for intended behavior. Do not modify unrelated code.
|
||||||
|
|
||||||
|
**Prerequisite:** This prompt assumes Phase 1 (`docs/phase1-codex-prompt.md`) is already merged — the SQLite atomic job claim and the `JobSource(job_id, source_id)` uniqueness constraint should already exist. If they do not, stop and flag this before proceeding, since this phase's regression tests depend on that groundwork.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement **Phase 2 (Enforcement hardening)** from `docs/architecture-code-review-2026-08-20.md`:
|
||||||
|
|
||||||
|
### 1. [HIGH-02] General UI media resolution falls back to basename instead of failing closed
|
||||||
|
**Location:** `src/transcription/ui/components/media_urls.py:41-74`, `src/transcription/ui/pages/sources_page.py:261-265`
|
||||||
|
|
||||||
|
**Problem:** When a stored path is not provably under `upload_dir` and does not match an approved prefix, `resolve_media_url` still returns `/uploads/{path_obj.name}`. The source detail page passes that URL directly to the viewer. If an unmanaged or stale DB path shares a basename with another upload, the UI can render the wrong file instead of the recorded source — violating record fidelity and the "controlled resolver paths" rule (`REQ-4-031`). This is distinct from print/export media, which is already correctly record-validated in `src/transcription/api/v4_print.py:31-54`.
|
||||||
|
|
||||||
|
**Required fix:**
|
||||||
|
- In `resolve_media_url` (`src/transcription/ui/components/media_urls.py`), remove the basename fallback. Return `None` (or an explicit "unavailable" sentinel/token consistent with how the rest of the UI layer signals missing/invalid media — check `error_presenter.py` and existing viewer components for the established pattern) unless the path is validated as either (a) safely resolvable under the configured `upload_dir`, or (b) an already-approved upload-relative form recognized elsewhere in the codebase (mirror the validation used by `v4_print.py`).
|
||||||
|
- Update `src/transcription/ui/pages/sources_page.py:261-265` (and any other call site relying on the old fallback behavior) to handle the `None`/unavailable case gracefully — e.g. show a clear "media unavailable" state in the viewer rather than crashing or rendering a blank/broken image.
|
||||||
|
- Add a dedicated resolver test suite (new or extended, e.g. `tests/ui/test_media_urls.py`) covering: a validated path under `upload_dir` (should resolve), an unmanaged absolute path outside `upload_dir` (should fail closed), a stale/nonexistent relative path (should fail closed), and a basename-collision scenario where an unmanaged path shares a filename with a legitimate upload (must NOT resolve to the wrong file). This directly satisfies the meta-tooling recommendation in `docs/architecture-code-review-2026-08-20.md` section 8: "Add a deterministic resolver test suite for `resolve_media_url`, not just `public_media_path_label`."
|
||||||
|
|
||||||
|
### 2. Deterministic regression tests for Phase 1 fixes
|
||||||
|
**Rationale:** Phase 1 fixed the underlying atomicity/uniqueness issues; this phase locks them in with deterministic, always-run tests so no future change can silently regress them (per the review's Action Plan, item 2, and section 8 meta-tooling recommendations).
|
||||||
|
|
||||||
|
**Required work:**
|
||||||
|
- Confirm (or add if missing) a concurrency regression test that races two concurrent `claim_next_queued_job` calls against the same queued job on SQLite and asserts exactly one caller wins. If Phase 1 already added this test, review it for robustness (e.g. does it actually force a race rather than relying on incidental ordering?) and strengthen it if needed — for example by using two independent sessions/connections and asserting via `asyncio.gather` that exactly one result is non-`None`.
|
||||||
|
- Confirm (or add if missing) a deterministic test asserting that creating a second `JobSource` for an existing `(job_id, source_id)` pair is rejected/handled predictably (not a silent duplicate). Ensure this test exercises the actual insertion code path used by `services/sources.py`, not just the raw model constraint.
|
||||||
|
- Both tests should live alongside the existing service test files for `jobs.py`/`sources.py` (check `tests/services/` for the correct location and naming convention).
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Run `pytest` (via the project's normal invocation, e.g. `uv run pytest`, potentially through `tools/run_destructive_tests.py --auto-restore -- pytest` if that's how destructive/DB tests are run in this repo — check `pyproject.toml`/README) and ensure all tests pass, including new/strengthened tests.
|
||||||
|
- Run `ruff check` and `ty check` and ensure no new issues are introduced by your changes (do not attempt to fix the pre-existing baseline debt — that is Phase 5's responsibility).
|
||||||
|
- Do not touch error taxonomy (Phase 3), UI/service consolidation (Phase 4), or provenance/env config and ruff/ty baseline cleanup (Phase 5) — those are out of scope for this task.
|
||||||
|
|
||||||
|
Report back with: files changed, the fail-closed validation logic chosen for `resolve_media_url`, the new/updated test coverage, and final `pytest`/`ruff`/`ty` results.
|
||||||
@@ -24,6 +24,7 @@ from .config import get_settings
|
|||||||
from .db import create_all
|
from .db import create_all
|
||||||
from .db import dispose_database_runtime
|
from .db import dispose_database_runtime
|
||||||
from .db import initialize_database_runtime
|
from .db import initialize_database_runtime
|
||||||
|
from .db import normalize_legacy_status_spellings
|
||||||
from .services import ServiceBundle
|
from .services import ServiceBundle
|
||||||
from .ui import register_pages
|
from .ui import register_pages
|
||||||
from .worker import worker_consumer_lifespan
|
from .worker import worker_consumer_lifespan
|
||||||
@@ -42,6 +43,7 @@ async def _lifespan(app: FastAPI):
|
|||||||
|
|
||||||
if settings.should_bootstrap_schema:
|
if settings.should_bootstrap_schema:
|
||||||
await create_all(engine=app.state.runtime.engine)
|
await create_all(engine=app.state.runtime.engine)
|
||||||
|
await normalize_legacy_status_spellings(engine=app.state.runtime.engine)
|
||||||
|
|
||||||
settings.upload_dir.mkdir(parents=True, exist_ok=True)
|
settings.upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
settings.prompt_dir.mkdir(parents=True, exist_ok=True)
|
settings.prompt_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .operations import create_all
|
from .operations import create_all
|
||||||
|
from .operations import normalize_legacy_status_spellings
|
||||||
from .runtime import dispose_database_runtime
|
from .runtime import dispose_database_runtime
|
||||||
from .runtime import initialize_database_runtime
|
from .runtime import initialize_database_runtime
|
||||||
from .session import session_scope
|
from .session import session_scope
|
||||||
@@ -8,6 +9,7 @@ __all__ = [
|
|||||||
"create_all",
|
"create_all",
|
||||||
"dispose_database_runtime",
|
"dispose_database_runtime",
|
||||||
"initialize_database_runtime",
|
"initialize_database_runtime",
|
||||||
|
"normalize_legacy_status_spellings",
|
||||||
"session_scope",
|
"session_scope",
|
||||||
"transaction_scope",
|
"transaction_scope",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import inspect as sqlalchemy_inspect
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
@@ -10,6 +12,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|||||||
|
|
||||||
from .engine import resolve_engine
|
from .engine import resolve_engine
|
||||||
from .models import DocumentType
|
from .models import DocumentType
|
||||||
|
from .models import JobSourceStatus
|
||||||
from .models import PersonRole
|
from .models import PersonRole
|
||||||
from .registries import BUILT_IN_DOCUMENT_TYPES
|
from .registries import BUILT_IN_DOCUMENT_TYPES
|
||||||
from .registries import BUILT_IN_PERSON_ROLES
|
from .registries import BUILT_IN_PERSON_ROLES
|
||||||
@@ -29,6 +32,47 @@ async def create_all(*, engine: AsyncEngine | None = None) -> None:
|
|||||||
logger.debug("Database schema bootstrap complete for database_url=%s", active_engine.url)
|
logger.debug("Database schema bootstrap complete for database_url=%s", active_engine.url)
|
||||||
|
|
||||||
|
|
||||||
|
async def normalize_legacy_status_spellings(*, engine: AsyncEngine | None = None) -> int:
|
||||||
|
"""Normalize legacy enum-name spellings to canonical enum values.
|
||||||
|
|
||||||
|
Historical databases may carry ``TRANSCRIBED``/``FAILED``-style enum *names*
|
||||||
|
in ``job_source.status`` or ``execution_attempt.status``. Runtime models
|
||||||
|
expect canonical lowercase values, so stale rows must be normalized before
|
||||||
|
ORM reads.
|
||||||
|
"""
|
||||||
|
active_engine = engine or resolve_engine()
|
||||||
|
if not hasattr(active_engine, "begin"):
|
||||||
|
return 0
|
||||||
|
replacements = {
|
||||||
|
status.name: status.value
|
||||||
|
for status in JobSourceStatus
|
||||||
|
if status.name != status.value
|
||||||
|
}
|
||||||
|
|
||||||
|
def _normalize(sync_connection) -> int:
|
||||||
|
inspector = sqlalchemy_inspect(sync_connection)
|
||||||
|
table_names = set(inspector.get_table_names())
|
||||||
|
if not table_names:
|
||||||
|
return 0
|
||||||
|
fixed = 0
|
||||||
|
for table_name in ("job_source", "execution_attempt"):
|
||||||
|
if table_name not in table_names:
|
||||||
|
continue
|
||||||
|
for legacy, canonical in replacements.items():
|
||||||
|
result = sync_connection.execute(
|
||||||
|
text(f'update "{table_name}" set status = :canonical where status = :legacy'),
|
||||||
|
{"canonical": canonical, "legacy": legacy},
|
||||||
|
)
|
||||||
|
fixed += result.rowcount or 0
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
async with active_engine.begin() as connection:
|
||||||
|
fixed_rows = await connection.run_sync(_normalize)
|
||||||
|
if fixed_rows:
|
||||||
|
logger.warning("Normalized %s legacy status row(s) to canonical spellings", fixed_rows)
|
||||||
|
return fixed_rows
|
||||||
|
|
||||||
|
|
||||||
async def seed_registry_defaults(*, engine: AsyncEngine | None = None) -> None:
|
async def seed_registry_defaults(*, engine: AsyncEngine | None = None) -> None:
|
||||||
"""Seed default registry rows for role and document type taxonomies."""
|
"""Seed default registry rows for role and document type taxonomies."""
|
||||||
active_engine = engine or resolve_engine()
|
active_engine = engine or resolve_engine()
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ Some who get this book will consider the group picture the best thing in the boo
|
|||||||
|
|
||||||
We are much impressed with the future possibilities of getting a complete geneology [sic] of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
|
We are much impressed with the future possibilities of getting a complete geneology [sic] of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
|
||||||
|
|
||||||
~2~
|
-2-
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ provider: openrouter
|
|||||||
model: openai/gpt-5.3-codex
|
model: openai/gpt-5.3-codex
|
||||||
---
|
---
|
||||||
[document body typeset]
|
[document body typeset]
|
||||||
JOHN E. COCHRAN
|
|
||||||
FAMILY ASSOCIATION
|
|
||||||
|
|
||||||
Family Only
|
Family Only
|
||||||
Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History | Next Reunion | JECMEF
|
Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History | Next Reunion | JECMEF
|
||||||
|
|
||||||
@@ -41,8 +38,7 @@ along the icebergs and the walrus were 'heisted' on board by the use of the cran
|
|||||||
tons of freight and the beasts were so huge that they made the pulleys just creak. They were
|
tons of freight and the beasts were so huge that they made the pulleys just creak. They were
|
||||||
over 12 feet long, as big around as three cows, had no feet but toenails on their flippers or
|
over 12 feet long, as big around as three cows, had no feet but toenails on their flippers or
|
||||||
flappers, no head but their body just suddenly ended with a hole for a mouth and big bristles
|
flappers, no head but their body just suddenly ended with a hole for a mouth and big bristles
|
||||||
[photograph of people standing in snow]
|
[photograph inserted at right of paragraph]
|
||||||
|
|
||||||
all around it similar to a currycomb in coarseness; no ears but huge tusks of ivory. They are
|
all around it similar to a currycomb in coarseness; no ears but huge tusks of ivory. They are
|
||||||
the most repulsive looking animals imaginable and tho I have always read about them I never
|
the most repulsive looking animals imaginable and tho I have always read about them I never
|
||||||
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
|
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
|
||||||
@@ -50,8 +46,8 @@ them looked warty. They must have weighed two ton at least. Ere we got them back
|
|||||||
to the natives they were getting extremely odiferous—in fact, you could scarcely stay on the
|
to the natives they were getting extremely odiferous—in fact, you could scarcely stay on the
|
||||||
ship with any degree of comfort unless you had per chance lost your sense of smell.
|
ship with any degree of comfort unless you had per chance lost your sense of smell.
|
||||||
|
|
||||||
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile
|
Then we went north to a few minutes beyond the 70th degree of latitude and thot [sic] for awhile
|
||||||
we would go to Wrangell Island where some men from Steffonsons ship were supposed to be
|
we would go to Wrangell Island where some men from Steffonsons [sic] ship were supposed to be
|
||||||
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in
|
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in
|
||||||
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
|
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
|
||||||
years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or
|
years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or
|
||||||
@@ -65,16 +61,16 @@ bears with faces. I guess they had never seen white women, not so many at one ti
|
|||||||
We went ashore in 2 life boats and a launch pulling them. On the launch was a six piece band
|
We went ashore in 2 life boats and a launch pulling them. On the launch was a six piece band
|
||||||
playing and the rear of the last life boat was the movie man. 'Twas very thrilling.
|
playing and the rear of the last life boat was the movie man. 'Twas very thrilling.
|
||||||
|
|
||||||
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists'[sic]
|
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists' [sic]
|
||||||
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
|
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
|
||||||
something to buy. Prices raised right before your eyes. One would but[sic] something for $1.00
|
something to buy. Prices raised right before your eyes. One would [buy?] something for $1.00
|
||||||
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
|
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
|
||||||
have it. One man I was sort of taking care of, tho he had his son along for the purpose,
|
have it. One man I was sort of taking care of, tho he had his son along for the purpose,
|
||||||
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one
|
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one
|
||||||
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
|
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
|
||||||
little natives throw them at the flocks of geese and ducks which fly close over the village and
|
little natives throw them at the flocks of geese and ducks which fly close over the village and
|
||||||
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
|
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
|
||||||
come down and the natives capture them. There was more junk brot aboard than baggage, I
|
come down and the natives capture them. There was more junk brot [sic] aboard than baggage, I
|
||||||
do believe. And they say that at the first stop it was worse than here. The red flag was flying
|
do believe. And they say that at the first stop it was worse than here. The red flag was flying
|
||||||
over Whalen and the Russian soldiers were there—a few, one or two or three, I forget the
|
over Whalen and the Russian soldiers were there—a few, one or two or three, I forget the
|
||||||
number.
|
number.
|
||||||
@@ -83,8 +79,8 @@ We got home yesterday morning at 5 a.m. but missed the first lighter in so had t
|
|||||||
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
|
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
|
||||||
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
|
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
|
||||||
all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he
|
all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he
|
||||||
didn't and Polly would [sit] in my lap and I don't know much about the youngest one of yours so
|
didn't and Polly would in my lap and I don't know much about the youngest one of yours so
|
||||||
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf.
|
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf [sic].
|
||||||
|
|
||||||
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
|
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
|
||||||
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
||||||
@@ -117,4 +113,4 @@ Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
|
|||||||
|
|
||||||
Up
|
Up
|
||||||
|
|
||||||
jecochranclan.org - Contact webmaster
|
jecochranclan.org ~ Contact webmaster
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ ISBILL & MOSER
|
|||||||
DEALERS IN
|
DEALERS IN
|
||||||
GENERAL MERCHANDISE
|
GENERAL MERCHANDISE
|
||||||
|
|
||||||
Vonore, Tenn., [handwritten: May 27-] 191[handwritten: 3?]
|
Vonore, Tenn., [handwritten: Aug 27-] 191[handwritten: 2]
|
||||||
|
|
||||||
[handwritten: Dear Uncle Aunt & Cousins
|
[handwritten: Dear [Nuch?] Aunt & Cousin
|
||||||
I was at home a
|
I was at home a
|
||||||
few nights ago & saw a
|
few nights ago & saw a
|
||||||
letter from your folks, so
|
letter from your folks, so
|
||||||
I decided to write you
|
I decided to write you
|
||||||
a few lines myself &
|
a few lines in regard of
|
||||||
am contemplating a
|
I am contemplating a
|
||||||
trip out west next summer
|
trip out west next summer
|
||||||
& want lots of places to go
|
& want [lots?] of [places?] to go
|
||||||
where I can stop.
|
where I am [from?].
|
||||||
|
|
||||||
Am getting
|
Am getting
|
||||||
up in years & unmarried,
|
up in years & [unmarried?].
|
||||||
so you see the object of
|
So you see the object of
|
||||||
my trip, is to get a wife.
|
my trip, is to get a wife
|
||||||
If there is any old maid
|
If there is any old maid
|
||||||
or widows out there I
|
or widow out there I
|
||||||
want you to [illegible] them
|
want you to [hiev?] them
|
||||||
at [illegible] me at them
|
at one [find?] me at them
|
||||||
as soon as I get there.]
|
as soon as I get there.]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import warnings
|
|||||||
import pytest
|
import pytest
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
from sqlalchemy.exc import SAWarning
|
from sqlalchemy.exc import SAWarning
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
@@ -16,8 +17,13 @@ from transcription.config import SqliteSettings
|
|||||||
from transcription.db import create_all
|
from transcription.db import create_all
|
||||||
from transcription.db import dispose_database_runtime
|
from transcription.db import dispose_database_runtime
|
||||||
from transcription.db import initialize_database_runtime
|
from transcription.db import initialize_database_runtime
|
||||||
|
from transcription.db import normalize_legacy_status_spellings
|
||||||
from transcription.db import session_scope
|
from transcription.db import session_scope
|
||||||
|
from transcription.db.models import Document
|
||||||
from transcription.db.models import DocumentType
|
from transcription.db.models import DocumentType
|
||||||
|
from transcription.db.models import Job
|
||||||
|
from transcription.db.models import JobSource
|
||||||
|
from transcription.db.models import JobSourceStatus
|
||||||
from transcription.db.models import PersonRole
|
from transcription.db.models import PersonRole
|
||||||
from transcription.db.models import Source
|
from transcription.db.models import Source
|
||||||
|
|
||||||
@@ -151,6 +157,60 @@ async def test_create_all_declares_hot_path_indexes(tmp_path):
|
|||||||
await dispose_database_runtime()
|
await dispose_database_runtime()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_normalize_legacy_status_spellings_repairs_job_source_status_rows(tmp_path):
|
||||||
|
settings = Settings(
|
||||||
|
openrouter_api_key="test-key",
|
||||||
|
database=SqliteSettings(path=str(tmp_path / "legacy-status.db")),
|
||||||
|
environment="test",
|
||||||
|
)
|
||||||
|
runtime = initialize_database_runtime(settings=settings)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await create_all(engine=runtime.engine)
|
||||||
|
async with AsyncSession(runtime.engine, expire_on_commit=False) as session:
|
||||||
|
document = Document(name="legacy-status-doc")
|
||||||
|
session.add(document)
|
||||||
|
await session.flush()
|
||||||
|
job = Job(document_id=document.id)
|
||||||
|
session.add(job)
|
||||||
|
await session.flush()
|
||||||
|
source = Source(
|
||||||
|
document_id=document.id,
|
||||||
|
page_number=1,
|
||||||
|
upload_name="legacy.jpg",
|
||||||
|
filename="legacy.jpg",
|
||||||
|
file_path="uploads/legacy.jpg",
|
||||||
|
file_hash="a" * 64,
|
||||||
|
file_size_bytes=1,
|
||||||
|
)
|
||||||
|
session.add(source)
|
||||||
|
await session.flush()
|
||||||
|
job_source = JobSource(job_id=job.id, source_id=source.id, status=JobSourceStatus.PENDING)
|
||||||
|
session.add(job_source)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(job_source)
|
||||||
|
|
||||||
|
async with runtime.engine.begin() as connection:
|
||||||
|
await connection.execute(
|
||||||
|
text('update "job_source" set status = :status where status = :expected'),
|
||||||
|
{"status": "TRANSCRIBED", "expected": JobSourceStatus.PENDING.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
fixed_rows = await normalize_legacy_status_spellings(engine=runtime.engine)
|
||||||
|
assert fixed_rows == 1
|
||||||
|
|
||||||
|
async with runtime.engine.connect() as connection:
|
||||||
|
status = (
|
||||||
|
await connection.execute(
|
||||||
|
text('select status from "job_source"'),
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
assert status == "transcribed"
|
||||||
|
finally:
|
||||||
|
await dispose_database_runtime()
|
||||||
|
|
||||||
|
|
||||||
def test_metadata_has_no_unresolvable_table_cycle():
|
def test_metadata_has_no_unresolvable_table_cycle():
|
||||||
"""create_all must be able to order every table, including on PostgreSQL."""
|
"""create_all must be able to order every table, including on PostgreSQL."""
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
|||||||
Reference in New Issue
Block a user