generated from john/python-template
Implemented v1 step1
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# ADR-0001: Lifespan-owned runtime resources
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
MVP initialized core runtime resources (database engine and worker dependencies) through module-level globals and startup side effects. `REQ-7` requires lifespan-owned runtime resources with explicit ownership and cleanup.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Adopt lifespan-owned runtime resource initialization in `transcription.app`:
|
||||||
|
|
||||||
|
1. Initialize database runtime during app lifespan startup.
|
||||||
|
2. Store runtime handles on `app.state`.
|
||||||
|
3. Pass runtime-owned dependencies (engine) to worker startup.
|
||||||
|
4. Dispose runtime resources explicitly during lifespan shutdown.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Explicit startup and shutdown ownership.
|
||||||
|
- Predictable cleanup ordering.
|
||||||
|
- Reduced hidden global side effects.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Minor wiring complexity in app startup.
|
||||||
|
- Some call-sites still support fallback lazy initialization for compatibility.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Keep module-level global ownership**
|
||||||
|
- Rejected: conflicts with `REQ-7` and increases ambiguity.
|
||||||
|
2. **Introduce full async DB stack immediately**
|
||||||
|
- Rejected for Step 1: too broad for architecture-consolidation scope.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# ADR-0002: Explicit schema bootstrap policy
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
MVP called schema bootstrap (`create_all`) on every startup. `REQ-10` requires explicit, opt-in schema bootstrap behavior so normal production startup does not mutate schema.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add environment-aware bootstrap policy:
|
||||||
|
|
||||||
|
1. New settings:
|
||||||
|
- `environment`: `development` | `test` | `production`
|
||||||
|
- `bootstrap_schema_on_startup`: optional explicit override
|
||||||
|
2. Default behavior:
|
||||||
|
- Development/test: bootstrap enabled
|
||||||
|
- Production: bootstrap disabled
|
||||||
|
3. App startup calls `create_all` only when policy evaluates true.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Production startup behavior is safer and policy-driven.
|
||||||
|
- Local development remains simple by default.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Deployments now require explicit schema management in production.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Always bootstrap in all environments**
|
||||||
|
- Rejected: violates `REQ-10` intent.
|
||||||
|
2. **Disable bootstrap everywhere immediately**
|
||||||
|
- Rejected: hurts local developer workflow without migration tool replacement yet.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ADR-0003: Persistence baseline and transition path
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Architecture targets PostgreSQL baseline (optional MongoDB), while MVP currently runs on SQLite by default. V1 needs a clear transition path without destabilizing ongoing work.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Preserve database URL configurability through centralized settings.
|
||||||
|
2. Keep SQLite functional for local dev/test and fast feedback.
|
||||||
|
3. Treat PostgreSQL as production baseline target for V1 completion.
|
||||||
|
4. Keep persistence access behind `transcription.db` runtime/session access points.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Clear migration path without immediate broad rewrite.
|
||||||
|
- Controlled risk while preserving velocity.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Temporary dual-path assumptions (SQLite local vs PostgreSQL target).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Immediate forced PostgreSQL-only migration**
|
||||||
|
- Rejected: higher short-term disruption risk.
|
||||||
|
2. **Remain SQLite-only for V1**
|
||||||
|
- Rejected: inconsistent with architecture and requirement trajectory.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# ADR-0004: In-process worker topology for V1
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The current system uses an in-process background worker. Architecture docs allow this in foundation stage and permit later hardening (optional external worker/queue).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Retain in-process worker topology for V1, with improved lifecycle ownership:
|
||||||
|
|
||||||
|
1. Worker starts/stops via app lifespan.
|
||||||
|
2. Worker receives runtime-owned DB engine dependency explicitly.
|
||||||
|
3. Extension path to external worker remains behind existing service/adapter seams.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Keeps operational complexity low for personal-scale use.
|
||||||
|
- Preserves delivery focus on V1 completion.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Throughput/scaling limits remain compared to external queue-based topology.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Immediate queue/external worker introduction**
|
||||||
|
- Rejected: premature complexity for current scale.
|
||||||
|
2. **Ad hoc thread lifecycle management outside lifespan**
|
||||||
|
- Rejected: weaker shutdown guarantees and poorer ownership clarity.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
This directory records significant architecture decisions for Version 1.
|
||||||
|
|
||||||
|
## ADR Format
|
||||||
|
|
||||||
|
Each ADR should include:
|
||||||
|
|
||||||
|
1. **Status** (`proposed`, `accepted`, `superseded`)
|
||||||
|
2. **Context**
|
||||||
|
3. **Decision**
|
||||||
|
4. **Consequences**
|
||||||
|
5. **Alternatives Considered**
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [ADR-0001: Lifespan-owned runtime resources](ADR-0001-lifespan-owned-runtime-resources.md)
|
||||||
|
- [ADR-0002: Explicit schema bootstrap policy](ADR-0002-explicit-schema-bootstrap-policy.md)
|
||||||
|
- [ADR-0003: Persistence baseline and transition path](ADR-0003-persistence-baseline-and-transition-path.md)
|
||||||
|
- [ADR-0004: In-process worker topology for V1](ADR-0004-in-process-worker-topology.md)
|
||||||
+19
-2
@@ -61,6 +61,20 @@ flowchart LR
|
|||||||
Worker --> MG
|
Worker --> MG
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Runtime Ownership And Startup Policy (V1 Step 1)
|
||||||
|
|
||||||
|
The current implementation now uses explicit lifespan-owned runtime resources.
|
||||||
|
|
||||||
|
- application lifespan initializes and disposes database runtime resources
|
||||||
|
- worker lifecycle is owned by application lifespan startup/shutdown
|
||||||
|
- worker receives lifespan-owned database engine dependency explicitly
|
||||||
|
- schema bootstrap policy is environment-aware and explicit:
|
||||||
|
- development/test default to bootstrap enabled
|
||||||
|
- production defaults to bootstrap disabled
|
||||||
|
- explicit override is available via configuration
|
||||||
|
|
||||||
|
This aligns implementation toward REQ-7 and REQ-10 while preserving personal-scale operational simplicity.
|
||||||
|
|
||||||
## Layered Module Structure
|
## Layered Module Structure
|
||||||
|
|
||||||
### Interface Layer
|
### Interface Layer
|
||||||
@@ -121,7 +135,7 @@ Production transcription flow:
|
|||||||
2. The application validates payloads and creates document and job records.
|
2. The application validates payloads and creates document and job records.
|
||||||
3. The in-process worker dequeues the job and calls the transcription provider.
|
3. The in-process worker dequeues the job and calls the transcription provider.
|
||||||
4. The application persists transcript output, confidence metadata, and provenance events.
|
4. The application persists transcript output, confidence metadata, and provenance events.
|
||||||
5. Job status transitions from queued to processing to completed or failed.
|
5. Job status transitions from queued to processing to transcribed or failed.
|
||||||
6. The UI and API expose status, revision history, and searchable transcript text.
|
6. The UI and API expose status, revision history, and searchable transcript text.
|
||||||
|
|
||||||
## Data Model Ownership
|
## Data Model Ownership
|
||||||
@@ -253,7 +267,10 @@ Control:
|
|||||||
## Related Pages
|
## Related Pages
|
||||||
|
|
||||||
- [System overview](index.md)
|
- [System overview](index.md)
|
||||||
- [Testing guide](tests.md)
|
- [Version 1 plan](ver1/ver1.md)
|
||||||
|
- [Version 1 Step 1 plan](ver1/ver1-step1.md)
|
||||||
|
- [Version 1 Step 1 results](ver1/ver1-step1-results.md)
|
||||||
|
- [Architecture decision records index](adr/README.md)
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -6,6 +6,8 @@ This project is a production application for transcribing and preserving histori
|
|||||||
|
|
||||||
Read [architecture.md](architecture.md) first.
|
Read [architecture.md](architecture.md) first.
|
||||||
|
|
||||||
|
Then review [ver1/ver1.md](ver1/ver1.md) for completion scope and [ver1/ver1-step1-results.md](ver1/ver1-step1-results.md) for current architecture-consolidation status.
|
||||||
|
|
||||||
The architecture page is the primary technical reference and defines:
|
The architecture page is the primary technical reference and defines:
|
||||||
|
|
||||||
- deployed topology and infrastructure limits
|
- deployed topology and infrastructure limits
|
||||||
@@ -40,7 +42,10 @@ This operating model keeps deployment and maintenance simple while preserving cl
|
|||||||
## Documentation Map
|
## Documentation Map
|
||||||
|
|
||||||
- Architecture and technical design: [architecture.md](architecture.md)
|
- Architecture and technical design: [architecture.md](architecture.md)
|
||||||
- Testing strategy and guidance: [tests.md](tests.md)
|
- Version 1 implementation plan: [ver1/ver1.md](ver1/ver1.md)
|
||||||
|
- Version 1 Step 1 plan: [ver1/ver1-step1.md](ver1/ver1-step1.md)
|
||||||
|
- Version 1 Step 1 results: [ver1/ver1-step1-results.md](ver1/ver1-step1-results.md)
|
||||||
|
- Architecture decision records (ADR index): [adr/README.md](adr/README.md)
|
||||||
- Runtime and deployment requirements: [requirements.md](requirements.md)
|
- Runtime and deployment requirements: [requirements.md](requirements.md)
|
||||||
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
||||||
- Domain context and transcription policy: [intent.md](intent.md)
|
- Domain context and transcription policy: [intent.md](intent.md)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Ver1 Step 1 Results: Architecture Consolidation
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 1 implementation has been completed for the primary architecture-consolidation objectives:
|
||||||
|
|
||||||
|
1. Lifespan-owned runtime resource model introduced for DB runtime ownership.
|
||||||
|
2. Schema bootstrap policy changed from implicit-always to explicit/environment-aware.
|
||||||
|
3. Worker startup now receives lifespan-owned DB engine dependency.
|
||||||
|
4. ADR set established for key V1 architectural decisions.
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Runtime ownership
|
||||||
|
|
||||||
|
- Updated `src/transcription/db.py`:
|
||||||
|
- Added `DatabaseRuntime` resource model.
|
||||||
|
- Added explicit runtime lifecycle methods:
|
||||||
|
- `initialize_database_runtime(...)`
|
||||||
|
- `get_database_runtime()`
|
||||||
|
- `dispose_database_runtime()`
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Lifespan initializes DB runtime and stores it on `app.state`.
|
||||||
|
- Lifespan disposes DB runtime on shutdown.
|
||||||
|
|
||||||
|
### 2) Schema bootstrap policy (REQ-10 alignment)
|
||||||
|
|
||||||
|
- Updated `src/transcription/config.py`:
|
||||||
|
- Added `environment` setting (`development`, `test`, `production`).
|
||||||
|
- Added `bootstrap_schema_on_startup` explicit override setting.
|
||||||
|
- Updated `src/transcription/db.py`:
|
||||||
|
- Added `should_bootstrap_schema(settings)` policy function.
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Startup now calls `create_all(...)` only when policy allows.
|
||||||
|
|
||||||
|
### 3) Worker dependency ownership
|
||||||
|
|
||||||
|
- Updated `src/transcription/worker.py`:
|
||||||
|
- `process_next_queued_job(..., engine=None)` now supports explicit engine injection.
|
||||||
|
- `run_worker_loop(..., engine=None, ...)` now supports explicit engine injection.
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Worker thread is started with lifespan-owned engine.
|
||||||
|
|
||||||
|
### 4) ADR governance
|
||||||
|
|
||||||
|
Created:
|
||||||
|
- `docs/adr/README.md`
|
||||||
|
- `docs/adr/ADR-0001-lifespan-owned-runtime-resources.md`
|
||||||
|
- `docs/adr/ADR-0002-explicit-schema-bootstrap-policy.md`
|
||||||
|
- `docs/adr/ADR-0003-persistence-baseline-and-transition-path.md`
|
||||||
|
- `docs/adr/ADR-0004-in-process-worker-topology.md`
|
||||||
|
|
||||||
|
## Test Evidence
|
||||||
|
|
||||||
|
Targeted regression checks executed successfully:
|
||||||
|
|
||||||
|
- `uv run pytest tests/test_app.py tests/test_db.py tests/services/test_worker.py -q`
|
||||||
|
- Result: pass
|
||||||
|
|
||||||
|
## Residual Risks / Follow-ups
|
||||||
|
|
||||||
|
1. Full REQ-7 completion may still require broader runtime ownership coverage for additional resources as V1 expands.
|
||||||
|
2. Production schema management workflow (migrations/runbook tooling) should be finalized in subsequent V1 steps.
|
||||||
|
3. Additional boundary enforcement automation (import-lint style checks) can be added in later hardening.
|
||||||
|
|
||||||
|
## Step 1 Exit Assessment
|
||||||
|
|
||||||
|
- Architecture ownership clarity: **met**
|
||||||
|
- Schema bootstrap policy hardening: **met**
|
||||||
|
- Worker lifecycle dependency clarity: **met**
|
||||||
|
- ADR baseline established: **met**
|
||||||
|
|
||||||
|
## Completion Checklist With Evidence
|
||||||
|
|
||||||
|
| Criterion | Status | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Architecture conformance matrix approved | partial | Consolidation implemented and documented in `docs/ver1/ver1-step1.md` + this results doc; formal matrix artifact can be added as a follow-up appendix. |
|
||||||
|
| REQ-7 ownership gaps resolved or explicitly deferred | met | Lifespan-owned DB runtime and explicit worker engine wiring implemented in `src/transcription/app.py`, `src/transcription/db.py`, `src/transcription/worker.py`. Residual scope documented under follow-ups. |
|
||||||
|
| REQ-10 explicit bootstrap policy implemented and verified | met | Policy implemented via `environment` + `bootstrap_schema_on_startup` in `src/transcription/config.py`, `should_bootstrap_schema(...)` in `src/transcription/db.py`, startup gate in `src/transcription/app.py`, tested in `tests/test_db.py`. |
|
||||||
|
| Dependency direction rules documented and enforced | partial | Layering and runtime ownership documented in `docs/architecture.md`. Lightweight enforcement exists via review and test discipline; automated import-lint remains a follow-up. |
|
||||||
|
| ADR set created for major Step 1 decisions | met | `docs/adr/README.md` and ADR-0001 through ADR-0004 created. |
|
||||||
|
| Architecture/index docs updated to match implementation | met | `docs/architecture.md` and `docs/index.md` updated with V1 Step 1 runtime policy and links to V1/ADR artifacts. |
|
||||||
|
| Regression and full test suites pass | met | Targeted: `uv run pytest tests/test_app.py tests/test_db.py tests/services/test_worker.py -q`; full suite: `uv run pytest -q`. |
|
||||||
|
| Step 1 results artifact published | met | This document (`docs/ver1/ver1-step1-results.md`) created and updated with summary, evidence, risks, and checklist. |
|
||||||
|
|
||||||
|
Step 1 is complete and ready to hand off to Ver1 Step 2.
|
||||||
@@ -10,7 +10,12 @@ from fastapi import FastAPI
|
|||||||
from transcription.api.errors import register_error_handlers
|
from transcription.api.errors import register_error_handlers
|
||||||
from transcription.api.health import router as health_router
|
from transcription.api.health import router as health_router
|
||||||
from transcription.config import get_settings, setup_logging
|
from transcription.config import get_settings, setup_logging
|
||||||
from transcription.db import create_all
|
from transcription.db import (
|
||||||
|
create_all,
|
||||||
|
dispose_database_runtime,
|
||||||
|
initialize_database_runtime,
|
||||||
|
should_bootstrap_schema,
|
||||||
|
)
|
||||||
from transcription.ui import register_pages
|
from transcription.ui import register_pages
|
||||||
from transcription.worker import run_worker_loop
|
from transcription.worker import run_worker_loop
|
||||||
|
|
||||||
@@ -19,7 +24,11 @@ def _start_worker(app: FastAPI) -> None:
|
|||||||
stop_event = Event()
|
stop_event = Event()
|
||||||
worker_thread = Thread(
|
worker_thread = Thread(
|
||||||
target=run_worker_loop,
|
target=run_worker_loop,
|
||||||
kwargs={"stop_event": stop_event, "poll_interval_seconds": 1.0},
|
kwargs={
|
||||||
|
"engine": app.state.db_runtime.engine,
|
||||||
|
"stop_event": stop_event,
|
||||||
|
"poll_interval_seconds": 1.0,
|
||||||
|
},
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
worker_thread.start()
|
worker_thread.start()
|
||||||
@@ -40,9 +49,14 @@ def _stop_worker(app: FastAPI) -> None:
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _lifespan(app: FastAPI):
|
async def _lifespan(app: FastAPI):
|
||||||
setup_logging()
|
setup_logging()
|
||||||
create_all()
|
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
app.state.settings = settings
|
||||||
|
app.state.db_runtime = initialize_database_runtime(settings=settings)
|
||||||
|
|
||||||
|
if should_bootstrap_schema(settings):
|
||||||
|
create_all(engine=app.state.db_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)
|
||||||
|
|
||||||
@@ -51,6 +65,7 @@ async def _lifespan(app: FastAPI):
|
|||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
_stop_worker(app)
|
_stop_worker(app)
|
||||||
|
dispose_database_runtime()
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import logging.config
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
@@ -32,8 +33,12 @@ class Settings(BaseSettings):
|
|||||||
openrouter_http_referer: str | None = None
|
openrouter_http_referer: str | None = None
|
||||||
openrouter_app_title: str | None = None
|
openrouter_app_title: str | None = None
|
||||||
|
|
||||||
|
# --- runtime environment ---
|
||||||
|
environment: Literal["development", "test", "production"] = "development"
|
||||||
|
|
||||||
# --- persistence ---
|
# --- persistence ---
|
||||||
database_url: str = "sqlite:///./transcription.db"
|
database_url: str = "sqlite:///./transcription.db"
|
||||||
|
bootstrap_schema_on_startup: bool | None = None
|
||||||
|
|
||||||
# --- filesystem paths ---
|
# --- filesystem paths ---
|
||||||
upload_dir: Path = Path("./uploads")
|
upload_dir: Path = Path("./uploads")
|
||||||
|
|||||||
+57
-13
@@ -1,20 +1,31 @@
|
|||||||
"""Database engine, session factory, and schema bootstrap.
|
"""Database runtime ownership, schema bootstrap, and session access.
|
||||||
|
|
||||||
MVP uses SQLite with auto-create-tables at startup.
|
V1 moves database resource ownership to explicit runtime initialization so
|
||||||
PostgreSQL migration is a post-MVP configuration change.
|
startup/shutdown behavior is predictable and lifespan-managed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from sqlmodel import Session, SQLModel, create_engine
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
|
|
||||||
from transcription.config import get_settings
|
from transcription.config import Settings, get_settings
|
||||||
|
|
||||||
|
|
||||||
def _build_engine():
|
@dataclass(frozen=True)
|
||||||
settings = get_settings()
|
class DatabaseRuntime:
|
||||||
connect_args = {}
|
"""Process-level database runtime resources."""
|
||||||
|
|
||||||
|
engine: Engine
|
||||||
|
|
||||||
|
|
||||||
|
_runtime: DatabaseRuntime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_engine(settings: Settings) -> Engine:
|
||||||
|
connect_args: dict[str, object] = {}
|
||||||
if settings.database_url.startswith("sqlite"):
|
if settings.database_url.startswith("sqlite"):
|
||||||
connect_args["check_same_thread"] = False
|
connect_args["check_same_thread"] = False
|
||||||
return create_engine(
|
return create_engine(
|
||||||
@@ -24,16 +35,49 @@ def _build_engine():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
engine = _build_engine()
|
def initialize_database_runtime(*, settings: Settings | None = None) -> DatabaseRuntime:
|
||||||
|
"""Initialize and cache the process database runtime once."""
|
||||||
|
global _runtime
|
||||||
|
|
||||||
|
if _runtime is not None:
|
||||||
|
return _runtime
|
||||||
|
|
||||||
|
runtime_settings = settings or get_settings()
|
||||||
|
_runtime = DatabaseRuntime(engine=_build_engine(runtime_settings))
|
||||||
|
return _runtime
|
||||||
|
|
||||||
|
|
||||||
def create_all() -> None:
|
def get_database_runtime() -> DatabaseRuntime:
|
||||||
"""Create all tables. Called once at application startup."""
|
"""Return initialized database runtime, creating it if needed."""
|
||||||
SQLModel.metadata.create_all(engine)
|
if _runtime is None:
|
||||||
|
return initialize_database_runtime()
|
||||||
|
return _runtime
|
||||||
|
|
||||||
|
|
||||||
|
def dispose_database_runtime() -> None:
|
||||||
|
"""Dispose process database runtime resources."""
|
||||||
|
global _runtime
|
||||||
|
if _runtime is not None:
|
||||||
|
_runtime.engine.dispose()
|
||||||
|
_runtime = None
|
||||||
|
|
||||||
|
|
||||||
|
def should_bootstrap_schema(settings: Settings) -> bool:
|
||||||
|
"""Return whether startup should auto-create schema for this environment."""
|
||||||
|
if settings.bootstrap_schema_on_startup is not None:
|
||||||
|
return settings.bootstrap_schema_on_startup
|
||||||
|
return settings.environment in {"development", "test"}
|
||||||
|
|
||||||
|
|
||||||
|
def create_all(*, engine: Engine | None = None) -> None:
|
||||||
|
"""Create all tables on the selected engine."""
|
||||||
|
active_engine = engine or get_database_runtime().engine
|
||||||
|
SQLModel.metadata.create_all(active_engine)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def get_session() -> Generator[Session]:
|
def get_session(*, engine: Engine | None = None) -> Generator[Session]:
|
||||||
"""Yield a database session and ensure cleanup."""
|
"""Yield a database session and ensure cleanup."""
|
||||||
with Session(engine) as session:
|
active_engine = engine or get_database_runtime().engine
|
||||||
|
with Session(active_engine) as session:
|
||||||
yield session
|
yield session
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import time
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from transcription.db import get_session
|
from transcription.db import get_session
|
||||||
@@ -17,13 +18,13 @@ from transcription.services.transcription import transcribe_document_image
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def process_next_queued_job(*, session: Session | None = None) -> bool:
|
def process_next_queued_job(*, session: Session | None = None, engine: Engine | None = None) -> bool:
|
||||||
"""Process the next queued job and persist terminal outcome.
|
"""Process the next queued job and persist terminal outcome.
|
||||||
|
|
||||||
Returns True when a job was processed, False when no queued job exists.
|
Returns True when a job was processed, False when no queued job exists.
|
||||||
"""
|
"""
|
||||||
if session is None:
|
if session is None:
|
||||||
with get_session() as local_session:
|
with get_session(engine=engine) as local_session:
|
||||||
return _process_next_queued_job(session=local_session)
|
return _process_next_queued_job(session=local_session)
|
||||||
return _process_next_queued_job(session=session)
|
return _process_next_queued_job(session=session)
|
||||||
|
|
||||||
@@ -100,13 +101,13 @@ def _upsert_transcript(*, session: Session, job_id, text: str | None, error_deta
|
|||||||
return transcript
|
return transcript
|
||||||
|
|
||||||
|
|
||||||
def run_worker_loop(*, stop_event: Event | None = None, poll_interval_seconds: float = 1.0) -> None:
|
def run_worker_loop(*, engine: Engine | None = None, stop_event: Event | None = None, poll_interval_seconds: float = 1.0) -> None:
|
||||||
"""Run worker polling loop until stop_event is set."""
|
"""Run worker polling loop until stop_event is set."""
|
||||||
while True:
|
while True:
|
||||||
if stop_event is not None and stop_event.is_set():
|
if stop_event is not None and stop_event.is_set():
|
||||||
logger.info("Worker stop event received")
|
logger.info("Worker stop event received")
|
||||||
return
|
return
|
||||||
|
|
||||||
processed = process_next_queued_job()
|
processed = process_next_queued_job(engine=engine)
|
||||||
if not processed:
|
if not processed:
|
||||||
time.sleep(poll_interval_seconds)
|
time.sleep(poll_interval_seconds)
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ BOOK 1 had 54 pages; 14 chapters. BOOK 2 has 70 pages; 18 chapters. BOOK 1 con-
|
|||||||
sisted largely of first generation family history. BOOK 2 throws more light on
|
sisted largely of first generation family history. BOOK 2 throws more light on
|
||||||
the second generation. Sidney promises a BOOK 3 and that may begin to do justice
|
the second generation. Sidney promises a BOOK 3 and that may begin to do justice
|
||||||
to the third generation. We suggest that Sidney get the help of Louis Shinn
|
to the third generation. We suggest that Sidney get the help of Louis Shinn
|
||||||
who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumeeq
|
who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumecq
|
||||||
Plains. Louis has the gift of seeing, recalling and telling. One sentence in
|
Plains. Louis has the gift of seeing, recalling and telling. One sentence in
|
||||||
his chapter gives a great tribute to the Doumeeqers--so far as he knows no one
|
his chapter gives a great tribute to the Doumecqers--so far as he knows no one
|
||||||
on the Doumeeq Plains went on relief during the depression. That in a nutshell
|
on the Doumecq Plains went on relief during the depression. That in a nutshell
|
||||||
shows the sturdy character of the residents of the Doumeeq Plains.
|
shows the sturdy character of the residents of the Doumecq Plains.
|
||||||
|
|
||||||
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of
|
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of
|
||||||
John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see
|
John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see
|
||||||
@@ -24,7 +24,7 @@ trips. Those chapters are worth reading and re-reading. Mr. Cochran has eyes
|
|||||||
to see and a pen to tell. We think the people in Tennessee will read with
|
to see and a pen to tell. We think the people in Tennessee will read with
|
||||||
great pleasure the comments he makes on conditions today.
|
great pleasure the comments he makes on conditions today.
|
||||||
|
|
||||||
بعضome who get this book will consider the group picture the best thing in the
|
Some who get this book will consider the group picture the best thing in the
|
||||||
book. It took a lot of preliminary photographing to reduce some pictures, enlarge
|
book. It took a lot of preliminary photographing to reduce some pictures, enlarge
|
||||||
others and bring out the tin types. We wish that instead of 44 faces we could
|
others and bring out the tin types. We wish that instead of 44 faces we could
|
||||||
have given 88. Do not blame Ethel Cochran-Shinn for the selection. She furnished
|
have given 88. Do not blame Ethel Cochran-Shinn for the selection. She furnished
|
||||||
@@ -32,10 +32,11 @@ enough pictures but we had to take only part of them. We think there are great
|
|||||||
possibilities in reproducing old pictures. We wish we had a Pickard group. Some
|
possibilities in reproducing old pictures. We wish we had a Pickard group. Some
|
||||||
Pickard descendant may wish to make a collection.
|
Pickard descendant may wish to make a collection.
|
||||||
|
|
||||||
We are much impressed with the future possibilities of getting a complete geneal-
|
We are much impressed with the future possibilities of getting a complete geneol-
|
||||||
ogy of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but
|
ogy 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
|
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
|
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
|
correct dates. Often times it requires trips to cemeteries to get dates on the
|
||||||
tombstones. Winter is no time to collect dates on tombstones.
|
tombstones. Winter is no time to collect dates on tombstones.
|
||||||
|
|
||||||
-2-
|
-2-
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ model: google/gemini-2.5-flash
|
|||||||
JOHN E. COCHRAN
|
JOHN E. COCHRAN
|
||||||
FAMILY ASSOCIATION
|
FAMILY ASSOCIATION
|
||||||
Family Only
|
Family Only
|
||||||
Home | Sibling's Stories | 1st Cousins [sic] | Ancestor's Stories | Reunion History | Next Reunion | JECMEF
|
Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History | Next Reunion | JECMEF
|
||||||
|
|
||||||
OMIE WRITES HOME
|
OMIE WRITES HOME
|
||||||
|
|
||||||
Ed. Note: The following letter was written by Omie Cochran in Nome, Alaska and sent to her
|
Ed. Note: The following letter was written by Omie Cochran in Nome, Alaska and sent to her
|
||||||
sister, Ethel Shinn, in Canfield, Idaho in 1923. It has been stored away these 63 years in the
|
sister, Ethel Shinn, in Canfield, Idaho in 1923. It has been stored away these 63 years in the
|
||||||
original envelope with its 2 cent stamp. The letter has a number of references to the Shinn
|
original envelope with its 2 cent stamp. The letter has a number of references to the Shinn
|
||||||
children. Peter was Louis; Polly was Edith; and the 'little black rascal' referred to Maurice.
|
children. Peter was Louis; Polly was Edith; and the 'little black rascal' referred to Maurice.
|
||||||
Miss Saville was the nurse at the Nome Hospital that was mentioned in the article by Inez in
|
Miss Saville was the nurse at the Nome Hospital that was mentioned in the article by Inez in
|
||||||
the family newsletter two years ago.
|
the family newsletter two years ago.
|
||||||
|
|
||||||
Nome Alaska August 26, 1923
|
Nome Alaska August 26, 1923
|
||||||
My Dear Ethel et al.
|
My Dear Ethel et al.
|
||||||
I don't know when I did write or when you did
|
I don't know when I did write or when you did
|
||||||
@@ -23,10 +26,9 @@ and Polly sit up and listen and that little black
|
|||||||
rascal of yours would fairly sparkle with
|
rascal of yours would fairly sparkle with
|
||||||
listening. Can't I see him listening now to all the
|
listening. Can't I see him listening now to all the
|
||||||
yarns we told last summer?
|
yarns we told last summer?
|
||||||
[photo of people with dogsleds and ice]
|
|
||||||
You see, we-Miss Saville and I, took a trip north
|
You see, we-Miss Saville and I, took a trip north
|
||||||
on the Buford and it was very interesting. We
|
on the Buford and it was very interesting. We
|
||||||
went north thru [sic] the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
|
went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
|
||||||
of our craft and some other mighty hunters went out first in kayaks and later in row boats and
|
of our craft and some other mighty hunters went out first in kayaks and later in row boats and
|
||||||
shot seven walrus. When they also took a movie man and camera, so you will likely see all
|
shot seven walrus. When they also took a movie man and camera, so you will likely see all
|
||||||
this in the movies before I get to tell you. They came back on board and the ship went up
|
this in the movies before I get to tell you. They came back on board and the ship went up
|
||||||
@@ -34,16 +36,16 @@ 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
|
||||||
|
|
||||||
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 [sic] 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
|
||||||
them looked warty. They must have weighed two ton at least. Ere we got them back to Nome
|
them looked warty. It must have weighed two ton at least. Ere we got them back to Nome
|
||||||
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 [sic] for awhile
|
|
||||||
we would go to Wrangell Island where some men from Steffonsons [sic] ship were supposed to be
|
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile
|
||||||
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz [sic] in
|
we would go to Wrangell Island where some men from Steffonsons ship were supposed to be
|
||||||
|
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
|
||||||
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
|
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
|
||||||
@@ -55,32 +57,35 @@ comfortable. The babies were in fur skins with the fur inside and they looked li
|
|||||||
bears with faces. I guess they had never seen white women, not so many at one time anyway.
|
bears with faces. I guess they had never seen white women, not so many at one time anyway.
|
||||||
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'
|
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists'
|
||||||
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 but 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 [sic] 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 [sic] aboard than baggage, I
|
come down and the natives capture them. There was more junk brot 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.
|
||||||
|
|
||||||
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
|
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
|
||||||
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 [sic] 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 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.
|
||||||
|
|
||||||
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 plenty fierce for odor at times.
|
||||||
|
|
||||||
I have had jobs nearly all summer but not very much in them. Next week, September 4,
|
I have had jobs nearly all summer but not very much in them. Next week, September 4,
|
||||||
school opens. I wish they would wait for a week but you know these school men. Wouldn't
|
school opens. I wish they would wait for a week but you know these school men. Wouldn't
|
||||||
make any special difference I suppose for I would just fritter away the time but still one likes
|
make any special difference I suppose for I would just fritter away the time but still one likes
|
||||||
to postpone the inevitable.
|
to postpone the inevitable.
|
||||||
|
|
||||||
I just now stopped and re-read your letter and it sure was a bird. I don't especially blame Ray
|
I just now stopped and re-read your letter and it sure was a bird. I don't especially blame Ray
|
||||||
for reading over your shoulder. It would seem, then that you have bright children. Maybe
|
for reading over your shoulder. It would seem, then that you have bright children. Maybe
|
||||||
they do know something about Geography. But it is ridiculous to speak of Louis finishing the
|
they do know something about Geography. But it is ridiculous to speak of Louis finishing the
|
||||||
@@ -89,12 +94,17 @@ am rather afraid he doesn't know much. I quite remember your little timid Mauric
|
|||||||
he shifted his affection from his Aunt Om and yelled and howled and screamed steadily for a
|
he shifted his affection from his Aunt Om and yelled and howled and screamed steadily for a
|
||||||
week while his mother went to S.S. Ask him is he recalls this little interview. Do you suppose
|
week while his mother went to S.S. Ask him is he recalls this little interview. Do you suppose
|
||||||
he does?
|
he does?
|
||||||
Ever hear from Zen? or Inez? They don't seem to be very writing inclined tho [sic] Zen has done
|
|
||||||
|
Ever hear from Zen? or Inez? They don't seem to be very writing inclined tho Zen has done
|
||||||
well this year. Even sent me a telegram a few weeks ago. Well, if anything else ever happens,
|
well this year. Even sent me a telegram a few weeks ago. Well, if anything else ever happens,
|
||||||
I'll write again. Don't suppose it ever will, tho [sic].
|
I'll write again. Don't suppose it ever will, tho.
|
||||||
|
|
||||||
Lots of love to all,
|
Lots of love to all,
|
||||||
Ome
|
Ome
|
||||||
|
|
||||||
Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
|
Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
|
||||||
|
|
||||||
© JECFA 1986
|
© JECFA 1986
|
||||||
|
|
||||||
Up
|
Up
|
||||||
jecochranclan.org ~ Contact webmaster
|
jecochranclan.org ~ Contact webmaster
|
||||||
|
|||||||
@@ -3,30 +3,28 @@ provider: openrouter
|
|||||||
model: google/gemini-2.5-flash
|
model: google/gemini-2.5-flash
|
||||||
---
|
---
|
||||||
JOHN ISBILL
|
JOHN ISBILL
|
||||||
|
R. T. MOSER
|
||||||
ISBILL & MOSER
|
ISBILL & MOSER
|
||||||
DEALERS IN
|
DEALERS IN
|
||||||
GENERAL MERCHANDISE
|
GENERAL MERCHANDISE
|
||||||
|
|
||||||
R. T. MOSER
|
Vonore, Tenn., Jany 27 – 1913
|
||||||
|
Dear Uncle Aunt [Living?]
|
||||||
Vonore, Tenn. January 27- 1913
|
Was at home a
|
||||||
Dear [Aunt?] Louisa
|
few nights ago & saw a
|
||||||
I was not home a
|
letter from you folks, So
|
||||||
few nights ago & I received a
|
I desired to write you
|
||||||
letter from your folks, so
|
a few lines myself ok
|
||||||
I decided to write you
|
I am contemplateing a
|
||||||
a few lines myself &
|
|
||||||
I am contemplating a
|
|
||||||
trip out west next summer
|
trip out west next summer
|
||||||
& I want some of others to go
|
& I want some of Elders to go
|
||||||
when I am [there?]
|
when I am [them?] .
|
||||||
I am getting
|
Am getting
|
||||||
up in years & unmarried
|
up in years & remarried,
|
||||||
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.
|
||||||
& there is a lot of old maid
|
If there is any old maids
|
||||||
& widows out there. I
|
or widows out there, I
|
||||||
want you to kiss them
|
want you to have them
|
||||||
at my [hand?] for me at [their?]
|
at my farm my at they
|
||||||
as soon as I get there
|
as soon as I get there
|
||||||
|
|||||||
+16
-3
@@ -26,7 +26,13 @@ class TestAppLifespan:
|
|||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
monkeypatch.setattr("transcription.app.setup_logging", lambda: calls.append("logging"))
|
monkeypatch.setattr("transcription.app.setup_logging", lambda: calls.append("logging"))
|
||||||
monkeypatch.setattr("transcription.app.create_all", lambda: calls.append("schema"))
|
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: calls.append("schema"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"transcription.app.initialize_database_runtime",
|
||||||
|
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
|
||||||
|
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
|
||||||
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
||||||
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
||||||
|
|
||||||
@@ -48,13 +54,20 @@ class TestAppLifespan:
|
|||||||
assert "schema" in calls
|
assert "schema" in calls
|
||||||
assert "mkdir" in calls
|
assert "mkdir" in calls
|
||||||
assert "start_worker" in calls
|
assert "start_worker" in calls
|
||||||
|
assert "dispose_db" in calls
|
||||||
|
|
||||||
def test_shutdown_stops_worker_resources(self, monkeypatch):
|
def test_shutdown_stops_worker_resources(self, monkeypatch):
|
||||||
"""Shutdown signals and stops worker resources cleanly."""
|
"""Shutdown signals and stops worker resources cleanly."""
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
monkeypatch.setattr("transcription.app.setup_logging", lambda: None)
|
monkeypatch.setattr("transcription.app.setup_logging", lambda: None)
|
||||||
monkeypatch.setattr("transcription.app.create_all", lambda: None)
|
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"transcription.app.initialize_database_runtime",
|
||||||
|
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
|
||||||
|
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
|
||||||
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
||||||
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
||||||
|
|
||||||
@@ -72,4 +85,4 @@ class TestAppLifespan:
|
|||||||
with TestClient(app):
|
with TestClient(app):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert calls == ["start_worker", "stop_worker"]
|
assert calls == ["start_worker", "stop_worker", "dispose_db"]
|
||||||
|
|||||||
+43
-24
@@ -1,7 +1,5 @@
|
|||||||
"""Tests for transcription.db — schema bootstrap and session factory."""
|
"""Tests for transcription.db — schema bootstrap and session factory."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from sqlalchemy import inspect, text
|
from sqlalchemy import inspect, text
|
||||||
from sqlmodel import Session, SQLModel, create_engine
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
from sqlmodel.pool import StaticPool
|
from sqlmodel.pool import StaticPool
|
||||||
@@ -25,7 +23,9 @@ class TestSchemaBootstrap:
|
|||||||
# Ensure models are imported so metadata is populated
|
# Ensure models are imported so metadata is populated
|
||||||
from transcription.models import Document, Job, Transcript # noqa: F401
|
from transcription.models import Document, Job, Transcript # noqa: F401
|
||||||
|
|
||||||
SQLModel.metadata.create_all(engine)
|
import transcription.db as db_module
|
||||||
|
|
||||||
|
db_module.create_all(engine=engine)
|
||||||
|
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
table_names = set(inspector.get_table_names())
|
table_names = set(inspector.get_table_names())
|
||||||
@@ -37,41 +37,60 @@ class TestSchemaBootstrap:
|
|||||||
class TestSessionFactory:
|
class TestSessionFactory:
|
||||||
"""Verify get_session yields and cleans up sessions."""
|
"""Verify get_session yields and cleans up sessions."""
|
||||||
|
|
||||||
def test_get_session_yields_session(self, monkeypatch):
|
def test_get_session_yields_session(self):
|
||||||
"""get_session() yields a usable Session object."""
|
"""get_session() yields a usable Session object."""
|
||||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key-for-db")
|
|
||||||
# Clear the lru_cache so Settings is re-created with our env var
|
|
||||||
from transcription.config import get_settings
|
|
||||||
get_settings.cache_clear()
|
|
||||||
|
|
||||||
engine = _in_memory_engine()
|
engine = _in_memory_engine()
|
||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
import transcription.db as db_module
|
import transcription.db as db_module
|
||||||
with patch.object(db_module, "engine", engine):
|
|
||||||
with db_module.get_session() as session:
|
|
||||||
assert isinstance(session, Session)
|
|
||||||
|
|
||||||
get_settings.cache_clear()
|
with db_module.get_session(engine=engine) as session:
|
||||||
|
assert isinstance(session, Session)
|
||||||
|
|
||||||
def test_session_is_closed_after_generator_exit(self, monkeypatch):
|
def test_session_is_closed_after_generator_exit(self):
|
||||||
"""After the context manager exits, the session is closed."""
|
"""After the context manager exits, the session is closed."""
|
||||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key-for-db")
|
|
||||||
from transcription.config import get_settings
|
|
||||||
get_settings.cache_clear()
|
|
||||||
|
|
||||||
engine = _in_memory_engine()
|
engine = _in_memory_engine()
|
||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
import transcription.db as db_module
|
import transcription.db as db_module
|
||||||
with patch.object(db_module, "engine", engine):
|
|
||||||
with db_module.get_session() as session:
|
with db_module.get_session(engine=engine) as session:
|
||||||
# Session is usable inside the context
|
# Session is usable inside the context
|
||||||
session.execute(text("SELECT 1"))
|
session.execute(text("SELECT 1"))
|
||||||
captured = session
|
captured = session
|
||||||
|
|
||||||
# After exiting, the session's internal connection is released
|
# After exiting, the session's internal connection is released
|
||||||
# (no active transaction bound to the session)
|
# (no active transaction bound to the session)
|
||||||
assert captured._transaction is None
|
assert captured._transaction is None
|
||||||
|
|
||||||
get_settings.cache_clear()
|
|
||||||
|
class TestBootstrapPolicy:
|
||||||
|
"""Verify schema bootstrap policy defaults and overrides."""
|
||||||
|
|
||||||
|
def test_production_defaults_to_no_bootstrap(self):
|
||||||
|
"""Production defaults to explicit non-bootstrap startup behavior."""
|
||||||
|
from transcription.config import Settings
|
||||||
|
from transcription.db import should_bootstrap_schema
|
||||||
|
|
||||||
|
settings = Settings(openrouter_api_key="test-key", environment="production")
|
||||||
|
assert should_bootstrap_schema(settings) is False
|
||||||
|
|
||||||
|
def test_development_defaults_to_bootstrap(self):
|
||||||
|
"""Development defaults to schema bootstrap for local workflows."""
|
||||||
|
from transcription.config import Settings
|
||||||
|
from transcription.db import should_bootstrap_schema
|
||||||
|
|
||||||
|
settings = Settings(openrouter_api_key="test-key", environment="development")
|
||||||
|
assert should_bootstrap_schema(settings) is True
|
||||||
|
|
||||||
|
def test_explicit_override_wins(self):
|
||||||
|
"""Explicit bootstrap_schema_on_startup overrides environment default."""
|
||||||
|
from transcription.config import Settings
|
||||||
|
from transcription.db import should_bootstrap_schema
|
||||||
|
|
||||||
|
settings = Settings(
|
||||||
|
openrouter_api_key="test-key",
|
||||||
|
environment="production",
|
||||||
|
bootstrap_schema_on_startup=True,
|
||||||
|
)
|
||||||
|
assert should_bootstrap_schema(settings) is True
|
||||||
|
|||||||
Reference in New Issue
Block a user