8.1 KiB
Step 4: services/upload.py + worker.py
Objective
Implement the MVP upload and background-processing pipeline so the system can:
- Save uploaded files into
UPLOAD_DIR - Create
Document+Job(status="queued") - Process queued jobs in a worker loop:
queued -> processing- call Step 3 transcription service
- persist
Transcript - finalize as
transcribedorfailed
This step advances MVP Feature 1 + Feature 2 and supports REQ-1, REQ-2, REQ-3, REQ-4, REQ-6.
Scope
In scope
src/transcription/services/upload.pysrc/transcription/worker.py- Upload persistence logic and initial job creation
- Worker polling and single-job lifecycle execution
- Deterministic test coverage for upload + worker (default suite)
Out of scope
- UI integration and pages (Step 5)
- Queue infrastructure beyond in-process loop
- Async DB/session architecture refactor
- Broad production hardening beyond MVP needs
Planned Deliverables
Source files
src/transcription/services/upload.pysrc/transcription/worker.pysrc/transcription/services/__init__.py(export updates as needed)
Test files
tests/services/test_upload.pytests/services/test_worker.py
Optional external lane (already present pattern)
- reuse
externalmarker for live-provider checks where appropriate - keep external out of default lane
Required MCP Prompt References (for test workflow)
Apply these resources directly during Step 4 test creation:
resource://catalog/prompts/pytest-scaffoldresource://prompts/pytest-scaffold/documentresource://catalog/prompts/pytest-fill-scaffoldresource://prompts/pytest-fill-scaffold/document
And (as referenced by those prompts) apply relevant pytest skill references for:
- naming/hierarchy
- marker defaults
- SQLAlchemy sync testing behavior where applicable
Design Decisions
-
Upload service owns initial file + record creation
- Writes file, creates
Document, creates queuedJob, returns IDs/path.
- Writes file, creates
-
Worker owns lifecycle transitions
- Worker is the single owner of
queued -> processing -> terminaljob state changes.
- Worker is the single owner of
-
Worker uses Step 3 service boundary
- Worker calls
transcribe_document_image(...); no provider-specific SDK logic in worker.
- Worker calls
-
Failure information is always persisted
- On failure: store
Transcript(text=None, error_detail=...)and setJob.status=failed.
- On failure: store
-
Loop remains simple and stoppable
- In-process polling loop with stop event and poll interval for MVP simplicity and testability.
Task-by-Task Execution Checklist
Phase A — Implement upload service (src/transcription/services/upload.py)
- Create
UploadErrorexception - Create
UploadJobResultdataclass with:document_idjob_idstored_pathoriginal_filename
- Add filename safety handling:
- normalize to basename
- avoid path traversal
- collision-safe stored name (e.g., UUID prefix/suffix)
- Validate upload payload:
- non-empty bytes required
- extension in supported set (
.jpg/.jpeg/.png/.tif/.tiff/.pdf)
- Ensure upload directory exists (
mkdir(parents=True, exist_ok=True)) - Write file bytes to
UPLOAD_DIR - Persist DB records in one transaction:
Document(filename, file_path)Job(document_id=..., status=queued)
- Return
UploadJobResult - Add logging for success/failure boundaries
Phase B — Implement worker core (src/transcription/worker.py)
- Add
process_next_queued_job(...) -> bool - Fetch oldest queued job
- Return
Falsewhen no queued jobs exist - Transition picked job to
processingand update timestamp - Resolve associated
Document.file_path - Call
transcribe_document_image(image_path=...) - On success:
- insert/update transcript text
- clear error detail
- mark job
transcribed - update timestamp
- On failure:
- insert/update transcript with
text=None,error_detail=... - mark job
failed - update timestamp
- insert/update transcript with
- Commit terminal state and return
True - Add logs around job pickup, transition, and terminal outcome
Phase C — Implement worker loop (src/transcription/worker.py)
- Add
run_worker_loop(...) - Accept configurable stop event/signal
- Accept configurable poll interval
- Repeatedly call
process_next_queued_job - Sleep only when queue is empty
- Exit cleanly when stop event is set
Phase D — Exports
- Update
src/transcription/services/__init__.pyto expose upload APIs - Keep existing transcription exports intact
Phase E — Tests via MCP scaffold -> fill flow
E1 Scaffold (structure only)
Use scaffold prompt workflow first for:
src/transcription/services/upload.pysrc/transcription/worker.py
Expected scaffold targets:
tests/services/test_upload.pytests/services/test_worker.py
Scaffold rules:
- Class hierarchy + method names + one-line docstrings only
- No assertions or implementation details in scaffold phase
- Keep method names concise and behavior-focused
Validation:
uv run pytest --collect-only -q
E2 Fill scaffold (implementation)
Use fill prompt workflow for:
tests/services/test_upload.pytests/services/test_worker.py- stack:
sqlalchemy-sync(ormixedif combining pure + DB behaviors) - marker lane preference:
unitandintegrationas appropriate - strategy: minimal deterministic implementation
Fill rules (invariants):
- Preserve scaffold class names, method names, and one-line docstrings
- Do not rename/re-nest scaffolded tests unless explicitly approved
- One behavior target per test
- Minimal mocking; mock only network/nondeterministic boundaries
Suggested test coverage:
tests/services/test_upload.py
- creates file + document + queued job (
integration) - rejects empty bytes (
unit) - rejects unsupported extension (
unit) - writes collision-safe unique filename (
integration) - persisted job status is
queued(integration)
tests/services/test_worker.py
- returns
Falsewhen queue empty (integration) - transitions
queued -> processing -> transcribedon success (integration) - stores transcript text on success (
integration) - transitions to
failedand storeserror_detailon failure (integration) - updates existing transcript instead of duplicate create (
integration) - worker loop exits when stop event set (
unit)
Marker Strategy
unit: pure logic tests (filename handling, loop stop behavior, validation logic)integration: DB + service orchestration tests (SQLite/session/contracts)external: opt-in live provider tests only (not part of default Step 4 lane)
No new marker needed; reuse existing marker registration.
Validation Sequence (strict order)
uv run pytest --collect-only -quv run pytest -m unit -q(if unit tests touched)uv run pytest tests/services/test_upload.py -quv run pytest tests/services/test_worker.py -quv run pytest -q
Reporting Requirements (after implementation)
Implementation report must include:
- Files created/updated
- Fixture and marker decisions
- MCP references used and why
- Validation command results
- Remaining risks/open questions (only blockers)
Guardrails
- Keep Step 4 independent from UI concerns.
- Do not call provider SDK directly from worker.
- Do not silently swallow exceptions.
- Always persist terminal job outcome.
- Keep default suite deterministic and fast.
- Preserve scaffold invariants during fill phase.
Definition of Done (Step 4)
Step 4 is complete when:
- Upload service writes file and creates
Document+ queuedJob - Worker processes queued jobs end-to-end using Step 3 transcription service
- Success path persists transcript text and sets
transcribed - Failure path persists error detail and sets
failed - Queue-empty path returns cleanly
- New tests pass and full suite is green (
uv run pytest -q) - Output report includes MCP reference usage + validation evidence