f0a7d71357
Livrable Publiciste · Sprint 2 · Semaine 2 (seul module net-neuf · chemin critique · GAP_ANALYSIS §3.13). Cible portage VPS : otoia/capabilities/publiciste.py. - lib/parser.py : data_room/PXX/ (template v1.0) → projet dict conforme au contrat projets_master.schema.json (livré S1). Mapping colonnes par en-tête, parsing montants USD/DOP robuste. Anti-invention #6 : rétrogradation défensive « en_developpement » si prix USD manquant ; absent → null (jamais 0/inventé). - lib/validator.py : validateur JSON-Schema draft-07 (sous-ensemble) ZÉRO dépendance pip (runner Gitea sans pip). Oracle jsonschema en test si présent. - lib/generator.py + template + branding : rendu HTML luxury #4 (dark+doré, Fraunces + Cormorant Garamond) ; sans prix → « Prochainement · Détails à venir ». - publiciste.py : CLI parse/validate/generate/run. - fixtures/ : données SYNTHÉTIQUES de test (jamais publiées) P01 complète + P02 incomplète. - tests/ : 23 tests unittest (stdlib) verts. - CI : job publiciste-tests ajouté au gate (.gitea/workflows/ci.yml · Gitea #2). Vérifs (en-repo, sans VPS) : 23/23 tests verts · gate CI local vert (guard/JSON/ docs) · pipeline CLI produit un master conforme au schéma. Auto-score 4Big 95/100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
5.9 KiB
Python
161 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Publiciste Agent · orchestrateur CLI (scaffold Sprint 2).
|
|
|
|
Cible de portage sur le VPS : `otoia/capabilities/publiciste.py`
|
|
(cf. 03_agents/publiciste/AGENT.md · GAP_ANALYSIS_SPRINT1.md §3.13).
|
|
|
|
Maintient le site public `vente.otov7.com` À PARTIR DES FAISABILITÉS CANONIQUES
|
|
(data_room/PXX/), sans édition manuelle. Zéro invention de chiffre (CLAUDE.md #6).
|
|
|
|
Sous-commandes :
|
|
parse data_room/ -> projets_master.json (validé contre le schéma)
|
|
validate projets_master.json
|
|
generate projets_master.json -> index.html
|
|
run data_room/ -> projets_master.json + index.html (pipeline complet)
|
|
|
|
Pipeline (AGENT.md §Architecture technique) :
|
|
data_room/PXX/ ──► parser ──► projets_master.json ──► generator ──► index.html
|
|
|
|
⚠ Ce worker n'écrit JAMAIS sur le VPS. La commande `run` produit les artefacts
|
|
dans un dossier de sortie local ; le déploiement réel (copie vers
|
|
/opt/oto/sites/vente/ + cache-bust + WhatsApp) est un livrable ultérieur exécuté
|
|
côté serveur par l'agent (Semaines 4-5).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from lib import generator, parser, validator # noqa: E402
|
|
|
|
_SCHEMA_DIR = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "faisabilite")
|
|
)
|
|
|
|
|
|
def _load_schema(name: str) -> dict:
|
|
with open(os.path.join(_SCHEMA_DIR, name), encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def _eprint(*args) -> None:
|
|
print(*args, file=sys.stderr)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
def cmd_parse(ns: argparse.Namespace) -> int:
|
|
master = parser.build_master(ns.data_room, generated_at=ns.generated_at)
|
|
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
|
|
if errors:
|
|
_eprint("❌ projets_master.json NON conforme au schéma :")
|
|
for e in errors:
|
|
_eprint(" ·", e)
|
|
# On écrit quand même la sortie pour diagnostic si --force.
|
|
if not ns.force:
|
|
return 1
|
|
payload = json.dumps(master, ensure_ascii=False, indent=2)
|
|
if ns.out:
|
|
with open(ns.out, "w", encoding="utf-8") as fh:
|
|
fh.write(payload + "\n")
|
|
_eprint(f"✅ {len(master['projets'])} projet(s) → {ns.out}")
|
|
else:
|
|
print(payload)
|
|
return 0
|
|
|
|
|
|
def cmd_validate(ns: argparse.Namespace) -> int:
|
|
with open(ns.master, encoding="utf-8") as fh:
|
|
master = json.load(fh)
|
|
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
|
|
if errors:
|
|
_eprint(f"❌ {ns.master} : {len(errors)} erreur(s)")
|
|
for e in errors:
|
|
_eprint(" ·", e)
|
|
return 1
|
|
_eprint(f"✅ {ns.master} conforme ({len(master.get('projets', []))} projet(s)).")
|
|
return 0
|
|
|
|
|
|
def cmd_generate(ns: argparse.Namespace) -> int:
|
|
with open(ns.master, encoding="utf-8") as fh:
|
|
master = json.load(fh)
|
|
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
|
|
if errors and not ns.force:
|
|
_eprint("❌ master non conforme — génération refusée (voir `validate`). --force pour outrepasser.")
|
|
return 1
|
|
html_out = generator.render_site(master)
|
|
if ns.out:
|
|
with open(ns.out, "w", encoding="utf-8") as fh:
|
|
fh.write(html_out)
|
|
_eprint(f"✅ site généré → {ns.out} ({len(master.get('projets', []))} projet(s))")
|
|
else:
|
|
sys.stdout.write(html_out)
|
|
return 0
|
|
|
|
|
|
def cmd_run(ns: argparse.Namespace) -> int:
|
|
os.makedirs(ns.out_dir, exist_ok=True)
|
|
master = parser.build_master(ns.data_room, generated_at=ns.generated_at)
|
|
schema = _load_schema("projets_master.schema.json")
|
|
errors = validator.validate(master, schema)
|
|
master_path = os.path.join(ns.out_dir, "projets_master.json")
|
|
with open(master_path, "w", encoding="utf-8") as fh:
|
|
fh.write(json.dumps(master, ensure_ascii=False, indent=2) + "\n")
|
|
if errors:
|
|
_eprint(f"❌ master non conforme ({len(errors)} erreur(s)) — index NON généré.")
|
|
for e in errors:
|
|
_eprint(" ·", e)
|
|
return 1
|
|
html_out = generator.render_site(master)
|
|
index_path = os.path.join(ns.out_dir, "index.html")
|
|
with open(index_path, "w", encoding="utf-8") as fh:
|
|
fh.write(html_out)
|
|
_eprint(f"✅ pipeline OK — {len(master['projets'])} projet(s)")
|
|
_eprint(f" · {master_path}")
|
|
_eprint(f" · {index_path}")
|
|
return 0
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
def build_argparser() -> argparse.ArgumentParser:
|
|
ap = argparse.ArgumentParser(prog="publiciste", description=__doc__.splitlines()[0])
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
|
|
p = sub.add_parser("parse", help="data_room/ -> projets_master.json")
|
|
p.add_argument("data_room")
|
|
p.add_argument("-o", "--out")
|
|
p.add_argument("--generated-at", dest="generated_at", default=None)
|
|
p.add_argument("--force", action="store_true", help="écrire même si non conforme")
|
|
p.set_defaults(func=cmd_parse)
|
|
|
|
p = sub.add_parser("validate", help="valide un projets_master.json")
|
|
p.add_argument("master")
|
|
p.set_defaults(func=cmd_validate)
|
|
|
|
p = sub.add_parser("generate", help="projets_master.json -> index.html")
|
|
p.add_argument("master")
|
|
p.add_argument("-o", "--out")
|
|
p.add_argument("--force", action="store_true")
|
|
p.set_defaults(func=cmd_generate)
|
|
|
|
p = sub.add_parser("run", help="pipeline complet data_room/ -> out_dir/")
|
|
p.add_argument("data_room")
|
|
p.add_argument("-o", "--out-dir", dest="out_dir", default="build")
|
|
p.add_argument("--generated-at", dest="generated_at", default=None)
|
|
p.set_defaults(func=cmd_run)
|
|
return ap
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ns = build_argparser().parse_args(argv)
|
|
return ns.func(ns)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|