feat(saints-dictons): table saint_du_jour + API + import standalone 366j
- Nouveau modèle SaintDuJour (mois+jour+saints_json, indépendant de l'année) - Router /api/saints et /api/saints/jour (mois+jour → liste de prénoms) - Script standalone import_webapp_db.py : saints_du_jour.json → saint_du_jour, dictons_du_jour.json → dicton ; modes replace/append, --dry-run, --region - Données JSON 366 jours : saints_du_jour.json + dictons_du_jour.json - Scripts scraping/export calendrier_lunaire/saints_dictons/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _ensure_schema(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS saint_du_jour (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mois INTEGER NOT NULL,
|
||||
jour INTEGER NOT NULL,
|
||||
saints_json TEXT NOT NULL,
|
||||
source_url TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(mois, jour)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dicton (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mois INTEGER NOT NULL,
|
||||
jour INTEGER,
|
||||
texte TEXT NOT NULL,
|
||||
region TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_dicton_mois_jour ON dicton(mois, jour)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_saint_du_jour_mois_jour ON saint_du_jour(mois, jour)")
|
||||
|
||||
|
||||
def _load_json_rows(path: Path, required_key: str) -> list[dict]:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError(f"{path}: format JSON attendu = liste d'objets")
|
||||
rows: list[dict] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if required_key not in item:
|
||||
continue
|
||||
rows.append(item)
|
||||
return rows
|
||||
|
||||
|
||||
def _import_saints(conn: sqlite3.Connection, saints_rows: list[dict], mode: str) -> int:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
inserted = 0
|
||||
|
||||
if mode == "replace":
|
||||
conn.execute("DELETE FROM saint_du_jour")
|
||||
|
||||
for row in saints_rows:
|
||||
mois = int(row["mois"])
|
||||
jour = int(row["jour"])
|
||||
saints = row.get("saints") or []
|
||||
if not isinstance(saints, list):
|
||||
saints = [str(saints)]
|
||||
saints = [str(x).strip() for x in saints if str(x).strip()]
|
||||
if not saints:
|
||||
continue
|
||||
saints_json = json.dumps(saints, ensure_ascii=False)
|
||||
source_url = row.get("source_url")
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO saint_du_jour (mois, jour, saints_json, source_url, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(mois, jour)
|
||||
DO UPDATE SET
|
||||
saints_json = excluded.saints_json,
|
||||
source_url = excluded.source_url,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(mois, jour, saints_json, source_url, now),
|
||||
)
|
||||
inserted += 1
|
||||
return inserted
|
||||
|
||||
|
||||
def _import_dictons(conn: sqlite3.Connection, dicton_rows: list[dict], mode: str, region: str | None) -> int:
|
||||
inserted = 0
|
||||
|
||||
if mode == "replace":
|
||||
if region:
|
||||
conn.execute("DELETE FROM dicton WHERE region = ?", (region,))
|
||||
else:
|
||||
conn.execute("DELETE FROM dicton")
|
||||
|
||||
for row in dicton_rows:
|
||||
mois = int(row["mois"])
|
||||
jour = int(row["jour"])
|
||||
dictons = row.get("dictons") or []
|
||||
if not isinstance(dictons, list):
|
||||
dictons = [str(dictons)]
|
||||
dictons = [str(x).strip() for x in dictons if str(x).strip()]
|
||||
if not dictons:
|
||||
continue
|
||||
|
||||
for texte in dictons:
|
||||
if mode == "append":
|
||||
exists = conn.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM dicton
|
||||
WHERE mois = ? AND jour = ? AND texte = ? AND COALESCE(region, '') = COALESCE(?, '')
|
||||
LIMIT 1
|
||||
""",
|
||||
(mois, jour, texte, region),
|
||||
).fetchone()
|
||||
if exists:
|
||||
continue
|
||||
conn.execute(
|
||||
"INSERT INTO dicton (mois, jour, texte, region) VALUES (?, ?, ?, ?)",
|
||||
(mois, jour, texte, region),
|
||||
)
|
||||
inserted += 1
|
||||
return inserted
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Importe saints du jour + dictons dans SQLite (hors webapp)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
default="data/jardin.db",
|
||||
help="Chemin SQLite cible",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--saints-json",
|
||||
default="calendrier_lunaire/saints_dictons/saints_du_jour.json",
|
||||
help="JSON saints_du_jour",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dictons-json",
|
||||
default="calendrier_lunaire/saints_dictons/dictons_du_jour.json",
|
||||
help="JSON dictons_du_jour",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["replace", "append"],
|
||||
default="replace",
|
||||
help="replace: purge puis recharge ; append: ajoute sans doublons",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--region",
|
||||
default="National",
|
||||
help="Région stockée dans table dicton (vide pour NULL)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
db_path = Path(args.db)
|
||||
saints_path = Path(args.saints_json)
|
||||
dictons_path = Path(args.dictons_json)
|
||||
region = args.region.strip() or None
|
||||
|
||||
saints_rows = _load_json_rows(saints_path, "saints")
|
||||
dicton_rows = _load_json_rows(dictons_path, "dictons")
|
||||
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
_ensure_schema(conn)
|
||||
conn.execute("BEGIN")
|
||||
saints_count = _import_saints(conn, saints_rows, args.mode)
|
||||
dictons_count = _import_dictons(conn, dicton_rows, args.mode, region)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(f"Import terminé ({args.mode})")
|
||||
print(f" Saints importés: {saints_count}")
|
||||
print(f" Dictons importés: {dictons_count} (region={region!r})")
|
||||
print(f" Base: {db_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user