Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""Creation du moteur SQLAlchemy et de la session de requete."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Generator
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import Engine, create_engine, event
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_SQLITE_FILE_PREFIX = "sqlite:///"
|
|
|
|
|
|
def _ensure_sqlite_directory(database_url: str) -> None:
|
|
"""Cree le dossier parent du fichier SQLite s'il n'existe pas encore."""
|
|
if not database_url.startswith(_SQLITE_FILE_PREFIX):
|
|
return
|
|
raw_path = database_url[len(_SQLITE_FILE_PREFIX) :]
|
|
if not raw_path or raw_path == ":memory:":
|
|
return
|
|
path = Path(raw_path)
|
|
if path.parent and str(path.parent) not in ("", "."):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def create_db_engine(database_url: str | None = None) -> Engine:
|
|
settings = get_settings()
|
|
url = database_url or settings.database_url
|
|
_ensure_sqlite_directory(url)
|
|
|
|
connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
|
|
engine = create_engine(url, connect_args=connect_args, future=True, echo=False)
|
|
|
|
if url.startswith("sqlite"):
|
|
|
|
@event.listens_for(engine, "connect")
|
|
def _set_sqlite_pragma(dbapi_connection, _connection_record): # type: ignore[no-untyped-def]
|
|
cursor = dbapi_connection.cursor()
|
|
# WAL : meilleure resistance aux arrets brutaux du conteneur.
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
return engine
|
|
|
|
|
|
engine: Engine = create_db_engine()
|
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
|
|
|
|
|
def get_db() -> Generator[Session, None, None]:
|
|
"""Dependance FastAPI : ouvre une session et la referme systematiquement."""
|
|
session = SessionLocal()
|
|
try:
|
|
yield session
|
|
finally:
|
|
session.close()
|