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>
122 lines
3.1 KiB
Python
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
|