Jonas 4c9deda66a Implement labeling backend: FastAPI + SQLAlchemy data model and CRUD API
Add the core labeling data model (label sets, ad-hoc Main->Sub class
hierarchy, frames, bounding-box labels, per-frame/per-set label
status) behind a FastAPI app, with SQLite as the default swappable
DATABASE_URL. Multi-user support is attribution-only for now
(get-or-create by username, no login flow yet). Dataset
versioning/promotion is intentionally deferred - it needs its own
design pass around snapshot semantics.

Each test gets a fully isolated app+DB via create_app(database_url=...)
rather than relying on process-global state. 13/13 tests pass; also
verified live end-to-end against a running uvicorn instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 11:24:57 +02:00

41 lines
1.4 KiB
Python

"""Database engine/session setup.
Each `Database` instance owns its own engine and session factory, so a
production run and a test can each get a fully isolated database instead of
sharing process-global state (see `main.create_app`).
"""
from __future__ import annotations
import os
from typing import Iterator, Optional
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from .models import Base
DEFAULT_DATABASE_URL = "sqlite:///./labeling.db"
class Database:
def __init__(self, database_url: Optional[str] = None):
self.url = database_url or os.environ.get("DATABASE_URL", DEFAULT_DATABASE_URL)
connect_args = {"check_same_thread": False} if self.url.startswith("sqlite") else {}
self.engine = create_engine(self.url, connect_args=connect_args)
self.session_factory = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
def init_models(self) -> None:
Base.metadata.create_all(bind=self.engine)
def get_session(self) -> Iterator[Session]:
session = self.session_factory()
try:
yield session
finally:
session.close()
def get_db() -> Iterator[Session]:
"""Marker dependency; `create_app` overrides this with a real `Database.get_session`."""
raise NotImplementedError("get_db must be overridden via app.dependency_overrides")