Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et finances personnelles avec import de relevés bancaires. Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS + Vite + Tailwind + ECharts, déploiement Docker Compose. Modules auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework de connecteurs à deux voies (importeurs de fichiers + ingestion JSON) pour brancher de nouvelles sources sans toucher au noyau. Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16 (28 tables, SPA servie par nginx, wizard de premier démarrage). Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les documents de conception et de recherche dans docs/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 lines
1.5 KiB
Python
46 lines
1.5 KiB
Python
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
from sqlalchemy import Engine, create_engine, event
|
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.core.config import get_settings
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
"""Single declarative base for the whole application."""
|
|
|
|
|
|
def _build_engine(url: str) -> Engine:
|
|
if url.startswith("sqlite"):
|
|
# Test configuration (pytest runs on SQLite in-memory): a StaticPool
|
|
# shares the single in-memory database across sessions/threads.
|
|
return create_engine(
|
|
url,
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
return create_engine(url, pool_pre_ping=True)
|
|
|
|
|
|
engine = _build_engine(get_settings().database_url)
|
|
|
|
if engine.dialect.name == "sqlite":
|
|
# SQLite does not enforce foreign keys (and thus ON DELETE CASCADE, used by
|
|
# the import rollback) unless the pragma is enabled per connection.
|
|
@event.listens_for(engine, "connect")
|
|
def _enable_sqlite_fks(dbapi_connection: Any, _record: Any) -> None:
|
|
cursor = dbapi_connection.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
|
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
|
|
|
|
|
def get_db() -> Iterator[Session]:
|
|
"""FastAPI dependency: one session per request, closed automatically."""
|
|
with SessionLocal() as session:
|
|
yield session
|