"""Crée une édition vierge et son premier compte coordination. À utiliser pour démarrer une vraie instance, sans le jeu de démonstration : docker compose run --rm backend uv run python -m app.bootstrap \\ --name "Festival 2026" --start 2026-07-01 --end 2026-07-04 \\ --email coordination@monfestival.fr --password "mot-de-passe-solide" Imprime l'UUID de l'édition à reporter dans VITE_EDITION_ID. """ import argparse import datetime as dt import sys from sqlmodel import Session, select from app.auth.passwords import MIN_LENGTH, hash_password from app.db import engine from app.models import Account, DayPart, Edition, Role # (code, libellé, début, fin) — fin <= début ⇒ passe minuit. _DEFAULT_DAY_PARTS = [ ("MATIN", "Matin", dt.time(8, 0), dt.time(13, 0)), ("APRES_MIDI", "Après-midi", dt.time(13, 0), dt.time(19, 0)), ("SOIREE", "Soirée", dt.time(19, 0), dt.time(2, 0)), ] def _parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(prog="python -m app.bootstrap") parser.add_argument("--name", required=True) parser.add_argument("--start", required=True, help="AAAA-MM-JJ") parser.add_argument("--end", required=True, help="AAAA-MM-JJ") parser.add_argument("--email", required=True, help="Compte coordination") parser.add_argument("--password", required=True) parser.add_argument("--year", type=int, default=None) parser.add_argument( "--no-day-parts", action="store_true", help="Ne pas créer les créneaux types par défaut", ) parser.add_argument("--retention-days", type=int, default=None) return parser.parse_args(argv) def run(args: argparse.Namespace, session: Session) -> Edition: start = dt.date.fromisoformat(args.start) end = dt.date.fromisoformat(args.end) if end < start: raise SystemExit("--end doit être postérieur ou égal à --start") if len(args.password) < MIN_LENGTH: raise SystemExit(f"--password : au moins {MIN_LENGTH} caractères") edition = Edition( year=args.year or start.year, name=args.name, start_date=start, end_date=end, retention_days=args.retention_days, ) session.add(edition) session.flush() existing = session.exec( select(Account) .where(Account.edition_id == edition.id) .where(Account.email == args.email.strip()) ).first() if existing is not None: raise SystemExit("Un compte avec cet email existe déjà.") session.add( Account( edition_id=edition.id, email=args.email.strip(), role=Role.COORDINATOR, password_hash=hash_password(args.password), ) ) if not args.no_day_parts: for order, (code, label, begin, finish) in enumerate( _DEFAULT_DAY_PARTS ): session.add( DayPart( edition_id=edition.id, code=code, label=label, start_time=begin, end_time=finish, order=order, ) ) session.commit() session.refresh(edition) return edition def main() -> None: args = _parse_args(sys.argv[1:]) with Session(engine) as session: edition = run(args, session) print(f"Édition créée : {edition.id}") print(f" {edition.name} ({edition.start_date} → {edition.end_date})") print(f"Compte coordination : {args.email} (mot de passe défini)") if not args.no_day_parts: print( "Créneaux types : " + ", ".join(code for code, *_ in _DEFAULT_DAY_PARTS) ) print() print("Frontend : VITE_API_MODE=http " f"VITE_EDITION_ID={edition.id}") if __name__ == "__main__": main()