MicroFish/backend/app/services/interviews/instrument_loader.py

56 lines
1.8 KiB
Python

from __future__ import annotations
import hashlib
import json
from pathlib import Path
import yaml
from pydantic import ValidationError
from app.models.interview import (
LikertInstrument, QSortInstrument,
)
class InstrumentValidationError(ValueError):
pass
def _parse_yaml(path: Path) -> dict:
if not path.exists():
raise InstrumentValidationError(f"instrument file not found: {path}")
try:
with path.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise InstrumentValidationError(f"YAML parse error in {path}: {e}") from e
if not isinstance(data, dict):
raise InstrumentValidationError(f"top-level YAML must be a mapping in {path}")
return data
def load_likert_instrument(path: Path) -> LikertInstrument:
data = _parse_yaml(Path(path))
try:
return LikertInstrument(**data)
except ValidationError as e:
raise InstrumentValidationError(str(e)) from e
def load_qsort_instrument(path: Path) -> QSortInstrument:
data = _parse_yaml(Path(path))
try:
return QSortInstrument(**data)
except ValidationError as e:
raise InstrumentValidationError(str(e)) from e
def instrument_hash(path: Path) -> str:
data = Path(path).read_bytes()
return hashlib.sha256(data).hexdigest()[:16]
def freeze_snapshot(instruments: dict[str, Path], out_path: Path) -> dict:
snapshot = {
name: {
"path": str(p),
"hash": instrument_hash(p),
"content": _parse_yaml(p),
}
for name, p in instruments.items()
}
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8")
return snapshot