-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbase.py
More file actions
144 lines (112 loc) · 5.47 KB
/
Copy pathbase.py
File metadata and controls
144 lines (112 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""ComputeBackend protocol and ExecutionMetadata dataclass."""
from __future__ import annotations
import platform
import socket
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol, runtime_checkable
from rdflib import URIRef
class ComputeUnavailable(RuntimeError):
"""Preflight probe detected the compute backend is unreachable / misconfigured.
Raised by `ComputeBackend.probe()`. The runner catches this at startup,
prints the backend's `describe()` output + the cause, and exits
with code 2 (matches WP2's ROBOT fail-fast shape — the integration
story must not silently degrade). `DockerNotAvailable` is a subclass
so existing call sites keep working.
"""
@dataclass(frozen=True)
class ExecutionMetadata:
"""Where and how an analysis stage was executed.
Emitted as PROV triples on the corresponding rtm:SymbolicAnalysis or
rtm:NumericalSimulation activity. Captures the audit-relevant facts:
the host the compute actually ran on, the container image (if any)
that pinned the toolchain, the container ID (for log retrieval),
and a precise timestamp.
URI contract (consumed by evidence.binding):
executor_uri() -> urn:adcs:executor:<suffix>
location_uri() -> urn:adcs:location:<location_kind>:<hostname-or-unknown>
The shapes are byte-identical to pre-WP1 evidence/binding.py
construction; centralizing them here lets WP3 / WP4 reuse the same
IRI shape when adding new evidence types.
"""
location_kind: str # "local" | "docker" | "remote-server"
hostname: str # uname -n inside the runner
image_digest: str = "" # sha256:... of the container image (Docker only)
image_label: str = "" # human-readable tag, e.g. adcs-compute:latest
container_id: str = "" # short docker container ID (Docker only)
python_version: str = "" # platform.python_version()
started_at: str = "" # ISO8601 UTC
ended_at: str = "" # ISO8601 UTC
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def executor_uri(self) -> URIRef:
"""Stable IRI for the prov:SoftwareAgent that ran this stage.
Prefers container_id (Docker runs) and falls back to hostname
(local runs); 'unknown' is the final sentinel. Colons in the
suffix are replaced with dashes so the URN parses cleanly.
"""
suffix = (self.container_id or self.hostname or "unknown").replace(":", "-")
return URIRef(f"urn:adcs:executor:{suffix}")
def location_uri(self) -> URIRef:
"""Stable IRI for the prov:Location where this stage ran."""
host = self.hostname or "unknown"
return URIRef(f"urn:adcs:location:{self.location_kind}:{host}")
def container_uri(self) -> URIRef | None:
"""Stable IRI for the rtm:DockerContainer materialization, or None.
WP4 §4.5 — the per-run container entity sits between the activity
and the image: the activity prov:used the container; the container
prov:wasDerivedFrom the image. Distinct from `location_uri()`
(the host machine) and `executor_uri()` (the agent that ran the work).
Returns None when not a Docker run or when container_id is empty —
in those cases no container entity is emitted.
"""
if self.location_kind != "docker" or not self.container_id:
return None
suffix = self.container_id.replace(":", "-")
return URIRef(f"urn:adcs:docker-container:{suffix}")
@runtime_checkable
class ComputeBackend(Protocol):
"""Pluggable host for analysis stages.
Implementations expose three methods that mirror the existing
analysis.* entry points but additionally return ExecutionMetadata:
run_symbolic_analysis(params) -> (sym_result, metadata)
run_step_response(params) -> (step_result, metadata)
run_disturbance_rejection(params) -> (dist_result, metadata)
"""
name: str
def probe(self) -> None:
"""Preflight reachability check; raise ComputeUnavailable on failure.
Called by the runner before Stage 1 so failure is fast and clear
rather than discovered at Stage 2. LocalCompute is a no-op
(in-process); DockerCompute wraps `_check_daemon`.
"""
...
def describe(self) -> str:
...
def run_symbolic_analysis(self, params: dict) -> tuple[Any, ExecutionMetadata]:
...
def run_step_response(self, params: dict) -> tuple[Any, ExecutionMetadata]:
...
def run_disturbance_rejection(self, params: dict) -> tuple[Any, ExecutionMetadata]:
...
def _local_metadata(stage: str) -> ExecutionMetadata:
"""Build an ExecutionMetadata for an in-process run."""
now = datetime.now(timezone.utc).isoformat()
return ExecutionMetadata(
location_kind="local",
hostname=socket.gethostname(),
python_version=platform.python_version(),
started_at=now,
ended_at=now,
)
def get_compute_backend(name: str, **kwargs) -> ComputeBackend:
"""Factory. Imports backend modules lazily so docker isn't loaded
when --compute=local."""
if name == "local":
from compute.local import LocalCompute
return LocalCompute(**kwargs)
if name == "docker":
from compute.docker_compute import DockerCompute
return DockerCompute(**kwargs)
raise ValueError(f"Unknown compute backend {name!r}. Choose: local | docker")