83 lines
3.7 KiB
Python
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
|