Standardisation 4
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user