generated from john/python-template
83 KiB
83 KiB
In [1]:
%load_ext autoreload
%autoreload 2In [ ]:
from pathlib import Path
from uuid import UUID
import rich
from transcription.config import configure_logging
from transcription.config import get_settings
from transcription.db.operations import create_all
from transcription.db.runtime import get_engine
from transcription.db.runtime import get_session
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
from transcription.services import ServiceBundle
from transcription.services.documents import DocumentService
from transcription.services.documents import create_upload_job
from transcription.services.jobs import JobService
from transcription.services.store import store_file
from transcription.worker import process_queued_job
configure_logging()
settings = get_settings(worker_max_retries=2)
engine = get_engine(settings=settings)
await create_all(engine=engine)
services = ServiceBundle()2026-06-27 22:09:01 DEBUG | Logging configured 2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db 2026-06-27 22:09:01 DEBUG | Database schema bootstrap complete for database_url=sqlite+aiosqlite:///./transcription.db
In [3]:
jobs = await services.jobs.list_jobs()
rich.print(jobs)[ Job( id=UUID('c79d20d1-ab5c-4b3b-a898-13eac7d2c71a'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 2, 55, 13, 69063), document_id=UUID('105e84b2-d773-4abb-bd49-8dc26b966046'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 2, 55, 13, 69054) ), Job( id=UUID('271b688e-63c7-4652-9ccf-5c1dd3f61058'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 2, 55, 17, 189346), document_id=UUID('06f49121-9b98-4b0e-b879-7a3b959fe984'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 2, 55, 17, 189338) ), Job( id=UUID('4386391c-3ce9-412a-9bd4-46b3860d0ef7'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 2, 55, 19, 161570), document_id=UUID('ac04c024-25ca-4707-8cd8-641606976344'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 2, 55, 19, 161560) ), Job( id=UUID('6637b36d-82e1-41e1-8776-037183851d30'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 2, 55, 26, 921982), document_id=UUID('9c6d8632-fe6b-4f70-bb9b-5e8d762b4b3e'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 2, 55, 26, 921974) ), Job( id=UUID('758f457a-343c-4b4a-98af-278875303803'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 3, 4, 40, 999438), document_id=UUID('0fb70d21-6409-40cc-83df-abce2917f367'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 3, 4, 40, 999427) ), Job( id=UUID('01060a33-464d-48c9-b4bb-f1f6acd91820'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 3, 7, 6, 284063), document_id=UUID('c8d0eacb-87dd-4d88-a576-ce810436dc45'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 3, 7, 6, 284053) ), Job( id=UUID('50800d67-fece-452e-b3ff-b7045fb1838f'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 3, 8, 2, 737235), document_id=UUID('273a58fd-4c4d-4e3a-93dd-4f391650e646'), status=<JobStatus.TRANSCRIBED: 'transcribed'>, created_at=datetime.datetime(2026, 6, 28, 3, 8, 2, 737226) ) ]
In [4]:
doc_path = Path("uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg")
doc_path.name.rpartition("_")[2]Out [4]:
'Time Rolls On - page 013.jpg'
In [5]:
async with get_session(settings=settings) as session:
result = await create_upload_job(
session=session,
filename=doc_path.name.rpartition("_")[2],
file_bytes=doc_path.read_bytes(),
settings=settings,
)
rich.print(result)2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db 2026-06-27 22:09:01 INFO | Stored uploaded file: uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg 2026-06-27 22:09:01 INFO | Created upload job document_id=7102ad0c-dcc3-4164-a86f-4e601fe31e70 job_id=5bb45fc1-6b3b-4e92-9942-f8f059baaaaf
UploadJobResult( document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'), job_id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'), stored_path=PosixPath('uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg'), original_filename='Time Rolls On - page 013.jpg' )
In [6]:
import random
job = random.choice(await services.jobs.query_jobs(status=JobStatus.QUEUED))
rich.print(job)Job( id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351464), document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'), status=<JobStatus.QUEUED: 'queued'>, created_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351451) )
In [7]:
async with get_session(settings=settings) as session:
jobs = await services.jobs.query_jobs(
status=JobStatus.QUEUED,
session=session,
)
job = random.choice(jobs)
job = Job.model_copy(job)
rich.print(job)2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
Job( id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'), retry_count=0, updated_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351464), document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'), status=<JobStatus.QUEUED: 'queued'>, created_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351451) )
In [ ]:
async with get_session(settings=settings) as session:
await process_queued_job(
job,
services=services,
settings=settings,
session=session,
)
rich.print(job)
2026-06-27 22:09:06 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
[31m---------------------------------------------------------------------------[39m [31mInvalidRequestError[39m Traceback (most recent call last) [36mCell[39m[36m [39m[32mIn[8][39m[32m, line 2[39m [32m 1[39m [38;5;28;01masync[39;00m [38;5;28;01mwith[39;00m get_session(settings=settings) [38;5;28;01mas[39;00m session: [32m----> [39m[32m2[39m await process_job( [32m 3[39m job, [32m 4[39m services=services, [32m 5[39m settings=settings, [36mFile [39m[32m~/Documents/transcription/src/transcription/worker.py:106[39m, in [36mprocess_job[39m[34m(job, services, settings, session)[39m [32m 104[39m [38;5;28;01mcase[39;00m[38;5;250m [39m[38;5;28;01m_[39;00m: [32m 105[39m [38;5;28;01mreturn[39;00m [32m--> [39m[32m106[39m [38;5;28;01mreturn[39;00m [38;5;28;01mawait[39;00m services.jobs.update_job(job, session=session) [36mFile [39m[32m~/Documents/transcription/src/transcription/services/jobs.py:50[39m, in [36mJobService.update_job[39m[34m(self, job, session)[39m [32m 48[39m [38;5;28;01mawait[39;00m _session.merge(job) [32m 49[39m [38;5;28;01mawait[39;00m _session.commit() [32m---> [39m[32m50[39m [38;5;28;01mawait[39;00m _session.refresh(job) [32m 51[39m [38;5;28;01mreturn[39;00m job [36mFile [39m[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py:328[39m, in [36mAsyncSession.refresh[39m[34m(self, instance, attribute_names, with_for_update)[39m [32m 308[39m [38;5;28;01masync[39;00m [38;5;28;01mdef[39;00m[38;5;250m [39m[34mrefresh[39m( [32m 309[39m [38;5;28mself[39m, [32m 310[39m instance: [38;5;28mobject[39m, [32m 311[39m attribute_names: Optional[Iterable[[38;5;28mstr[39m]] = [38;5;28;01mNone[39;00m, [32m 312[39m with_for_update: ForUpdateParameter = [38;5;28;01mNone[39;00m, [32m 313[39m ) -> [38;5;28;01mNone[39;00m: [32m 314[39m [38;5;250m [39m[33;03m"""Expire and refresh the attributes on the given instance.[39;00m [32m 315[39m [32m 316[39m [33;03m A query will be issued to the database and all attributes will be[39;00m [32m (...)[39m[32m 325[39m [32m 326[39m [33;03m """[39;00m [32m--> [39m[32m328[39m [38;5;28;01mawait[39;00m greenlet_spawn( [32m 329[39m [38;5;28mself[39m.sync_session.refresh, [32m 330[39m instance, [32m 331[39m attribute_names=attribute_names, [32m 332[39m with_for_update=with_for_update, [32m 333[39m ) [36mFile [39m[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py:190[39m, in [36mgreenlet_spawn[39m[34m(fn, _require_await, *args, **kwargs)[39m [32m 185[39m [38;5;66;03m# runs the function synchronously in gl greenlet. If the execution[39;00m [32m 186[39m [38;5;66;03m# is interrupted by await_only, context is not dead and result is a[39;00m [32m 187[39m [38;5;66;03m# coroutine to wait. If the context is dead the function has[39;00m [32m 188[39m [38;5;66;03m# returned, and its result can be returned.[39;00m [32m 189[39m switch_occurred = [38;5;28;01mFalse[39;00m [32m--> [39m[32m190[39m result = [30;43mcontext[39;49m[30;43m.[39;49m[30;43mswitch[39;49m[30;43m([39;49m[30;43m*[39;49m[30;43margs[39;49m[30;43m,[39;49m[30;43m [39;49m[30;43m*[39;49m[30;43m*[39;49m[30;43mkwargs[39;49m[30;43m)[39;49m [32m 191[39m [38;5;28;01mwhile[39;00m [38;5;129;01mnot[39;00m context.dead: [32m 192[39m switch_occurred = [38;5;28;01mTrue[39;00m [36mFile [39m[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3155[39m, in [36mSession.refresh[39m[34m(self, instance, attribute_names, with_for_update)[39m [32m 3152[39m [38;5;28;01mexcept[39;00m exc.NO_STATE [38;5;28;01mas[39;00m err: [32m 3153[39m [38;5;28;01mraise[39;00m exc.UnmappedInstanceError(instance) [38;5;28;01mfrom[39;00m[38;5;250m [39m[34;01merr[39;00m [32m-> [39m[32m3155[39m [30;43mself[39;49m[30;43m.[39;49m[30;43m_expire_state[39;49m[30;43m([39;49m[30;43mstate[39;49m[30;43m,[39;49m[30;43m [39;49m[30;43mattribute_names[39;49m[30;43m)[39;49m [32m 3157[39m [38;5;66;03m# this autoflush previously used to occur as a secondary effect[39;00m [32m 3158[39m [38;5;66;03m# of the load_on_ident below. Meaning we'd organize the SELECT[39;00m [32m 3159[39m [38;5;66;03m# based on current DB pks, then flush, then if pks changed in that[39;00m [32m 3160[39m [38;5;66;03m# flush, crash. this was unticketed but discovered as part of[39;00m [32m 3161[39m [38;5;66;03m# #8703. So here, autoflush up front, dont autoflush inside[39;00m [32m 3162[39m [38;5;66;03m# load_on_ident.[39;00m [32m 3163[39m [38;5;28mself[39m._autoflush() [36mFile [39m[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3281[39m, in [36mSession._expire_state[39m[34m(self, state, attribute_names)[39m [32m 3276[39m [38;5;28;01mdef[39;00m[38;5;250m [39m[34m_expire_state[39m( [32m 3277[39m [38;5;28mself[39m, [32m 3278[39m state: InstanceState[Any], [32m 3279[39m attribute_names: Optional[Iterable[[38;5;28mstr[39m]], [32m 3280[39m ) -> [38;5;28;01mNone[39;00m: [32m-> [39m[32m3281[39m [30;43mself[39;49m[30;43m.[39;49m[30;43m_validate_persistent[39;49m[30;43m([39;49m[30;43mstate[39;49m[30;43m)[39;49m [32m 3282[39m [38;5;28;01mif[39;00m attribute_names: [32m 3283[39m state._expire_attributes(state.dict, attribute_names) [36mFile [39m[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:4139[39m, in [36mSession._validate_persistent[39m[34m(self, state)[39m [32m 4137[39m [38;5;28;01mdef[39;00m[38;5;250m [39m[34m_validate_persistent[39m([38;5;28mself[39m, state: InstanceState[Any]) -> [38;5;28;01mNone[39;00m: [32m 4138[39m [38;5;28;01mif[39;00m [38;5;129;01mnot[39;00m [38;5;28mself[39m.identity_map.contains_state(state): [32m-> [39m[32m4139[39m [38;5;28;01mraise[39;00m sa_exc.InvalidRequestError( [32m 4140[39m [33m"[39m[33mInstance [39m[33m'[39m[38;5;132;01m%s[39;00m[33m'[39m[33m is not persistent within this Session[39m[33m"[39m [32m 4141[39m % state_str(state) [32m 4142[39m ) [31mInvalidRequestError[39m: Instance '<Job at 0x7b19c4bb0b40>' is not persistent within this Session
In [ ]:
doc_path = Path("uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg")
idx, og_name = doc_path.name.rpartition("_")[::2]
doc_id = UUID(idx)
doc = Document(id=doc_id, filename=og_name, file_path=str(doc_path))
document_service = DocumentService()
doc = await document_service.create_document(document=doc)
rich.print(doc)In [ ]:
docs = await document_service.list_documents()
rich.print(docs)In [ ]:
job = await services.jobs.read_job(UUID("6400c83d-e245-497c-902b-d8bc9db8a216"))
rich.print(job)
rich.print(job.document)In [ ]:
rich.print(await services.jobs.list_jobs())In [ ]:
rich.print(await document_service.list_documents())In [ ]:
async with services.jobs._session_scope() as session:
jobs = await services.jobs.list_jobs(session=session, load_docs=True)
rich.print({job.id: job.document for job in jobs})In [ ]:
job_service = JobService()In [ ]:
doc_path.stem.rpartition("-")[0]In [ ]:
async with get_session() as session:
job = await services.jobs.read_job(job.id, session=session)
rich.print(job.status)
job.status = JobStatus.FAILED
job.retry_count = 0
await services.jobs.update_job(job, session=session)
rich.print(job)
await process_queued_job(job, services=services, session=session)
rich.print(job)In [ ]: