from decimal import Decimal, InvalidOperation from pathlib import Path from copienator import atomic_update_json, read_json def read_scores(path: Path) -> dict: scores = read_json(annotation_score_for_return(path)) if not isinstance(scores, dict): raise ValueError(f"score.json doit contenir un objet JSON : {path}") return scores def set_score(path: Path, label: str, value: str) -> str: try: number = Decimal(value.strip().replace(",", ".")) except InvalidOperation: raise ValueError("Le score doit être un nombre.") from None if not number.is_finite() or number < 0: raise ValueError("Le score doit être un nombre positif ou nul.") previous = "" def update(scores): nonlocal previous if not isinstance(scores, dict) or label not in scores: raise ValueError(f"Label absent de score.json : {label}") previous = str(scores[label]) scores[label] = str(number) # A Rendre/score.json is often a symlink to the annotation score file. # Resolve it before atomic replacement so the link continues to work. atomic_update_json(annotation_score_for_return(path).resolve(strict=True), update, default_factory=dict) return previous def annotation_score_for_return(path: Path) -> Path: """Resolve fallback return copies as well as symlinks to their source scores.""" if path.is_symlink() or path.parent.parent.name != 'A Rendre': return path.resolve() from copienator.return_identity import RETURN_FOLDER_PATTERN match = RETURN_FOLDER_PATTERN.fullmatch(path.parent.name) if not match: return path root = path.parent.parent.parent cid = match['copy_id'] sources = read_json(root / '.copienator' / 'return_sources.json', default={}) mode = sources.get(str(int(cid))) modes = (mode,) if mode in ('Bnot', 'BGnot', 'Anot') else ('Bnot', 'BGnot', 'Anot') candidates = [p for m in modes for p in (root / m).glob('Copie*/score.json') if p.parent.name[5:].isdigit() and int(p.parent.name[5:]) == int(cid)] if len(candidates) > 1: raise ValueError('Source des scores ambiguë : relancez la préparation de A Rendre dans le mode souhaité.') return candidates[0] if candidates else path def remember_return_source(root: Path, copy_id: str, source: Path): path = root / '.copienator' / 'return_sources.json' path.parent.mkdir(exist_ok=True) atomic_update_json(path, lambda data: data.update({str(int(copy_id)): source.parent.name}), default_factory=dict)