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:
@@ -49,6 +49,7 @@ from app.routers import ( # noqa
|
|||||||
media,
|
media,
|
||||||
tools,
|
tools,
|
||||||
dictons,
|
dictons,
|
||||||
|
saints,
|
||||||
astuces,
|
astuces,
|
||||||
recoltes,
|
recoltes,
|
||||||
lunar,
|
lunar,
|
||||||
@@ -64,6 +65,7 @@ app.include_router(settings.router, prefix="/api")
|
|||||||
app.include_router(media.router, prefix="/api")
|
app.include_router(media.router, prefix="/api")
|
||||||
app.include_router(tools.router, prefix="/api")
|
app.include_router(tools.router, prefix="/api")
|
||||||
app.include_router(dictons.router, prefix="/api")
|
app.include_router(dictons.router, prefix="/api")
|
||||||
|
app.include_router(saints.router, prefix="/api")
|
||||||
app.include_router(astuces.router, prefix="/api")
|
app.include_router(astuces.router, prefix="/api")
|
||||||
app.include_router(recoltes.router, prefix="/api")
|
app.include_router(recoltes.router, prefix="/api")
|
||||||
app.include_router(lunar.router, prefix="/api")
|
app.include_router(lunar.router, prefix="/api")
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ from app.models.dicton import Dicton # noqa
|
|||||||
from app.models.astuce import Astuce # noqa
|
from app.models.astuce import Astuce # noqa
|
||||||
from app.models.recolte import Recolte, Observation # noqa
|
from app.models.recolte import Recolte, Observation # noqa
|
||||||
from app.models.meteo import MeteoStation, MeteoOpenMeteo # noqa
|
from app.models.meteo import MeteoStation, MeteoOpenMeteo # noqa
|
||||||
|
from app.models.saint import SaintDuJour # noqa
|
||||||
|
|||||||
14
backend/app/models/saint.py
Normal file
14
backend/app/models/saint.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class SaintDuJour(SQLModel, table=True):
|
||||||
|
"""Saints fêtés pour un jour donné (indépendant de l'année)."""
|
||||||
|
|
||||||
|
__tablename__ = "saint_du_jour"
|
||||||
|
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
mois: int = Field(index=True) # 1-12
|
||||||
|
jour: int = Field(index=True) # 1-31
|
||||||
|
saints_json: str = Field(default="[]") # JSON array : ["St-Basile", "St-Grégoire", ...]
|
||||||
|
source_url: Optional[str] = None # URL source de scraping
|
||||||
57
backend/app/routers/saints.py
Normal file
57
backend/app/routers/saints.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""Router saints — consultation des saints du jour (indépendant de l'année)."""
|
||||||
|
import json
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from app.database import get_session
|
||||||
|
from app.models.saint import SaintDuJour
|
||||||
|
|
||||||
|
router = APIRouter(tags=["saints"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/saints", response_model=List[SaintDuJour])
|
||||||
|
def list_saints(
|
||||||
|
mois: Optional[int] = Query(None, ge=1, le=12),
|
||||||
|
jour: Optional[int] = Query(None, ge=1, le=31),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Liste les saints. Filtrer par mois et/ou jour."""
|
||||||
|
q = select(SaintDuJour)
|
||||||
|
if mois is not None:
|
||||||
|
q = q.where(SaintDuJour.mois == mois)
|
||||||
|
if jour is not None:
|
||||||
|
q = q.where(SaintDuJour.jour == jour)
|
||||||
|
q = q.order_by(SaintDuJour.mois, SaintDuJour.jour)
|
||||||
|
return session.exec(q).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/saints/jour", response_model=dict)
|
||||||
|
def get_saints_du_jour(
|
||||||
|
mois: int = Query(..., ge=1, le=12),
|
||||||
|
jour: int = Query(..., ge=1, le=31),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Retourne les saints et leur liste parsée pour un jour précis."""
|
||||||
|
row = session.exec(
|
||||||
|
select(SaintDuJour).where(
|
||||||
|
SaintDuJour.mois == mois,
|
||||||
|
SaintDuJour.jour == jour,
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return {"mois": mois, "jour": jour, "saints": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
saints_list = json.loads(row.saints_json)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
saints_list = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mois": row.mois,
|
||||||
|
"jour": row.jour,
|
||||||
|
"saints": saints_list,
|
||||||
|
"source_url": row.source_url,
|
||||||
|
}
|
||||||
2930
calendrier_lunaire/saints_dictons/dictons_du_jour.json
Normal file
2930
calendrier_lunaire/saints_dictons/dictons_du_jour.json
Normal file
File diff suppressed because it is too large
Load Diff
113
calendrier_lunaire/saints_dictons/export_saints_dictons_json.py
Normal file
113
calendrier_lunaire/saints_dictons/export_saints_dictons_json.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_mmdd(mmdd: str) -> tuple[int, int]:
|
||||||
|
if len(mmdd) != 4 or not mmdd.isdigit():
|
||||||
|
raise ValueError(f"MMDD invalide: {mmdd}")
|
||||||
|
month = int(mmdd[:2])
|
||||||
|
day = int(mmdd[2:])
|
||||||
|
if not (1 <= month <= 12 and 1 <= day <= 31):
|
||||||
|
raise ValueError(f"MMDD hors plage: {mmdd}")
|
||||||
|
return month, day
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list(value: object) -> list[str]:
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
if isinstance(value, list):
|
||||||
|
out: list[str] = []
|
||||||
|
for item in value:
|
||||||
|
txt = str(item).strip()
|
||||||
|
if txt and txt not in out:
|
||||||
|
out.append(txt)
|
||||||
|
return out
|
||||||
|
txt = str(value).strip()
|
||||||
|
return [txt] if txt else []
|
||||||
|
|
||||||
|
|
||||||
|
def export_files(source_file: Path, saints_out: Path, dictons_out: Path) -> tuple[int, int]:
|
||||||
|
source = json.loads(source_file.read_text(encoding="utf-8"))
|
||||||
|
rows = source.get("data", [])
|
||||||
|
saints_rows: list[dict] = []
|
||||||
|
dictons_rows: list[dict] = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
mmdd = str(row.get("mmdd", "")).strip()
|
||||||
|
if not mmdd:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
month, day = _parse_mmdd(mmdd)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
saints = _as_list(row.get("saints"))
|
||||||
|
dictons = _as_list(row.get("dictons"))
|
||||||
|
source_url = row.get("source_url")
|
||||||
|
|
||||||
|
if saints:
|
||||||
|
saints_rows.append(
|
||||||
|
{
|
||||||
|
"mois": month,
|
||||||
|
"jour": day,
|
||||||
|
"saints": saints,
|
||||||
|
"source_url": source_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if dictons:
|
||||||
|
dictons_rows.append(
|
||||||
|
{
|
||||||
|
"mois": month,
|
||||||
|
"jour": day,
|
||||||
|
"dictons": dictons,
|
||||||
|
"source_url": source_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
saints_rows.sort(key=lambda r: (r["mois"], r["jour"]))
|
||||||
|
dictons_rows.sort(key=lambda r: (r["mois"], r["jour"]))
|
||||||
|
|
||||||
|
saints_out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dictons_out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
saints_out.write_text(json.dumps(saints_rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
dictons_out.write_text(json.dumps(dictons_rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return len(saints_rows), len(dictons_rows)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Génère saints_du_jour.json et dictons_du_jour.json depuis saints_YYYY.json."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--source",
|
||||||
|
default="calendrier_lunaire/saints_dictons/saints_2026.json",
|
||||||
|
help="Source JSON issue du scraper annuel",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--saints-out",
|
||||||
|
default="calendrier_lunaire/saints_dictons/saints_du_jour.json",
|
||||||
|
help="Fichier JSON de sortie pour les saints",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dictons-out",
|
||||||
|
default="calendrier_lunaire/saints_dictons/dictons_du_jour.json",
|
||||||
|
help="Fichier JSON de sortie pour les dictons",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
source_file = Path(args.source)
|
||||||
|
saints_out = Path(args.saints_out)
|
||||||
|
dictons_out = Path(args.dictons_out)
|
||||||
|
saints_count, dictons_count = export_files(source_file, saints_out, dictons_out)
|
||||||
|
print(f"Saints exportés : {saints_count} jours -> {saints_out}")
|
||||||
|
print(f"Dictons exportés : {dictons_count} jours -> {dictons_out}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
191
calendrier_lunaire/saints_dictons/import_saints_dictons_db.py
Normal file
191
calendrier_lunaire/saints_dictons/import_saints_dictons_db.py
Normal file
@@ -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())
|
||||||
307
calendrier_lunaire/saints_dictons/import_webapp_db.py
Normal file
307
calendrier_lunaire/saints_dictons/import_webapp_db.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Import des saints et dictons dans la base de données de la webapp Jardin.
|
||||||
|
|
||||||
|
Les données sont INDÉPENDANTES DE L'ANNÉE : stockées par (mois, jour) uniquement.
|
||||||
|
|
||||||
|
Sources JSON :
|
||||||
|
- saints_du_jour.json → table saint_du_jour
|
||||||
|
- dictons_du_jour.json → table dicton
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
# Import complet (remplace les données existantes)
|
||||||
|
python import_webapp_db.py
|
||||||
|
|
||||||
|
# Import vers une autre DB
|
||||||
|
python import_webapp_db.py --db /chemin/vers/jardin.db
|
||||||
|
|
||||||
|
# Mode append (ajoute sans supprimer les existants)
|
||||||
|
python import_webapp_db.py --mode append
|
||||||
|
|
||||||
|
# Seulement les saints ou seulement les dictons
|
||||||
|
python import_webapp_db.py --only saints
|
||||||
|
python import_webapp_db.py --only dictons
|
||||||
|
|
||||||
|
# Tagger les dictons avec une région
|
||||||
|
python import_webapp_db.py --region "Auvergne"
|
||||||
|
|
||||||
|
# Prévisualisation sans modification
|
||||||
|
python import_webapp_db.py --dry-run
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Chemins par défaut relatifs à ce script
|
||||||
|
_HERE = Path(__file__).parent
|
||||||
|
_REPO_ROOT = _HERE.parent.parent # racine du projet jardin/
|
||||||
|
DEFAULT_DB = _REPO_ROOT / "data" / "jardin.db"
|
||||||
|
DEFAULT_SAINTS_JSON = _HERE / "saints_du_jour.json"
|
||||||
|
DEFAULT_DICTONS_JSON = _HERE / "dictons_du_jour.json"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Schémas SQL attendus par la webapp
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
SQL_CREATE_SAINT_DU_JOUR = """
|
||||||
|
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 DEFAULT '[]',
|
||||||
|
source_url TEXT,
|
||||||
|
UNIQUE (mois, jour)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_CREATE_INDEX_SAINT = """
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_saint_du_jour_mois_jour
|
||||||
|
ON saint_du_jour (mois, jour)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# La table dicton existe déjà dans la webapp (SQLModel).
|
||||||
|
# Colonnes: id, mois, jour, texte, region
|
||||||
|
# On s'assure juste qu'elle existe (ne recrée pas si déjà présente).
|
||||||
|
SQL_CREATE_DICTON = """
|
||||||
|
CREATE TABLE IF NOT EXISTS dicton (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mois INTEGER NOT NULL,
|
||||||
|
jour INTEGER,
|
||||||
|
texte TEXT NOT NULL,
|
||||||
|
region TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_CREATE_INDEX_DICTON = """
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dicton_mois_jour ON dicton (mois, jour)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Chargement JSON
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_json(path: Path, label: str) -> list:
|
||||||
|
if not path.exists():
|
||||||
|
print(f"[ERREUR] Fichier introuvable : {path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
with path.open(encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
print(f"[ERREUR] {label} : attendu une liste JSON, obtenu {type(data).__name__}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f" {label} : {len(data)} entrées chargées depuis {path.name}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Import saints
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def import_saints(conn: sqlite3.Connection, data: list, mode: str, dry_run: bool) -> int:
|
||||||
|
"""Importe les saints dans saint_du_jour. Retourne le nombre d'entrées traitées."""
|
||||||
|
conn.execute(SQL_CREATE_SAINT_DU_JOUR)
|
||||||
|
conn.execute(SQL_CREATE_INDEX_SAINT)
|
||||||
|
|
||||||
|
if mode == "replace" and not dry_run:
|
||||||
|
conn.execute("DELETE FROM saint_du_jour")
|
||||||
|
print(" [replace] saint_du_jour vidée")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for entry in data:
|
||||||
|
mois = entry.get("mois")
|
||||||
|
jour = entry.get("jour")
|
||||||
|
saints = entry.get("saints", [])
|
||||||
|
source_url = entry.get("source_url")
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if not isinstance(mois, int) or not isinstance(jour, int):
|
||||||
|
continue
|
||||||
|
if not (1 <= mois <= 12 and 1 <= jour <= 31):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normaliser en liste de strings non vides
|
||||||
|
if isinstance(saints, str):
|
||||||
|
saints = [saints]
|
||||||
|
saints = [s.strip() for s in saints if isinstance(s, str) and s.strip()]
|
||||||
|
saints_json = json.dumps(saints, ensure_ascii=False)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mode == "append":
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM saint_du_jour WHERE mois=? AND jour=?", (mois, jour)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO saint_du_jour (mois, jour, saints_json, source_url) VALUES (?,?,?,?)",
|
||||||
|
(mois, jour, saints_json, source_url),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if skipped:
|
||||||
|
print(f" saints ignorés (mode append, déjà présents) : {skipped}")
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Import dictons
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def import_dictons(conn: sqlite3.Connection, data: list, mode: str, region: str | None, dry_run: bool) -> int:
|
||||||
|
"""Importe les dictons dans la table dicton. Retourne le nombre d'entrées insérées."""
|
||||||
|
conn.execute(SQL_CREATE_DICTON)
|
||||||
|
conn.execute(SQL_CREATE_INDEX_DICTON)
|
||||||
|
|
||||||
|
if mode == "replace" and not dry_run:
|
||||||
|
conn.execute("DELETE FROM dicton")
|
||||||
|
print(" [replace] dicton vidée")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for entry in data:
|
||||||
|
mois = entry.get("mois")
|
||||||
|
jour = entry.get("jour") # peut être None
|
||||||
|
dictons = entry.get("dictons", [])
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if not isinstance(mois, int):
|
||||||
|
continue
|
||||||
|
if not (1 <= mois <= 12):
|
||||||
|
continue
|
||||||
|
if jour is not None and not (1 <= jour <= 31):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normaliser
|
||||||
|
if isinstance(dictons, str):
|
||||||
|
dictons = [dictons]
|
||||||
|
dictons = [d.strip() for d in dictons if isinstance(d, str) and d.strip()]
|
||||||
|
|
||||||
|
for texte in dictons:
|
||||||
|
if dry_run:
|
||||||
|
count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mode == "append":
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM dicton WHERE mois=? AND jour IS ? AND texte=?",
|
||||||
|
(mois, jour, texte),
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO dicton (mois, jour, texte, region) VALUES (?,?,?,?)",
|
||||||
|
(mois, jour, texte, region),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Point d'entrée
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Import saints et dictons dans la BDD webapp Jardin (indépendant de l'année)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--db", default=str(DEFAULT_DB),
|
||||||
|
help=f"Chemin vers jardin.db (défaut: {DEFAULT_DB})")
|
||||||
|
parser.add_argument("--saints-json", default=str(DEFAULT_SAINTS_JSON),
|
||||||
|
help=f"Fichier saints_du_jour.json (défaut: {DEFAULT_SAINTS_JSON.name})")
|
||||||
|
parser.add_argument("--dictons-json", default=str(DEFAULT_DICTONS_JSON),
|
||||||
|
help=f"Fichier dictons_du_jour.json (défaut: {DEFAULT_DICTONS_JSON.name})")
|
||||||
|
parser.add_argument("--mode", choices=["replace", "append"], default="replace",
|
||||||
|
help="replace = purge + recharge (défaut) | append = ajoute sans doublons")
|
||||||
|
parser.add_argument("--only", choices=["saints", "dictons", "both"], default="both",
|
||||||
|
help="Importer uniquement saints, dictons, ou les deux (défaut: both)")
|
||||||
|
parser.add_argument("--region", default=None,
|
||||||
|
help="Tag région pour les dictons (ex: 'Auvergne', 'National'). Vide = NULL")
|
||||||
|
parser.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Simulation : affiche les comptages sans modifier la BDD")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db_path = Path(args.db)
|
||||||
|
if not db_path.exists():
|
||||||
|
print(f"[ERREUR] Base de données introuvable : {db_path}", file=sys.stderr)
|
||||||
|
print(" → Vérifiez que la webapp a démarré au moins une fois pour créer la BDD.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
region = args.region if args.region else None
|
||||||
|
do_saints = args.only in ("saints", "both")
|
||||||
|
do_dictons = args.only in ("dictons", "both")
|
||||||
|
|
||||||
|
print(f"\n=== Import saints/dictons → {db_path} ===")
|
||||||
|
print(f" Mode : {args.mode}")
|
||||||
|
print(f" Scope : {args.only}")
|
||||||
|
if region:
|
||||||
|
print(f" Région: {region}")
|
||||||
|
if args.dry_run:
|
||||||
|
print(" *** DRY-RUN — aucune modification en BDD ***")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Charger les JSON nécessaires
|
||||||
|
saints_data = load_json(Path(args.saints_json), "saints_du_jour") if do_saints else []
|
||||||
|
dictons_data = load_json(Path(args.dictons_json), "dictons_du_jour") if do_dictons else []
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Connexion SQLite
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA foreign_keys=OFF") # pas de cascade gestion manuelle
|
||||||
|
|
||||||
|
saints_count = 0
|
||||||
|
dictons_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
|
||||||
|
if do_saints:
|
||||||
|
saints_count = import_saints(conn, saints_data, args.mode, args.dry_run)
|
||||||
|
if do_dictons:
|
||||||
|
dictons_count = import_dictons(conn, dictons_data, args.mode, region, args.dry_run)
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
conn.rollback()
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"\n[ERREUR] Transaction annulée : {exc}", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Résumé
|
||||||
|
print()
|
||||||
|
print("=== Résultat ===")
|
||||||
|
if do_saints:
|
||||||
|
prefix = "[DRY-RUN] " if args.dry_run else ""
|
||||||
|
print(f" {prefix}Saints importés : {saints_count} jours → table saint_du_jour")
|
||||||
|
if do_dictons:
|
||||||
|
prefix = "[DRY-RUN] " if args.dry_run else ""
|
||||||
|
print(f" {prefix}Dictons importés : {dictons_count} entrées → table dicton")
|
||||||
|
print(f" Base : {db_path}")
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n → Relancez sans --dry-run pour appliquer les changements.")
|
||||||
|
else:
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
print(f"\n Import terminé le {ts}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
5271
calendrier_lunaire/saints_dictons/saints_du_jour.json
Normal file
5271
calendrier_lunaire/saints_dictons/saints_du_jour.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user