EvaluationWorkspace
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class WorkspaceNotFoundError(FileNotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class WorkspaceValidationError(ValueError):
|
||||
def __init__(self, root: Path, missing: list[str]) -> None:
|
||||
self.root = root
|
||||
self.missing = missing
|
||||
super().__init__(
|
||||
f"Invalid evaluation workspace {root}: missing {', '.join(missing)}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EvaluationWorkspace:
|
||||
root: Path
|
||||
repository: Path | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "root", Path(self.root).expanduser().resolve())
|
||||
if self.repository is not None:
|
||||
object.__setattr__(
|
||||
self, "repository", Path(self.repository).expanduser().resolve()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def discover(
|
||||
cls,
|
||||
target: str | Path,
|
||||
*,
|
||||
repository: str | Path | None = None,
|
||||
) -> EvaluationWorkspace:
|
||||
candidate = Path(target).expanduser().resolve()
|
||||
if candidate.is_file():
|
||||
candidate = candidate.parent
|
||||
repository_path = Path(repository).expanduser().resolve() if repository else None
|
||||
|
||||
for directory in (candidate, *candidate.parents):
|
||||
if cls.looks_like_evaluation(directory):
|
||||
return cls(directory, repository_path)
|
||||
if repository_path is not None and directory == repository_path:
|
||||
break
|
||||
raise WorkspaceNotFoundError(
|
||||
f"Could not find an evaluation workspace from: {target}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def looks_like_evaluation(path: Path) -> bool:
|
||||
markers = (
|
||||
"enonce.pdf",
|
||||
"enonce.tex",
|
||||
"labels",
|
||||
"Copies",
|
||||
"correction.json",
|
||||
".copienator",
|
||||
".copienator-gui.json",
|
||||
)
|
||||
return path.is_dir() and any((path / marker).exists() for marker in markers)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.root.name
|
||||
|
||||
@property
|
||||
def metadata_dir(self) -> Path:
|
||||
return self.root / ".copienator"
|
||||
|
||||
@property
|
||||
def logs_dir(self) -> Path:
|
||||
return self.metadata_dir / "logs"
|
||||
|
||||
@property
|
||||
def runs_dir(self) -> Path:
|
||||
return self.metadata_dir / "runs"
|
||||
|
||||
@property
|
||||
def state_database(self) -> Path:
|
||||
return self.metadata_dir / "state.sqlite3"
|
||||
|
||||
@property
|
||||
def gui_state_file(self) -> Path:
|
||||
return self.root / ".copienator-gui.json"
|
||||
|
||||
@property
|
||||
def labels_file(self) -> Path:
|
||||
return self.root / "labels"
|
||||
|
||||
@property
|
||||
def correction_file(self) -> Path:
|
||||
return self.root / "correction.json"
|
||||
|
||||
@property
|
||||
def correction_progress_file(self) -> Path:
|
||||
return self.root / "correction_progress.json"
|
||||
|
||||
@property
|
||||
def manual_resolutions_file(self) -> Path:
|
||||
return self.root / "manual_resolutions.txt"
|
||||
|
||||
@property
|
||||
def refaire_file(self) -> Path:
|
||||
return self.root / "refaire.json"
|
||||
|
||||
@property
|
||||
def copies_dir(self) -> Path:
|
||||
return self.root / "Copies"
|
||||
|
||||
@property
|
||||
def original_copies_dir(self) -> Path:
|
||||
return self.root / "Copies Originales"
|
||||
|
||||
@property
|
||||
def cutleft_dir(self) -> Path:
|
||||
return self.root / "Cutleft"
|
||||
|
||||
@property
|
||||
def groups_dir(self) -> Path:
|
||||
return self.root / "Par label"
|
||||
|
||||
@property
|
||||
def text_dir(self) -> Path:
|
||||
return self.root / "Text"
|
||||
|
||||
@property
|
||||
def solution_dir(self) -> Path:
|
||||
return self.root / "Sol"
|
||||
|
||||
@property
|
||||
def rendered_text_dir(self) -> Path:
|
||||
return self.root / "Text2"
|
||||
|
||||
@property
|
||||
def rendered_solution_dir(self) -> Path:
|
||||
return self.root / "Sol2"
|
||||
|
||||
@property
|
||||
def rubric_dir(self) -> Path:
|
||||
return self.root / "Persp"
|
||||
|
||||
@property
|
||||
def return_dir(self) -> Path:
|
||||
return self.root / "A Rendre"
|
||||
|
||||
def annotation_dir(self, mode: str) -> Path:
|
||||
directories = {
|
||||
"simple": "Anot",
|
||||
"checks": "Bnot",
|
||||
"grouped": "BGnot",
|
||||
"refaire": "BRnot",
|
||||
}
|
||||
try:
|
||||
return self.root / directories[mode]
|
||||
except KeyError as exc:
|
||||
choices = ", ".join(directories)
|
||||
raise ValueError(
|
||||
f"Unknown annotation mode {mode!r}; expected one of: {choices}"
|
||||
) from exc
|
||||
|
||||
def names_file(self) -> Path:
|
||||
local = self.root / "names"
|
||||
if local.exists() or self.repository is None:
|
||||
return local
|
||||
return self.repository / "names"
|
||||
|
||||
def read_labels(self) -> list[str]:
|
||||
return [
|
||||
line.strip()
|
||||
for line in self.labels_file.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
def require(self, *relative_paths: str) -> None:
|
||||
missing = [
|
||||
relative_path
|
||||
for relative_path in relative_paths
|
||||
if not (self.root / relative_path).exists()
|
||||
]
|
||||
if missing:
|
||||
raise WorkspaceValidationError(self.root, missing)
|
||||
|
||||
def ensure_control_directories(self) -> None:
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def command_argument(self) -> str:
|
||||
if self.repository is not None:
|
||||
try:
|
||||
return str(self.root.relative_to(self.repository))
|
||||
except ValueError:
|
||||
pass
|
||||
return str(self.root)
|
||||
|
||||
def log_path(self, step_id: str, when: datetime | None = None) -> Path:
|
||||
timestamp = (when or datetime.now().astimezone()).strftime("%Y%m%d-%H%M%S")
|
||||
safe_step = re.sub(r"[^A-Za-z0-9_.-]+", "_", step_id)
|
||||
return self.logs_dir / f"{timestamp}-{safe_step}.log"
|
||||
Reference in New Issue
Block a user