trainlog/tools/exercise_names.py

83 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Strict reader for the shared canonical exercise-name ownership map."""
import json
import unicodedata
import uuid
from pathlib import Path
CATALOG_PATH = (
Path(__file__).resolve().parents[1] / "catalog" / "exercise-names-v1.json"
)
class ExerciseNameCatalogError(ValueError):
pass
def normalize_catalog_name(value):
if not isinstance(value, str):
raise ExerciseNameCatalogError("nom canonique non textuel")
normalized = " ".join(unicodedata.normalize("NFC", value.casefold()).split())
if not normalized:
raise ExerciseNameCatalogError("nom canonique vide")
return normalized
def load_exercise_names(path=CATALOG_PATH):
try:
root = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ExerciseNameCatalogError(
f"catalogue de noms d'exercices illisible: {error}"
) from error
if not isinstance(root, dict) or set(root) != {"format", "version", "names"}:
raise ExerciseNameCatalogError("forme racine exercise-names-v1 invalide")
if root["format"] != "trainlog-exercise-names-v1" or root["version"] != 1:
raise ExerciseNameCatalogError("format/version exercise-names-v1 invalide")
if not isinstance(root["names"], list):
raise ExerciseNameCatalogError("liste names absente")
result = {}
normalized_owners = set()
previous_names = set()
for index, item in enumerate(root["names"]):
label = f"names[{index}]"
if not isinstance(item, dict) or set(item) != {
"exercise_id", "display_name", "normalized_name", "previous_display_names"
}:
raise ExerciseNameCatalogError(f"{label}: forme invalide")
exercise_id = item["exercise_id"]
if not isinstance(exercise_id, str) or not exercise_id.startswith("ex_"):
raise ExerciseNameCatalogError(f"{label}: exercise_id invalide")
try:
parsed = uuid.UUID(exercise_id[3:])
except (ValueError, AttributeError) as error:
raise ExerciseNameCatalogError(f"{label}: exercise_id invalide") from error
if parsed.version != 4 or str(parsed) != exercise_id[3:]:
raise ExerciseNameCatalogError(f"{label}: exercise_id UUIDv4 invalide")
display_name = item["display_name"]
normalized_name = normalize_catalog_name(display_name)
if display_name != display_name.strip() or item["normalized_name"] != normalized_name:
raise ExerciseNameCatalogError(f"{label}: nom normalisé incohérent")
if exercise_id in result or normalized_name in normalized_owners:
raise ExerciseNameCatalogError(f"{label}: identité canonique dupliquée")
previous = item["previous_display_names"]
if not isinstance(previous, list) or not previous:
raise ExerciseNameCatalogError(f"{label}: previous_display_names invalide")
local_previous = set()
for old_name in previous:
old_normalized = normalize_catalog_name(old_name)
if old_name != old_name.strip() or old_normalized == normalized_name:
raise ExerciseNameCatalogError(f"{label}: ancien nom invalide")
if old_normalized in local_previous or old_normalized in previous_names:
raise ExerciseNameCatalogError(f"{label}: ancien nom dupliqué")
local_previous.add(old_normalized)
result[exercise_id] = display_name
normalized_owners.add(normalized_name)
previous_names.update(local_previous)
if normalized_owners & previous_names:
raise ExerciseNameCatalogError("un ancien nom appartient déjà à un nom canonique")
return result