123 lines
3.5 KiB
Python
123 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
SCDM_RAW_SCHEMA_VERSION = 1
|
|
SCDM_CACHE_SCHEMA_VERSION = 1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScdmProbeJob:
|
|
step_path: Path
|
|
output_dir: Path
|
|
raw_features_path: Path
|
|
error_path: Path
|
|
model_fingerprint: str
|
|
unit: str = "model"
|
|
scan_scope: str = "all"
|
|
adapter: str = "spaceclaim-v1"
|
|
backend_path: str = ""
|
|
backend_version: str = ""
|
|
created_at: str = ""
|
|
|
|
def to_payload(self) -> dict[str, object]:
|
|
return {
|
|
"schemaVersion": SCDM_RAW_SCHEMA_VERSION,
|
|
"adapter": self.adapter,
|
|
"createdAt": self.created_at or utc_now(),
|
|
"backend": {
|
|
"name": "SCDM",
|
|
"path": self.backend_path,
|
|
"version": self.backend_version,
|
|
},
|
|
"model": {
|
|
"path": str(self.step_path),
|
|
"fingerprint": self.model_fingerprint,
|
|
"unit": self.unit,
|
|
},
|
|
"scan": {
|
|
"scope": self.scan_scope,
|
|
},
|
|
"outputs": {
|
|
"rawFeatures": str(self.raw_features_path),
|
|
"error": str(self.error_path),
|
|
},
|
|
}
|
|
|
|
|
|
def utc_now() -> str:
|
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def file_fingerprint(path: str | Path) -> str:
|
|
source = Path(path)
|
|
digest = hashlib.sha256()
|
|
stat = source.stat()
|
|
digest.update(str(source.resolve(strict=False)).encode("utf-8", errors="replace"))
|
|
digest.update(str(stat.st_size).encode("ascii"))
|
|
digest.update(str(stat.st_mtime_ns).encode("ascii"))
|
|
with source.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def read_json(path: str | Path) -> dict[str, object]:
|
|
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
if not isinstance(payload, dict):
|
|
raise ValueError(f"JSON payload must be an object: {path}")
|
|
return payload
|
|
|
|
|
|
def write_json(path: str | Path, payload: Mapping[str, object]) -> Path:
|
|
target = Path(path)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(json.dumps(dict(payload), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
return target
|
|
|
|
|
|
def default_scdm_work_dir(
|
|
step_path: str | Path,
|
|
*,
|
|
project_root: str | Path | None = None,
|
|
fingerprint: str | None = None,
|
|
) -> Path:
|
|
root = Path(project_root).expanduser() if project_root else Path(__file__).resolve().parent.parent
|
|
source = Path(step_path)
|
|
short = (fingerprint or file_fingerprint(source))[:12]
|
|
return root / "local" / "scdm" / f"{source.stem}_{short}"
|
|
|
|
|
|
def payload_model_fingerprint(payload: Mapping[str, object]) -> str:
|
|
model = payload.get("model")
|
|
if not isinstance(model, Mapping):
|
|
return ""
|
|
return str(model.get("fingerprint") or "")
|
|
|
|
|
|
def payload_backend_version(payload: Mapping[str, object]) -> str:
|
|
backend = payload.get("backend")
|
|
if not isinstance(backend, Mapping):
|
|
return ""
|
|
return str(backend.get("version") or "")
|
|
|
|
|
|
__all__ = [
|
|
"SCDM_CACHE_SCHEMA_VERSION",
|
|
"SCDM_RAW_SCHEMA_VERSION",
|
|
"ScdmProbeJob",
|
|
"default_scdm_work_dir",
|
|
"file_fingerprint",
|
|
"payload_backend_version",
|
|
"payload_model_fingerprint",
|
|
"read_json",
|
|
"utc_now",
|
|
"write_json",
|
|
]
|