Standardisation 4

This commit is contained in:
2026-08-20 14:45:53 +02:00
parent 8b087bb3e4
commit bcba5facc8
6 changed files with 838 additions and 732 deletions
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
from pathlib import Path
from typing import Any
from .json_io import read_json
Log = Callable[[str], None]
def apply_checkbox_actions(
labels_data: dict[str, dict[str, Any]],
actions: list[dict[str, Any]],
log: Log,
) -> set[str]:
actions_by_label: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
for action in actions:
actions_by_label[str(action.get("label", ""))].append(action)
dirty_labels: set[str] = set()
for label, label_actions in actions_by_label.items():
if label not in labels_data:
continue
result = labels_data[label]["result"]
feedbacks = result.get("feedback", [])
global_feedbacks = [item for item in feedbacks if not item.get("box_2d")]
local_feedbacks = [item for item in feedbacks if item.get("box_2d")]
local_feedbacks.sort(key=lambda item: item["box_2d"][0])
for action in label_actions:
action_type = action.get("type")
if action_type == "score":
result["score"] = action.get("value")
dirty_labels.add(label)
log(f" > Updated score for {label} to {action.get('value')}")
elif action_type == "clear_all":
for feedback in feedbacks:
feedback["to_delete"] = True
if feedback.get("box_2d"):
feedback["norectangle"] = True
dirty_labels.add(label)
log(f" > Cleared all feedbacks in {label}")
elif action_type == "del_global":
index = int(action.get("index", -1))
if 0 <= index < len(global_feedbacks):
global_feedbacks[index]["to_delete"] = True
dirty_labels.add(label)
log(f" > Deleted global feedback in {label}")
elif action_type in {"del_local", "del_local_rect"}:
index = int(action.get("index", -1))
if 0 <= index < len(local_feedbacks):
target = local_feedbacks[index]
if action_type == "del_local":
target["to_delete"] = True
log(f" > Deleted local feedback in {label}")
else:
target["norectangle"] = True
log(f" > Deleted rectangle in {label}")
dirty_labels.add(label)
return dirty_labels
def apply_score_overrides(
labels_data: dict[str, dict[str, Any]],
score_path: Path,
log: Log,
) -> set[str]:
if not score_path.exists():
return set()
loaded = read_json(score_path)
if not isinstance(loaded, dict):
raise TypeError(f"Expected a JSON object in {score_path}")
dirty: set[str] = set()
for label, score in loaded.items():
if label not in labels_data:
continue
current = str(labels_data[label]["result"].get("score", 0))
if current != str(score):
labels_data[label]["result"]["score"] = score
dirty.add(label)
log(f" > Overrode score for {label} to {score} from score.json")
return dirty
+40
View File
@@ -41,3 +41,43 @@ def staged_directory(destination: str | Path):
_remove_path(staging)
if not committed and backup.exists() and not target.exists():
backup.replace(target)
@contextmanager
def staged_files(destination: str | Path):
"""Stage a set of files and merge them into a directory with rollback."""
target = Path(destination)
target.parent.mkdir(parents=True, exist_ok=True)
token = uuid.uuid4().hex
staging = target.parent / f".{target.name}.{token}.files.tmp"
backup = target.parent / f".{target.name}.{token}.files.backup"
staging.mkdir()
committed: list[Path] = []
try:
yield staging
staged = sorted(path for path in staging.iterdir() if path.is_file())
target.mkdir(parents=True, exist_ok=True)
backup.mkdir()
try:
for source in staged:
destination_path = target / source.name
if destination_path.exists() or destination_path.is_symlink():
destination_path.replace(backup / source.name)
source.replace(destination_path)
committed.append(destination_path)
except Exception:
for destination_path in reversed(committed):
_remove_path(destination_path)
for saved in backup.iterdir():
saved.replace(target / saved.name)
raise
_remove_path(backup)
finally:
if staging.exists():
_remove_path(staging)
if backup.exists():
for saved in backup.iterdir():
destination_path = target / saved.name
if not destination_path.exists():
saved.replace(destination_path)
_remove_path(backup)