all
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""Service de gestion des événements et statistiques.
|
||||
|
||||
Chaque événement (soirée, mariage, fête...) a ses propres compteurs :
|
||||
- photos_taken : incrémenté par le webhook 'finished' de photobooth-app
|
||||
- print_requests : incrémenté à chaque demande d'impression dans la file
|
||||
- prints_done : incrémenté à chaque impression réussie
|
||||
- downloads : incrémenté à chaque téléchargement via /api/gallery/download/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import aiosqlite
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def slugify(name: str) -> str:
|
||||
"""Transforme un nom d'événement en slug ASCII sans accents ni espaces."""
|
||||
# Normalise les caractères Unicode (enlève les accents)
|
||||
nfkd = unicodedata.normalize("NFD", name)
|
||||
ascii_str = "".join(c for c in nfkd if unicodedata.category(c) != "Mn")
|
||||
ascii_str = ascii_str.lower()
|
||||
# Garde lettres, chiffres, espaces, tirets
|
||||
ascii_str = re.sub(r"[^\w\s-]", "", ascii_str)
|
||||
# Remplace espaces/tirets multiples par un underscore
|
||||
ascii_str = re.sub(r"[\s_-]+", "_", ascii_str)
|
||||
return ascii_str.strip("_") or "evenement"
|
||||
|
||||
|
||||
_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
started_at REAL NOT NULL,
|
||||
ended_at REAL,
|
||||
photos_taken INTEGER NOT NULL DEFAULT 0,
|
||||
print_requests INTEGER NOT NULL DEFAULT 0,
|
||||
prints_done INTEGER NOT NULL DEFAULT 0,
|
||||
downloads INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
"""
|
||||
|
||||
_VALID_COUNTERS = {"photos_taken", "print_requests", "prints_done", "downloads"}
|
||||
|
||||
|
||||
class EventService:
|
||||
def __init__(self):
|
||||
self._db: aiosqlite.Connection | None = None
|
||||
|
||||
async def init_db(self, db_path: Path):
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._db = await aiosqlite.connect(str(db_path))
|
||||
self._db.row_factory = aiosqlite.Row
|
||||
await self._db.execute(_SCHEMA)
|
||||
await self._db.commit()
|
||||
logger.info("EventService initialisé : %s", db_path)
|
||||
|
||||
async def ensure_event(self, slug: str, name: str, started_at: float):
|
||||
"""Crée la ligne pour cet événement si elle n'existe pas encore."""
|
||||
await self._db.execute(
|
||||
"""INSERT INTO events (name, slug, started_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(slug) DO UPDATE SET name = excluded.name""",
|
||||
(name, slug, started_at),
|
||||
)
|
||||
await self._db.commit()
|
||||
|
||||
async def archive_event(self, slug: str):
|
||||
"""Marque un événement comme terminé (ended_at = maintenant)."""
|
||||
await self._db.execute(
|
||||
"UPDATE events SET ended_at = ? WHERE slug = ? AND ended_at IS NULL",
|
||||
(datetime.now().timestamp(), slug),
|
||||
)
|
||||
await self._db.commit()
|
||||
|
||||
async def increment(self, slug: str, counter: str):
|
||||
"""Incrémente un compteur de l'événement identifié par son slug.
|
||||
|
||||
Crée automatiquement une ligne si elle n'existe pas.
|
||||
"""
|
||||
if counter not in _VALID_COUNTERS:
|
||||
logger.warning("Compteur inconnu : %s", counter)
|
||||
return
|
||||
# Upsert : insère si absent, sinon incrémente
|
||||
await self._db.execute(
|
||||
f"""INSERT INTO events (name, slug, started_at, {counter})
|
||||
VALUES (?, ?, unixepoch(), 1)
|
||||
ON CONFLICT(slug) DO UPDATE SET {counter} = {counter} + 1""",
|
||||
(slug, slug),
|
||||
)
|
||||
await self._db.commit()
|
||||
|
||||
async def get_stats(self, slug: str) -> dict:
|
||||
"""Retourne les stats pour un slug donné."""
|
||||
async with self._db.execute(
|
||||
"""SELECT photos_taken, print_requests, prints_done, downloads
|
||||
FROM events WHERE slug = ?""",
|
||||
(slug,),
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
if not row:
|
||||
return {"photos_taken": 0, "print_requests": 0, "prints_done": 0, "downloads": 0}
|
||||
return dict(row)
|
||||
|
||||
async def get_history(self) -> list[dict]:
|
||||
"""Retourne tous les événements triés du plus récent au plus ancien."""
|
||||
async with self._db.execute(
|
||||
"""SELECT id, name, slug, started_at, ended_at,
|
||||
photos_taken, print_requests, prints_done, downloads
|
||||
FROM events
|
||||
ORDER BY started_at DESC"""
|
||||
) as cur:
|
||||
rows = await cur.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
async def close(self):
|
||||
if self._db:
|
||||
await self._db.close()
|
||||
Reference in New Issue
Block a user