Files
pdf_oralia_vibe/src/plesna_gerance/database/connection.py
Bertrand Benjamin a90d1c4f64 feat: mode application de bureau + packaging Windows autonome
Permet de distribuer l'application en executable Windows double-clic,
sans Python/Node/terminal pour l'utilisateur final.

- `desktop.py` : demarre le serveur sur un port libre dans un thread et
  ouvre une fenetre native (pywebview). `PLESNA_DEBUG=1` active les
  DevTools. Commande `plesna-gerance desktop` (import paresseux).
- `paths.py` : resolution centralisee des chemins selon le contexte
  (dev, executable PyInstaller, surcharge par variables d'env). Les
  donnees vont dans un dossier utilisateur inscriptible (%APPDATA%),
  les ressources dans le bundle. connection.py/storage.py utilisent
  `get_data_dir()`, app.py `resource_path()`.
- app.py : type MIME `.mjs` force (modules ES / worker PDF.js sous
  Windows).
- Groupes de deps optionnels desktop / desktop-linux / build.
- packaging/ : spec PyInstaller, build_windows.ps1, installeur Inno
  Setup. Workflow GitHub build-windows.yml (runner Windows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:21:27 +02:00

122 lines
3.1 KiB
Python

"""Database connection management for SQLite."""
import os
from collections.abc import Generator
from pathlib import Path
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
from ..paths import get_data_dir
from .models import Base, Tag
# Global engine instance
_engine = None
_SessionLocal = None
def get_db_path() -> Path:
"""Get database path from environment or default.
Resolved lazily so a packaged build writes to the per-user data dir.
"""
env_path = os.environ.get("PLESNA_DB_PATH")
if env_path:
return Path(env_path)
return get_data_dir() / "database.sqlite"
def _build_engine(db_path: Path):
"""Create a SQLAlchemy engine for the given SQLite path.
Single source of truth for engine configuration.
"""
# Create parent directory if needed
db_path.parent.mkdir(parents=True, exist_ok=True)
return create_engine(
f"sqlite:///{db_path}",
echo=False, # Set to True for SQL debugging
connect_args={"check_same_thread": False}, # Required for FastAPI
)
def get_engine(db_path: Path | None = None):
"""Get or create SQLAlchemy engine (singleton pattern)."""
global _engine
if _engine is None:
_engine = _build_engine(db_path or get_db_path())
return _engine
def get_session_factory(engine=None):
"""Get or create session factory."""
global _SessionLocal
if _SessionLocal is None:
if engine is None:
engine = get_engine()
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
return _SessionLocal
def get_session() -> Generator[Session, None, None]:
"""Dependency for FastAPI to get database session."""
SessionLocal = get_session_factory()
session = SessionLocal()
try:
yield session
finally:
session.close()
def init_db(db_path: Path | None = None) -> Path:
"""Initialize database: create all tables.
Returns the path to the database file.
"""
if db_path is None:
db_path = get_db_path()
# Reset globals so the engine/session factory rebuild against db_path
reset_connection()
# Build the engine (reuses the shared configuration) and create tables
engine = get_engine(db_path)
Base.metadata.create_all(bind=engine)
# Seed predefined tags if the table is empty
_seed_tags_if_empty(engine)
return db_path
def _seed_tags_if_empty(engine):
"""Insert predefined tags if the tags table is empty."""
from ..scripts.seed_tags import PREDEFINED_TAGS
session = Session(bind=engine)
try:
count = session.execute(text("SELECT COUNT(*) FROM tags")).scalar()
if count == 0:
for tag_name in PREDEFINED_TAGS:
session.add(Tag(nom=tag_name))
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def reset_connection():
"""Reset global connection (useful for testing)."""
global _engine, _SessionLocal
if _engine is not None:
_engine.dispose()
_engine = None
_SessionLocal = None