"""Dependances partagees du generateur SEO · reutilisation stricte (#5). On NE reimplemente rien qui existe deja dans le mandat : - `validator` (validateur JSON-Schema draft-07 maison, zero pip) et - `branding` (tokens de marque #4 + STATUTS_SANS_PRIX / devise #10) sont importes du livrable Publiciste, source unique de verite pour ces deux briques. Le contrat d'entree consomme est `projets_master.schema.json` (contrat Faisabilite<->Publiciste), lui aussi partage. Fonctions locales minimales : chargement des fichiers + `slugify` deterministe (pas de dependance externe, pas d'horloge). """ from __future__ import annotations import json import os import re import sys import unicodedata _HERE = os.path.dirname(os.path.abspath(__file__)) _SEO = os.path.dirname(_HERE) _DELIVERABLES = os.path.normpath(os.path.join(_SEO, "..")) # 05_deliverables_mvp/ _PUBLICISTE = os.path.join(_DELIVERABLES, "publiciste") _FAISABILITE = os.path.join(_DELIVERABLES, "faisabilite") if _PUBLICISTE not in sys.path: sys.path.insert(0, _PUBLICISTE) from lib import branding # type: ignore # noqa: E402 from lib import validator as maison # type: ignore # noqa: E402 # Chemins des contrats/entrees par defaut. SPEC_PATH = os.path.join(_SEO, "seo_spec.json") SCHEMA_PATH = os.path.join(_SEO, "seo.schema.json") MASTER_DEFAULT = os.path.join(_SEO, "fixtures", "projets_master.json") MASTER_SCHEMA_PATH = os.path.join(_FAISABILITE, "projets_master.schema.json") def load_json(path: str) -> dict: with open(path, encoding="utf-8") as fh: return json.load(fh) def load_spec() -> dict: return load_json(SPEC_PATH) def load_master(path: str | None = None) -> dict: return load_json(path or MASTER_DEFAULT) def validate_master(master: dict) -> list[str]: """Verifie que l'entree respecte le contrat Publiciste avant tout usage.""" return list(maison.validate(master, load_json(MASTER_SCHEMA_PATH))) def validate(instance, schema: dict) -> list[str]: return list(maison.validate(instance, schema)) _SLUG_STRIP = re.compile(r"[^a-z0-9]+") def slugify(text: str) -> str: """Slug URL deterministe : minuscule, sans accent, mots joints par '-'. Conserve les chiffres presents dans la donnee source (ex. « 1069 Crisfer » -> « 1069-crisfer ») : on ne retire ni n'invente aucun chiffre (#6).""" norm = unicodedata.normalize("NFKD", text) norm = "".join(c for c in norm if not unicodedata.combining(c)) return _SLUG_STRIP.sub("-", norm.lower()).strip("-") def digits(text: str) -> set[str]: """Ensemble des chiffres presents dans une chaine (audit anti-invention).""" return set(re.findall(r"\d", text)) # Re-exports pour les modules soeurs (une seule source d'import). STATUTS_SANS_PRIX = branding.STATUTS_SANS_PRIX DEVISE_PRIMAIRE = branding.DEVISE_PRIMAIRE