zeromodel-observation 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zeromodel_observation-1.1.0/PKG-INFO +128 -0
- zeromodel_observation-1.1.0/README.md +102 -0
- zeromodel_observation-1.1.0/pyproject.toml +44 -0
- zeromodel_observation-1.1.0/setup.cfg +4 -0
- zeromodel_observation-1.1.0/src/zeromodel/observation/__init__.py +37 -0
- zeromodel_observation-1.1.0/src/zeromodel/observation/deployment_binding.py +203 -0
- zeromodel_observation-1.1.0/src/zeromodel/observation/visual_address.py +401 -0
- zeromodel_observation-1.1.0/src/zeromodel/observation/visual_address_manifest.py +169 -0
- zeromodel_observation-1.1.0/src/zeromodel_observation.egg-info/PKG-INFO +128 -0
- zeromodel_observation-1.1.0/src/zeromodel_observation.egg-info/SOURCES.txt +14 -0
- zeromodel_observation-1.1.0/src/zeromodel_observation.egg-info/dependency_links.txt +1 -0
- zeromodel_observation-1.1.0/src/zeromodel_observation.egg-info/requires.txt +2 -0
- zeromodel_observation-1.1.0/src/zeromodel_observation.egg-info/top_level.txt +1 -0
- zeromodel_observation-1.1.0/tests/test_deployment_binding.py +86 -0
- zeromodel_observation-1.1.0/tests/test_observation_api_isolation.py +58 -0
- zeromodel_observation-1.1.0/tests/test_visual_address_contracts.py +308 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zeromodel-observation
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: ZeroModel observation package
|
|
5
|
+
Author-email: Ernan Hughes <ernanhughes@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ernanhughes/zeromodel
|
|
8
|
+
Project-URL: Repository, https://github.com/ernanhughes/zeromodel
|
|
9
|
+
Project-URL: Documentation, https://ernanhughes.github.io/zeromodel/
|
|
10
|
+
Keywords: visual-policy-map,vpm,observation-contracts,provider-protocols
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: numpy>=1.23
|
|
25
|
+
Requires-Dist: zeromodel==1.1.0
|
|
26
|
+
|
|
27
|
+
# zeromodel-observation
|
|
28
|
+
|
|
29
|
+
`zeromodel-observation` defines provider-neutral observation contracts for
|
|
30
|
+
ZeroModel. It is the boundary between raw observations and deterministic policy
|
|
31
|
+
addressing, exposed through `zeromodel.observation`.
|
|
32
|
+
|
|
33
|
+
The package depends on the validated core distribution, `zeromodel==1.0.13`, and
|
|
34
|
+
uses NumPy for immutable image-array contracts. It does not implement perception,
|
|
35
|
+
visual recognition, video policy behavior, model inference, or persistence.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
python -m pip install zeromodel==1.0.13 zeromodel-observation==1.0.13
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Provider-Neutral Contracts
|
|
44
|
+
|
|
45
|
+
`ImageObservation` stores an owned, read-only `uint8` image array plus source,
|
|
46
|
+
timestamp, and metadata. It accepts `HxW`, `HxWx3`, and `HxWx4` arrays and
|
|
47
|
+
derives a deterministic raw digest from the canonical shape and bytes.
|
|
48
|
+
|
|
49
|
+
`VisualAddressContract` describes what a provider promises before inference:
|
|
50
|
+
provider identity, score polarity, replay semantics, representation identity,
|
|
51
|
+
calibration identity, policy identity, and source scope.
|
|
52
|
+
|
|
53
|
+
`VisualAddressDecision` records an accepted address or evidence-bearing
|
|
54
|
+
rejection. It links the observation, representation, provider, address artifact,
|
|
55
|
+
calibration artifact, and policy artifact without depending on any concrete
|
|
56
|
+
vision implementation.
|
|
57
|
+
|
|
58
|
+
`VisualAddressManifest` and `PrototypeBinding` bind representation rows to
|
|
59
|
+
policy row IDs and link those rows to a content-addressed matrix blob. The
|
|
60
|
+
manifest is an address contract artifact, not a visual reader.
|
|
61
|
+
|
|
62
|
+
`DeploymentBinding` records an approved pairing of policy, address artifact,
|
|
63
|
+
calibration, source scope, and optional encoder identity. It is not a
|
|
64
|
+
cryptographic signature or authorization system.
|
|
65
|
+
|
|
66
|
+
## Minimal Provider
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import numpy as np
|
|
70
|
+
|
|
71
|
+
from zeromodel.observation import (
|
|
72
|
+
ImageObservation,
|
|
73
|
+
VisualAddressContract,
|
|
74
|
+
VisualAddressDecision,
|
|
75
|
+
VisualAddressProvider,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TableProvider:
|
|
80
|
+
def contract(self) -> VisualAddressContract:
|
|
81
|
+
return VisualAddressContract(
|
|
82
|
+
provider_kind="table-provider",
|
|
83
|
+
provider_version="v1",
|
|
84
|
+
score_semantics="distance",
|
|
85
|
+
observation_spec_digest="obs-spec",
|
|
86
|
+
representation_spec_digest="rep-spec",
|
|
87
|
+
address_artifact_id="address-1",
|
|
88
|
+
calibration_artifact_id="calibration-1",
|
|
89
|
+
policy_artifact_id="policy-1",
|
|
90
|
+
source_scope="fixture:camera",
|
|
91
|
+
replay_contract="exact_decision",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def read(self, observation: ImageObservation) -> VisualAddressDecision:
|
|
95
|
+
return VisualAddressDecision(
|
|
96
|
+
accepted=True,
|
|
97
|
+
reason="accepted",
|
|
98
|
+
observation_digest=observation.raw_digest,
|
|
99
|
+
representation_digest="sha256:representation",
|
|
100
|
+
provider_kind="table-provider",
|
|
101
|
+
provider_version="v1",
|
|
102
|
+
score_semantics="distance",
|
|
103
|
+
address_artifact_id="address-1",
|
|
104
|
+
calibration_artifact_id="calibration-1",
|
|
105
|
+
policy_artifact_id="policy-1",
|
|
106
|
+
nearest_row_id="policy-row-1",
|
|
107
|
+
nearest_score=0.0,
|
|
108
|
+
second_row_id=None,
|
|
109
|
+
second_score=None,
|
|
110
|
+
ambiguity_measure=None,
|
|
111
|
+
matched_row_id="policy-row-1",
|
|
112
|
+
exact_match=True,
|
|
113
|
+
accepted_by=("fixture",),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
provider: VisualAddressProvider = TableProvider()
|
|
118
|
+
decision = provider.read(ImageObservation(np.zeros((2, 2), dtype=np.uint8)))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Exclusions
|
|
122
|
+
|
|
123
|
+
Concrete deterministic visual addressing belongs to `zeromodel-vision`.
|
|
124
|
+
Temporal policy and video-domain behavior belong to `zeromodel-video`.
|
|
125
|
+
Durable SQL persistence belongs to `zeromodel-sqlalchemy`.
|
|
126
|
+
|
|
127
|
+
This package does not import or require `zeromodel-analysis`, `zeromodel-vision`,
|
|
128
|
+
`zeromodel-video`, SQLAlchemy, Torch, TorchVision, Transformers, or Pillow.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# zeromodel-observation
|
|
2
|
+
|
|
3
|
+
`zeromodel-observation` defines provider-neutral observation contracts for
|
|
4
|
+
ZeroModel. It is the boundary between raw observations and deterministic policy
|
|
5
|
+
addressing, exposed through `zeromodel.observation`.
|
|
6
|
+
|
|
7
|
+
The package depends on the validated core distribution, `zeromodel==1.0.13`, and
|
|
8
|
+
uses NumPy for immutable image-array contracts. It does not implement perception,
|
|
9
|
+
visual recognition, video policy behavior, model inference, or persistence.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
python -m pip install zeromodel==1.0.13 zeromodel-observation==1.0.13
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Provider-Neutral Contracts
|
|
18
|
+
|
|
19
|
+
`ImageObservation` stores an owned, read-only `uint8` image array plus source,
|
|
20
|
+
timestamp, and metadata. It accepts `HxW`, `HxWx3`, and `HxWx4` arrays and
|
|
21
|
+
derives a deterministic raw digest from the canonical shape and bytes.
|
|
22
|
+
|
|
23
|
+
`VisualAddressContract` describes what a provider promises before inference:
|
|
24
|
+
provider identity, score polarity, replay semantics, representation identity,
|
|
25
|
+
calibration identity, policy identity, and source scope.
|
|
26
|
+
|
|
27
|
+
`VisualAddressDecision` records an accepted address or evidence-bearing
|
|
28
|
+
rejection. It links the observation, representation, provider, address artifact,
|
|
29
|
+
calibration artifact, and policy artifact without depending on any concrete
|
|
30
|
+
vision implementation.
|
|
31
|
+
|
|
32
|
+
`VisualAddressManifest` and `PrototypeBinding` bind representation rows to
|
|
33
|
+
policy row IDs and link those rows to a content-addressed matrix blob. The
|
|
34
|
+
manifest is an address contract artifact, not a visual reader.
|
|
35
|
+
|
|
36
|
+
`DeploymentBinding` records an approved pairing of policy, address artifact,
|
|
37
|
+
calibration, source scope, and optional encoder identity. It is not a
|
|
38
|
+
cryptographic signature or authorization system.
|
|
39
|
+
|
|
40
|
+
## Minimal Provider
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import numpy as np
|
|
44
|
+
|
|
45
|
+
from zeromodel.observation import (
|
|
46
|
+
ImageObservation,
|
|
47
|
+
VisualAddressContract,
|
|
48
|
+
VisualAddressDecision,
|
|
49
|
+
VisualAddressProvider,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TableProvider:
|
|
54
|
+
def contract(self) -> VisualAddressContract:
|
|
55
|
+
return VisualAddressContract(
|
|
56
|
+
provider_kind="table-provider",
|
|
57
|
+
provider_version="v1",
|
|
58
|
+
score_semantics="distance",
|
|
59
|
+
observation_spec_digest="obs-spec",
|
|
60
|
+
representation_spec_digest="rep-spec",
|
|
61
|
+
address_artifact_id="address-1",
|
|
62
|
+
calibration_artifact_id="calibration-1",
|
|
63
|
+
policy_artifact_id="policy-1",
|
|
64
|
+
source_scope="fixture:camera",
|
|
65
|
+
replay_contract="exact_decision",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def read(self, observation: ImageObservation) -> VisualAddressDecision:
|
|
69
|
+
return VisualAddressDecision(
|
|
70
|
+
accepted=True,
|
|
71
|
+
reason="accepted",
|
|
72
|
+
observation_digest=observation.raw_digest,
|
|
73
|
+
representation_digest="sha256:representation",
|
|
74
|
+
provider_kind="table-provider",
|
|
75
|
+
provider_version="v1",
|
|
76
|
+
score_semantics="distance",
|
|
77
|
+
address_artifact_id="address-1",
|
|
78
|
+
calibration_artifact_id="calibration-1",
|
|
79
|
+
policy_artifact_id="policy-1",
|
|
80
|
+
nearest_row_id="policy-row-1",
|
|
81
|
+
nearest_score=0.0,
|
|
82
|
+
second_row_id=None,
|
|
83
|
+
second_score=None,
|
|
84
|
+
ambiguity_measure=None,
|
|
85
|
+
matched_row_id="policy-row-1",
|
|
86
|
+
exact_match=True,
|
|
87
|
+
accepted_by=("fixture",),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
provider: VisualAddressProvider = TableProvider()
|
|
92
|
+
decision = provider.read(ImageObservation(np.zeros((2, 2), dtype=np.uint8)))
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Exclusions
|
|
96
|
+
|
|
97
|
+
Concrete deterministic visual addressing belongs to `zeromodel-vision`.
|
|
98
|
+
Temporal policy and video-domain behavior belong to `zeromodel-video`.
|
|
99
|
+
Durable SQL persistence belongs to `zeromodel-sqlalchemy`.
|
|
100
|
+
|
|
101
|
+
This package does not import or require `zeromodel-analysis`, `zeromodel-vision`,
|
|
102
|
+
`zeromodel-video`, SQLAlchemy, Torch, TorchVision, Transformers, or Pillow.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zeromodel-observation"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "ZeroModel observation package"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Ernan Hughes", email = "ernanhughes@gmail.com"}]
|
|
13
|
+
keywords = [
|
|
14
|
+
"visual-policy-map",
|
|
15
|
+
"vpm",
|
|
16
|
+
"observation-contracts",
|
|
17
|
+
"provider-protocols",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"numpy>=1.23",
|
|
34
|
+
"zeromodel==1.1.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/ernanhughes/zeromodel"
|
|
39
|
+
Repository = "https://github.com/ernanhughes/zeromodel"
|
|
40
|
+
Documentation = "https://ernanhughes.github.io/zeromodel/"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
include = ["zeromodel.observation*"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""ZeroModel observation public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .deployment_binding import (
|
|
6
|
+
DEPLOYMENT_BINDING_VERSION,
|
|
7
|
+
DeploymentBinding,
|
|
8
|
+
)
|
|
9
|
+
from .visual_address import (
|
|
10
|
+
IMAGE_OBSERVATION_VERSION,
|
|
11
|
+
ImageObservation,
|
|
12
|
+
VISUAL_ADDRESS_CONTRACT_VERSION,
|
|
13
|
+
VISUAL_ADDRESS_DECISION_VERSION,
|
|
14
|
+
VISUAL_ADDRESS_MANIFEST_VERSION,
|
|
15
|
+
VisualAddressContract,
|
|
16
|
+
VisualAddressDecision,
|
|
17
|
+
VisualAddressProvider,
|
|
18
|
+
)
|
|
19
|
+
from .visual_address_manifest import (
|
|
20
|
+
PrototypeBinding,
|
|
21
|
+
VisualAddressManifest,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEPLOYMENT_BINDING_VERSION",
|
|
26
|
+
"DeploymentBinding",
|
|
27
|
+
"IMAGE_OBSERVATION_VERSION",
|
|
28
|
+
"ImageObservation",
|
|
29
|
+
"PrototypeBinding",
|
|
30
|
+
"VISUAL_ADDRESS_CONTRACT_VERSION",
|
|
31
|
+
"VISUAL_ADDRESS_DECISION_VERSION",
|
|
32
|
+
"VISUAL_ADDRESS_MANIFEST_VERSION",
|
|
33
|
+
"VisualAddressContract",
|
|
34
|
+
"VisualAddressDecision",
|
|
35
|
+
"VisualAddressManifest",
|
|
36
|
+
"VisualAddressProvider",
|
|
37
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Approved binding between a visual address contract and a VPM policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, Mapping, Optional
|
|
9
|
+
|
|
10
|
+
from zeromodel.core.artifact import VPMValidationError
|
|
11
|
+
from zeromodel.observation.visual_address import VisualAddressContract
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEPLOYMENT_BINDING_VERSION = "zeromodel-deployment-binding/v1"
|
|
15
|
+
_DEPLOYMENT_STATUSES = {"validated", "research"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _canonical_json_bytes(value: Any) -> bytes:
|
|
19
|
+
try:
|
|
20
|
+
return json.dumps(
|
|
21
|
+
value,
|
|
22
|
+
sort_keys=True,
|
|
23
|
+
separators=(",", ":"),
|
|
24
|
+
ensure_ascii=False,
|
|
25
|
+
allow_nan=False,
|
|
26
|
+
).encode("utf-8")
|
|
27
|
+
except (TypeError, ValueError) as exc:
|
|
28
|
+
raise VPMValidationError(
|
|
29
|
+
"deployment binding must be JSON-serializable"
|
|
30
|
+
) from exc
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class DeploymentBinding:
|
|
35
|
+
"""Identity-bearing approval of one perception-policy pairing.
|
|
36
|
+
|
|
37
|
+
A binding is not a cryptographic signature. It records exactly which
|
|
38
|
+
address, calibration, encoder, policy, and source scope were approved
|
|
39
|
+
together so the runtime can reject accidental cross-pairing.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
policy_artifact_id: str
|
|
43
|
+
address_artifact_id: str
|
|
44
|
+
calibration_artifact_id: str
|
|
45
|
+
source_scope: str
|
|
46
|
+
encoder_manifest_id: Optional[str] = None
|
|
47
|
+
deployment_status: str = "validated"
|
|
48
|
+
version: str = DEPLOYMENT_BINDING_VERSION
|
|
49
|
+
binding_id: str = ""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
policy_artifact_id: str,
|
|
55
|
+
address_artifact_id: str,
|
|
56
|
+
calibration_artifact_id: str,
|
|
57
|
+
source_scope: str,
|
|
58
|
+
encoder_manifest_id: Optional[str] = None,
|
|
59
|
+
deployment_status: str = "validated",
|
|
60
|
+
version: str = DEPLOYMENT_BINDING_VERSION,
|
|
61
|
+
binding_id: Optional[str] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
object.__setattr__(self, "policy_artifact_id", str(policy_artifact_id))
|
|
64
|
+
object.__setattr__(self, "address_artifact_id", str(address_artifact_id))
|
|
65
|
+
object.__setattr__(
|
|
66
|
+
self,
|
|
67
|
+
"calibration_artifact_id",
|
|
68
|
+
str(calibration_artifact_id),
|
|
69
|
+
)
|
|
70
|
+
object.__setattr__(self, "source_scope", str(source_scope))
|
|
71
|
+
object.__setattr__(
|
|
72
|
+
self,
|
|
73
|
+
"encoder_manifest_id",
|
|
74
|
+
None if encoder_manifest_id is None else str(encoder_manifest_id),
|
|
75
|
+
)
|
|
76
|
+
object.__setattr__(self, "deployment_status", str(deployment_status))
|
|
77
|
+
object.__setattr__(self, "version", str(version))
|
|
78
|
+
computed = self.compute_binding_id()
|
|
79
|
+
object.__setattr__(self, "binding_id", str(binding_id or computed))
|
|
80
|
+
self.validate()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def deployment_permitted(self) -> bool:
|
|
84
|
+
return self.deployment_status == "validated"
|
|
85
|
+
|
|
86
|
+
def validate(self) -> None:
|
|
87
|
+
if self.version != DEPLOYMENT_BINDING_VERSION:
|
|
88
|
+
raise VPMValidationError(
|
|
89
|
+
"unsupported deployment binding version: %r" % self.version
|
|
90
|
+
)
|
|
91
|
+
for name, value in (
|
|
92
|
+
("policy_artifact_id", self.policy_artifact_id),
|
|
93
|
+
("address_artifact_id", self.address_artifact_id),
|
|
94
|
+
("calibration_artifact_id", self.calibration_artifact_id),
|
|
95
|
+
("source_scope", self.source_scope),
|
|
96
|
+
):
|
|
97
|
+
if not value:
|
|
98
|
+
raise VPMValidationError("%s cannot be empty" % name)
|
|
99
|
+
if self.deployment_status not in _DEPLOYMENT_STATUSES:
|
|
100
|
+
raise VPMValidationError(
|
|
101
|
+
"deployment_status must be 'validated' or 'research'"
|
|
102
|
+
)
|
|
103
|
+
expected = self.compute_binding_id()
|
|
104
|
+
if self.binding_id != expected:
|
|
105
|
+
raise VPMValidationError(
|
|
106
|
+
"deployment binding id mismatch: expected %s, got %s"
|
|
107
|
+
% (expected, self.binding_id)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def identity_payload(self) -> Dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"version": self.version,
|
|
113
|
+
"policy_artifact_id": self.policy_artifact_id,
|
|
114
|
+
"address_artifact_id": self.address_artifact_id,
|
|
115
|
+
"calibration_artifact_id": self.calibration_artifact_id,
|
|
116
|
+
"encoder_manifest_id": self.encoder_manifest_id,
|
|
117
|
+
"source_scope": self.source_scope,
|
|
118
|
+
"deployment_status": self.deployment_status,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def compute_binding_id(self) -> str:
|
|
122
|
+
return hashlib.sha256(
|
|
123
|
+
b"zeromodel.deployment-binding.identity.v1\0"
|
|
124
|
+
+ _canonical_json_bytes(self.identity_payload())
|
|
125
|
+
).hexdigest()
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
128
|
+
payload = self.identity_payload()
|
|
129
|
+
payload["binding_id"] = self.binding_id
|
|
130
|
+
payload["deployment_permitted"] = self.deployment_permitted
|
|
131
|
+
return payload
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "DeploymentBinding":
|
|
135
|
+
return cls(
|
|
136
|
+
policy_artifact_id=str(data["policy_artifact_id"]),
|
|
137
|
+
address_artifact_id=str(data["address_artifact_id"]),
|
|
138
|
+
calibration_artifact_id=str(data["calibration_artifact_id"]),
|
|
139
|
+
encoder_manifest_id=data.get("encoder_manifest_id"),
|
|
140
|
+
source_scope=str(data["source_scope"]),
|
|
141
|
+
deployment_status=str(data.get("deployment_status", "validated")),
|
|
142
|
+
version=str(data.get("version", DEPLOYMENT_BINDING_VERSION)),
|
|
143
|
+
binding_id=data.get("binding_id"),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_contract(
|
|
148
|
+
cls,
|
|
149
|
+
contract: VisualAddressContract,
|
|
150
|
+
*,
|
|
151
|
+
deployment_status: str = "validated",
|
|
152
|
+
encoder_manifest_id: Optional[str] = None,
|
|
153
|
+
) -> "DeploymentBinding":
|
|
154
|
+
if contract.source_scope is None or not contract.source_scope:
|
|
155
|
+
raise VPMValidationError(
|
|
156
|
+
"deployment binding requires contract source_scope"
|
|
157
|
+
)
|
|
158
|
+
return cls(
|
|
159
|
+
policy_artifact_id=contract.policy_artifact_id,
|
|
160
|
+
address_artifact_id=contract.address_artifact_id,
|
|
161
|
+
calibration_artifact_id=contract.calibration_artifact_id,
|
|
162
|
+
encoder_manifest_id=encoder_manifest_id,
|
|
163
|
+
source_scope=contract.source_scope,
|
|
164
|
+
deployment_status=deployment_status,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def verify_contract(
|
|
168
|
+
self,
|
|
169
|
+
contract: VisualAddressContract,
|
|
170
|
+
*,
|
|
171
|
+
allow_research: bool = False,
|
|
172
|
+
) -> None:
|
|
173
|
+
contract.validate()
|
|
174
|
+
mismatches = []
|
|
175
|
+
for name, expected, actual in (
|
|
176
|
+
(
|
|
177
|
+
"policy_artifact_id",
|
|
178
|
+
self.policy_artifact_id,
|
|
179
|
+
contract.policy_artifact_id,
|
|
180
|
+
),
|
|
181
|
+
(
|
|
182
|
+
"address_artifact_id",
|
|
183
|
+
self.address_artifact_id,
|
|
184
|
+
contract.address_artifact_id,
|
|
185
|
+
),
|
|
186
|
+
(
|
|
187
|
+
"calibration_artifact_id",
|
|
188
|
+
self.calibration_artifact_id,
|
|
189
|
+
contract.calibration_artifact_id,
|
|
190
|
+
),
|
|
191
|
+
("source_scope", self.source_scope, contract.source_scope),
|
|
192
|
+
):
|
|
193
|
+
if expected != actual:
|
|
194
|
+
mismatches.append("%s=%r (expected %r)" % (name, actual, expected))
|
|
195
|
+
if mismatches:
|
|
196
|
+
raise VPMValidationError(
|
|
197
|
+
"deployment binding does not match visual address contract: %s"
|
|
198
|
+
% "; ".join(mismatches)
|
|
199
|
+
)
|
|
200
|
+
if not self.deployment_permitted and not allow_research:
|
|
201
|
+
raise VPMValidationError(
|
|
202
|
+
"research deployment binding is not permitted in validated runtime"
|
|
203
|
+
)
|