validibot-shared 0.12.0__tar.gz → 0.13.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.
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/PKG-INFO +45 -5
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/README.md +43 -3
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/pyproject.toml +2 -2
- validibot_shared-0.13.0/tests/test_artifacts.py +101 -0
- validibot_shared-0.13.0/tests/test_canonicalization.py +100 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_validations_envelopes.py +27 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/__init__.py +10 -0
- validibot_shared-0.13.0/validibot_shared/_vendor/__init__.py +1 -0
- validibot_shared-0.13.0/validibot_shared/_vendor/jcs.py +191 -0
- validibot_shared-0.13.0/validibot_shared/canonicalization.py +57 -0
- validibot_shared-0.13.0/validibot_shared/validations/artifacts.py +155 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/validations/envelopes.py +16 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/.gitignore +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/LICENSE +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/NOTICE +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_energyplus_envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_energyplus_models.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_evidence_manifest.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_fmu_envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_fmu_models.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_package_init.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_schematron_envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_schematron_svrl.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/tests/test_shacl_envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/energyplus/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/energyplus/envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/energyplus/models.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/evidence/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/evidence/manifest.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/fmu/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/fmu/envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/fmu/models.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/py.typed +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/schematron/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/schematron/envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/schematron/svrl.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/shacl/__init__.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/shacl/envelopes.py +0 -0
- {validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/validations/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: validibot-shared
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Shared library for data interchange between Validibot and validator containers
|
|
5
5
|
Project-URL: Homepage, https://validibot.com
|
|
6
6
|
Project-URL: Documentation, https://docs.validibot.com
|
|
@@ -28,7 +28,7 @@ Requires-Dist: defusedxml<0.8,>=0.7.1
|
|
|
28
28
|
Requires-Dist: pydantic<3.0,>=2.13
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
31
|
-
Requires-Dist: ruff==0.15.
|
|
31
|
+
Requires-Dist: ruff==0.15.21; extra == 'dev'
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
34
34
|
<div align="center">
|
|
@@ -167,11 +167,46 @@ Supporting models include:
|
|
|
167
167
|
| Class | Purpose |
|
|
168
168
|
|-------|---------|
|
|
169
169
|
| `InputFileItem` | File reference with URI, MIME type, and role |
|
|
170
|
+
| `ResourceFileItem` | Managed auxiliary file reference with URI and resource type |
|
|
170
171
|
| `ValidatorInfo` | Validator identification (ID, type, version) |
|
|
171
172
|
| `ExecutionContext` | Callback URL, execution bundle URI, timeout |
|
|
172
173
|
| `ValidationMessage` | Individual finding (error, warning, info) |
|
|
173
174
|
| `ValidationMetric` | Named numeric metric with optional unit |
|
|
174
175
|
| `ValidationArtifact` | Output file reference (reports, logs, etc.) |
|
|
176
|
+
| `ArtifactRef` | Indexed run artifact reference for workflow/evidence control planes |
|
|
177
|
+
| `FilePortContract` | Shared file-port vocabulary for declared validator file inputs/outputs |
|
|
178
|
+
|
|
179
|
+
### Designing File Inputs
|
|
180
|
+
|
|
181
|
+
The shared envelope exposes file inputs as `input_files` and `resource_files`.
|
|
182
|
+
Validator authors should still design those files as **declared ports** in the
|
|
183
|
+
core platform.
|
|
184
|
+
|
|
185
|
+
A file port answers:
|
|
186
|
+
|
|
187
|
+
- what the file means to the validator (`primary_model`, `weather_file`,
|
|
188
|
+
`data_graph`, `xml_document`, `schema_file`, `fmu_model`);
|
|
189
|
+
- how many files are valid (`1..1`, `0..1`, or a future collection);
|
|
190
|
+
- which envelope channel it renders into (`input_files` or `resource_files`);
|
|
191
|
+
- which backend role/type it uses (`primary-model`, `weather`, `fmu`,
|
|
192
|
+
`data-graph`);
|
|
193
|
+
- which formats and MIME types are accepted;
|
|
194
|
+
- whether the source may be a submitted file, workflow resource, upstream
|
|
195
|
+
artifact, or signal containing an artifact reference.
|
|
196
|
+
|
|
197
|
+
Keep small configuration in typed `inputs`. Use file/resource/artifact ports
|
|
198
|
+
for bytes. For example, EnergyPlus timestep settings belong in
|
|
199
|
+
`EnergyPlusInputs`; the IDF/epJSON model and EPW weather file belong in file
|
|
200
|
+
ports rendered to `input_files` / `resource_files`.
|
|
201
|
+
|
|
202
|
+
Backends should read files by role, and by future optional `port_key` when
|
|
203
|
+
available, not by assuming `input_files[0]` forever.
|
|
204
|
+
|
|
205
|
+
`ArtifactRef` and `FilePortContract` live in
|
|
206
|
+
`validibot_shared.validations.artifacts`. `InputFileItem` and
|
|
207
|
+
`ResourceFileItem` also accept an optional `port_key` so a backend or evidence
|
|
208
|
+
builder can correlate an envelope item back to the declared Validibot port
|
|
209
|
+
without relying only on backend role/type strings.
|
|
175
210
|
|
|
176
211
|
### Typed Subclassing Pattern
|
|
177
212
|
|
|
@@ -204,9 +239,14 @@ validibot_shared/
|
|
|
204
239
|
├── energyplus/ # EnergyPlus-specific models and envelopes
|
|
205
240
|
│ ├── models.py # Simulation output models (metrics, results)
|
|
206
241
|
│ └── envelopes.py # Typed envelope subclasses
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
242
|
+
├── fmu/ # FMU-specific models
|
|
243
|
+
│ ├── models.py # Probe/simulation result models
|
|
244
|
+
│ └── envelopes.py # FMU envelope subclasses
|
|
245
|
+
├── shacl/ # SHACL isolated-backend envelopes
|
|
246
|
+
│ └── envelopes.py # SHACL inputs/outputs and builder
|
|
247
|
+
└── schematron/ # Schematron isolated-backend envelopes
|
|
248
|
+
├── envelopes.py # Schematron inputs/outputs and builder
|
|
249
|
+
└── svrl.py # SVRL parsing helpers
|
|
210
250
|
```
|
|
211
251
|
|
|
212
252
|
## Usage Examples
|
|
@@ -134,11 +134,46 @@ Supporting models include:
|
|
|
134
134
|
| Class | Purpose |
|
|
135
135
|
|-------|---------|
|
|
136
136
|
| `InputFileItem` | File reference with URI, MIME type, and role |
|
|
137
|
+
| `ResourceFileItem` | Managed auxiliary file reference with URI and resource type |
|
|
137
138
|
| `ValidatorInfo` | Validator identification (ID, type, version) |
|
|
138
139
|
| `ExecutionContext` | Callback URL, execution bundle URI, timeout |
|
|
139
140
|
| `ValidationMessage` | Individual finding (error, warning, info) |
|
|
140
141
|
| `ValidationMetric` | Named numeric metric with optional unit |
|
|
141
142
|
| `ValidationArtifact` | Output file reference (reports, logs, etc.) |
|
|
143
|
+
| `ArtifactRef` | Indexed run artifact reference for workflow/evidence control planes |
|
|
144
|
+
| `FilePortContract` | Shared file-port vocabulary for declared validator file inputs/outputs |
|
|
145
|
+
|
|
146
|
+
### Designing File Inputs
|
|
147
|
+
|
|
148
|
+
The shared envelope exposes file inputs as `input_files` and `resource_files`.
|
|
149
|
+
Validator authors should still design those files as **declared ports** in the
|
|
150
|
+
core platform.
|
|
151
|
+
|
|
152
|
+
A file port answers:
|
|
153
|
+
|
|
154
|
+
- what the file means to the validator (`primary_model`, `weather_file`,
|
|
155
|
+
`data_graph`, `xml_document`, `schema_file`, `fmu_model`);
|
|
156
|
+
- how many files are valid (`1..1`, `0..1`, or a future collection);
|
|
157
|
+
- which envelope channel it renders into (`input_files` or `resource_files`);
|
|
158
|
+
- which backend role/type it uses (`primary-model`, `weather`, `fmu`,
|
|
159
|
+
`data-graph`);
|
|
160
|
+
- which formats and MIME types are accepted;
|
|
161
|
+
- whether the source may be a submitted file, workflow resource, upstream
|
|
162
|
+
artifact, or signal containing an artifact reference.
|
|
163
|
+
|
|
164
|
+
Keep small configuration in typed `inputs`. Use file/resource/artifact ports
|
|
165
|
+
for bytes. For example, EnergyPlus timestep settings belong in
|
|
166
|
+
`EnergyPlusInputs`; the IDF/epJSON model and EPW weather file belong in file
|
|
167
|
+
ports rendered to `input_files` / `resource_files`.
|
|
168
|
+
|
|
169
|
+
Backends should read files by role, and by future optional `port_key` when
|
|
170
|
+
available, not by assuming `input_files[0]` forever.
|
|
171
|
+
|
|
172
|
+
`ArtifactRef` and `FilePortContract` live in
|
|
173
|
+
`validibot_shared.validations.artifacts`. `InputFileItem` and
|
|
174
|
+
`ResourceFileItem` also accept an optional `port_key` so a backend or evidence
|
|
175
|
+
builder can correlate an envelope item back to the declared Validibot port
|
|
176
|
+
without relying only on backend role/type strings.
|
|
142
177
|
|
|
143
178
|
### Typed Subclassing Pattern
|
|
144
179
|
|
|
@@ -171,9 +206,14 @@ validibot_shared/
|
|
|
171
206
|
├── energyplus/ # EnergyPlus-specific models and envelopes
|
|
172
207
|
│ ├── models.py # Simulation output models (metrics, results)
|
|
173
208
|
│ └── envelopes.py # Typed envelope subclasses
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
├── fmu/ # FMU-specific models
|
|
210
|
+
│ ├── models.py # Probe/simulation result models
|
|
211
|
+
│ └── envelopes.py # FMU envelope subclasses
|
|
212
|
+
├── shacl/ # SHACL isolated-backend envelopes
|
|
213
|
+
│ └── envelopes.py # SHACL inputs/outputs and builder
|
|
214
|
+
└── schematron/ # Schematron isolated-backend envelopes
|
|
215
|
+
├── envelopes.py # Schematron inputs/outputs and builder
|
|
216
|
+
└── svrl.py # SVRL parsing helpers
|
|
177
217
|
```
|
|
178
218
|
|
|
179
219
|
## Usage Examples
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "validibot-shared"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.0"
|
|
8
8
|
description = "Shared library for data interchange between Validibot and validator containers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -37,7 +37,7 @@ classifiers = [
|
|
|
37
37
|
dependencies = ["pydantic>=2.13,<3.0", "defusedxml>=0.7.1,<0.8"]
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
|
-
dev = ["pytest==9.1.1", "ruff==0.15.
|
|
40
|
+
dev = ["pytest==9.1.1", "ruff==0.15.21"]
|
|
41
41
|
|
|
42
42
|
[project.urls]
|
|
43
43
|
Homepage = "https://validibot.com"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Tests for shared artifact references and file-port contracts.
|
|
2
|
+
|
|
3
|
+
These schemas form the cross-repo contract between Django, validator
|
|
4
|
+
backends, evidence builders, and future UI/API projections. The tests focus
|
|
5
|
+
on JSON shape stability and enum serialization because those are the pieces
|
|
6
|
+
that must remain portable across repos.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from validibot_shared.validations.artifacts import (
|
|
13
|
+
ArtifactKind,
|
|
14
|
+
ArtifactRef,
|
|
15
|
+
BindingSourceScope,
|
|
16
|
+
DefaultSourceStrategy,
|
|
17
|
+
EnvelopeChannel,
|
|
18
|
+
FilePortContract,
|
|
19
|
+
StepIODirection,
|
|
20
|
+
StepIOMedium,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_artifact_ref_serializes_as_json_safe_control_plane_object():
|
|
25
|
+
"""ArtifactRef should carry lineage metadata without embedding bytes."""
|
|
26
|
+
ref = ArtifactRef(
|
|
27
|
+
artifact_id="artifact-1",
|
|
28
|
+
run_id="run-1",
|
|
29
|
+
step_run_id="step-run-1",
|
|
30
|
+
producer_step_key="build_model",
|
|
31
|
+
contract_key="generated_model",
|
|
32
|
+
name="model.epjson",
|
|
33
|
+
role="primary-model",
|
|
34
|
+
kind=ArtifactKind.FILE,
|
|
35
|
+
media_type="application/vnd.energyplus.epjson",
|
|
36
|
+
data_format="energyplus_epjson",
|
|
37
|
+
filename="model.epjson",
|
|
38
|
+
size_bytes=1234,
|
|
39
|
+
sha256="a" * 64,
|
|
40
|
+
uri="gs://bucket/runs/run-1/model.epjson",
|
|
41
|
+
producer_validator_type="BUILDINGSYNC_TO_ENERGYPLUS",
|
|
42
|
+
producer_validator_version="1",
|
|
43
|
+
producer_backend_image_digest="repo/image@sha256:" + "b" * 64,
|
|
44
|
+
retention_class="standard",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
dumped = ref.model_dump(mode="json")
|
|
48
|
+
|
|
49
|
+
assert dumped["schema_version"] == "validibot.artifact_ref.v1"
|
|
50
|
+
assert dumped["kind"] == "file"
|
|
51
|
+
assert dumped["producer_step_key"] == "build_model"
|
|
52
|
+
assert dumped["uri"].startswith("gs://")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_file_port_contract_serializes_shared_vocabulary():
|
|
56
|
+
"""File-port contracts should expose stable enum values to every repo."""
|
|
57
|
+
contract = FilePortContract(
|
|
58
|
+
contract_key="primary_model",
|
|
59
|
+
label="Primary Model",
|
|
60
|
+
direction=StepIODirection.INPUT,
|
|
61
|
+
io_medium=StepIOMedium.ARTIFACT,
|
|
62
|
+
envelope_channel=EnvelopeChannel.INPUT_FILES,
|
|
63
|
+
role="primary-model",
|
|
64
|
+
artifact_kind=ArtifactKind.FILE,
|
|
65
|
+
min_items=1,
|
|
66
|
+
max_items=1,
|
|
67
|
+
accepted_data_formats=["energyplus_idf", "energyplus_epjson"],
|
|
68
|
+
accepted_media_types=[
|
|
69
|
+
"application/vnd.energyplus.idf",
|
|
70
|
+
"application/vnd.energyplus.epjson",
|
|
71
|
+
],
|
|
72
|
+
allowed_source_scopes=[
|
|
73
|
+
BindingSourceScope.SUBMISSION_FILE,
|
|
74
|
+
BindingSourceScope.UPSTREAM_ARTIFACT,
|
|
75
|
+
BindingSourceScope.SIGNAL,
|
|
76
|
+
],
|
|
77
|
+
default_source_strategy=DefaultSourceStrategy.SUBMITTED_FILE_FIRST,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
dumped = contract.model_dump(mode="json")
|
|
81
|
+
|
|
82
|
+
assert dumped["contract_key"] == "primary_model"
|
|
83
|
+
assert dumped["io_medium"] == "artifact"
|
|
84
|
+
assert dumped["envelope_channel"] == "input_files"
|
|
85
|
+
assert dumped["allowed_source_scopes"] == [
|
|
86
|
+
"submission_file",
|
|
87
|
+
"upstream_artifact",
|
|
88
|
+
"signal",
|
|
89
|
+
]
|
|
90
|
+
assert dumped["default_source_strategy"] == "submitted_file_first"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_file_port_contract_rejects_extra_fields():
|
|
94
|
+
"""The contract should stay explicit rather than absorbing typos."""
|
|
95
|
+
with pytest.raises(ValidationError):
|
|
96
|
+
FilePortContract(
|
|
97
|
+
contract_key="primary_model",
|
|
98
|
+
direction=StepIODirection.INPUT,
|
|
99
|
+
envelope_channel=EnvelopeChannel.INPUT_FILES,
|
|
100
|
+
unknown=True,
|
|
101
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Tests for shared RFC 8785 / JCS canonicalization helpers.
|
|
2
|
+
|
|
3
|
+
The workflow-definition hash and Pro credential signing path both depend on
|
|
4
|
+
these helpers producing stable bytes from the same logical JSON. Keeping these
|
|
5
|
+
tests in ``validibot-shared`` pins the contract at the package boundary used by
|
|
6
|
+
community evidence producers, commercial signing code, and third-party
|
|
7
|
+
verifiers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
|
|
17
|
+
from validibot_shared.canonicalization import (
|
|
18
|
+
canonicalize_dict,
|
|
19
|
+
canonicalize_model,
|
|
20
|
+
sha256_hex_for_dict,
|
|
21
|
+
sha256_hex_for_model,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
UNSAFE_INTEGER = 9007199254740993
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SampleSchema(BaseModel):
|
|
28
|
+
"""Small frozen schema matching the credential-builder model pattern."""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
31
|
+
|
|
32
|
+
alpha: str
|
|
33
|
+
beta: int
|
|
34
|
+
gamma: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── RFC 8785 ordering and scalar handling ───────────────────────────────────
|
|
38
|
+
# These tests protect the cross-language JSON contract that hashes/signatures
|
|
39
|
+
# depend on: same logical object, same canonical bytes everywhere.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dict_keys_are_sorted_recursively() -> None:
|
|
43
|
+
"""Object keys must be sorted at every level of the JSON tree."""
|
|
44
|
+
result = canonicalize_dict(
|
|
45
|
+
{"outer_z": {"inner_b": 2, "inner_a": 1}, "outer_a": 0},
|
|
46
|
+
)
|
|
47
|
+
decoded = result.decode("utf-8")
|
|
48
|
+
assert decoded.index('"outer_a"') < decoded.index('"outer_z"')
|
|
49
|
+
assert decoded.index('"inner_a"') < decoded.index('"inner_b"')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_large_integer_is_preserved_exactly() -> None:
|
|
53
|
+
"""Integers larger than 2**53 must not be rounded through ``float``.
|
|
54
|
+
|
|
55
|
+
Credential signatures may include large identifiers or nonces. If JCS casts
|
|
56
|
+
them to IEEE-754 doubles, the canonical bytes silently change and a
|
|
57
|
+
standards-compliant verifier computes a different digest.
|
|
58
|
+
"""
|
|
59
|
+
result = canonicalize_dict({"n": UNSAFE_INTEGER})
|
|
60
|
+
assert result == b'{"n":9007199254740993}'
|
|
61
|
+
assert b"9007199254740992" not in result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_bool_still_serializes_as_json_literals() -> None:
|
|
65
|
+
"""``bool`` must remain JSON ``true``/``false`` despite subclassing ``int``."""
|
|
66
|
+
assert canonicalize_dict({"yes": True, "no": False}) == (b'{"no":false,"yes":true}')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_unicode_strings_are_preserved() -> None:
|
|
70
|
+
"""Non-ASCII text must round-trip through canonical bytes unchanged."""
|
|
71
|
+
result = canonicalize_dict({"name": "Prüfung", "description": "日本語テスト"})
|
|
72
|
+
parsed = json.loads(result)
|
|
73
|
+
assert parsed["name"] == "Prüfung"
|
|
74
|
+
assert parsed["description"] == "日本語テスト"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── Pydantic integration and digest wrappers ────────────────────────────────
|
|
78
|
+
# Producers build Pydantic models, while external verifiers often reconstruct
|
|
79
|
+
# plain dictionaries. Both paths must canonicalize to identical bytes.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_model_produces_same_bytes_as_equivalent_dict() -> None:
|
|
83
|
+
"""Pydantic model dumping must match an equivalent JSON dictionary."""
|
|
84
|
+
schema = SampleSchema(alpha="hello", beta=42, gamma=None)
|
|
85
|
+
plain = {"alpha": "hello", "beta": 42, "gamma": None}
|
|
86
|
+
assert canonicalize_model(schema) == canonicalize_dict(plain)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_sha256_for_dict_matches_manual_hash() -> None:
|
|
90
|
+
"""The dict digest helper must hash the canonical byte preimage exactly."""
|
|
91
|
+
data = {"key": "value", "number": 7}
|
|
92
|
+
expected = hashlib.sha256(canonicalize_dict(data)).hexdigest()
|
|
93
|
+
assert sha256_hex_for_dict(data) == expected
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_sha256_for_model_matches_manual_hash() -> None:
|
|
97
|
+
"""The model digest helper must hash the canonical byte preimage exactly."""
|
|
98
|
+
schema = SampleSchema(alpha="test", beta=99, gamma="yes")
|
|
99
|
+
expected = hashlib.sha256(canonicalize_model(schema)).hexdigest()
|
|
100
|
+
assert sha256_hex_for_model(schema) == expected
|
|
@@ -9,6 +9,7 @@ from validibot_shared.validations.envelopes import (
|
|
|
9
9
|
ExecutionContext,
|
|
10
10
|
InputFileItem,
|
|
11
11
|
MessageLocation,
|
|
12
|
+
ResourceFileItem,
|
|
12
13
|
Severity,
|
|
13
14
|
SupportedMimeType,
|
|
14
15
|
ValidationCallback,
|
|
@@ -51,9 +52,35 @@ def test_validation_input_envelope_defaults_schema_version():
|
|
|
51
52
|
|
|
52
53
|
assert envelope.schema_version == "validibot.input.v1"
|
|
53
54
|
assert envelope.input_files[0].role == "primary-model"
|
|
55
|
+
assert envelope.input_files[0].port_key is None
|
|
54
56
|
assert envelope.inputs == {}
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
def test_input_file_item_accepts_optional_port_key():
|
|
60
|
+
"""InputFileItem should carry declared file-port identity when available."""
|
|
61
|
+
item = InputFileItem(
|
|
62
|
+
name="model.idf",
|
|
63
|
+
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
64
|
+
role="primary-model",
|
|
65
|
+
port_key="primary_model",
|
|
66
|
+
uri="gs://bucket/model.idf",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert item.port_key == "primary_model"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_resource_file_item_accepts_optional_port_key():
|
|
73
|
+
"""ResourceFileItem should carry declared file-port identity when available."""
|
|
74
|
+
item = ResourceFileItem(
|
|
75
|
+
id="resource-1",
|
|
76
|
+
type="energyplus_weather",
|
|
77
|
+
port_key="weather_file",
|
|
78
|
+
uri="gs://bucket/weather.epw",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert item.port_key == "weather_file"
|
|
82
|
+
|
|
83
|
+
|
|
57
84
|
def test_input_file_item_forbids_extra_fields():
|
|
58
85
|
"""InputFileItem should forbid extra fields."""
|
|
59
86
|
with pytest.raises(ValidationError):
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Shared domain models and utilities for Validibot."""
|
|
2
2
|
|
|
3
|
+
from validibot_shared.canonicalization import (
|
|
4
|
+
canonicalize_dict,
|
|
5
|
+
canonicalize_model,
|
|
6
|
+
sha256_hex_for_dict,
|
|
7
|
+
sha256_hex_for_model,
|
|
8
|
+
)
|
|
3
9
|
from validibot_shared.energyplus.models import (
|
|
4
10
|
EnergyPlusSimulationLogs,
|
|
5
11
|
EnergyPlusSimulationMetrics,
|
|
@@ -30,6 +36,8 @@ __all__ = [
|
|
|
30
36
|
"EVIDENCE_SCHEMA_VERSION",
|
|
31
37
|
"ContractConstant",
|
|
32
38
|
"ContractSignalMapping",
|
|
39
|
+
"canonicalize_dict",
|
|
40
|
+
"canonicalize_model",
|
|
33
41
|
"EnergyPlusSimulationLogs",
|
|
34
42
|
"EnergyPlusSimulationMetrics",
|
|
35
43
|
"EnergyPlusSimulationOutputs",
|
|
@@ -43,6 +51,8 @@ __all__ = [
|
|
|
43
51
|
"SHACLOutputEnvelope",
|
|
44
52
|
"SHACLOutputs",
|
|
45
53
|
"SHACLSparqlAssertionSpec",
|
|
54
|
+
"sha256_hex_for_dict",
|
|
55
|
+
"sha256_hex_for_model",
|
|
46
56
|
"StepValidatorRecord",
|
|
47
57
|
"WorkflowContractSnapshot",
|
|
48
58
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored compatibility code used by ``validibot_shared`` internals."""
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vendored RFC 8785 JSON Canonicalization Scheme (JCS) implementation.
|
|
3
|
+
|
|
4
|
+
Vendored from: jcs 0.2.1 (https://pypi.org/project/jcs/)
|
|
5
|
+
Original author: WebPKI.org (http://webpki.org)
|
|
6
|
+
License: Apache License, Version 2.0
|
|
7
|
+
|
|
8
|
+
Copyright 2006-2019 WebPKI.org (http://webpki.org).
|
|
9
|
+
|
|
10
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
you may not use this file except in compliance with the License.
|
|
12
|
+
You may obtain a copy of the License at
|
|
13
|
+
|
|
14
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
|
|
16
|
+
Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
See the License for the specific language governing permissions and
|
|
20
|
+
limitations under the License.
|
|
21
|
+
|
|
22
|
+
This file combines _jcs.py and ntoj.py from the original package
|
|
23
|
+
into a single module for simplicity. The public API is the single
|
|
24
|
+
``canonicalize(obj)`` function.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# ntoj: Convert Python float/int into an ES6/V8-compatible JSON string
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _convert_to_es6_format(value):
|
|
37
|
+
"""Convert a Python number to its ES6/V8 canonical string representation.
|
|
38
|
+
|
|
39
|
+
This handles the tricky parts of RFC 8785 number serialization:
|
|
40
|
+
no trailing zeros, no positive sign, specific exponent formatting,
|
|
41
|
+
and special handling for zero and small/large values.
|
|
42
|
+
"""
|
|
43
|
+
fvalue = float(value)
|
|
44
|
+
|
|
45
|
+
# Zero is a special case. Handles "-0" as well.
|
|
46
|
+
if fvalue == 0:
|
|
47
|
+
return "0"
|
|
48
|
+
|
|
49
|
+
py_double = str(fvalue)
|
|
50
|
+
|
|
51
|
+
# Catch "inf" and "nan" values
|
|
52
|
+
if "n" in py_double:
|
|
53
|
+
msg = f"Invalid JSON number: {py_double}"
|
|
54
|
+
raise ValueError(msg)
|
|
55
|
+
|
|
56
|
+
# Save sign separately
|
|
57
|
+
py_sign = ""
|
|
58
|
+
if py_double.startswith("-"):
|
|
59
|
+
py_sign = "-"
|
|
60
|
+
py_double = py_double[1:]
|
|
61
|
+
|
|
62
|
+
# Extract exponent if present
|
|
63
|
+
py_exp_str = ""
|
|
64
|
+
py_exp_val = 0
|
|
65
|
+
q = py_double.find("e")
|
|
66
|
+
if q > 0:
|
|
67
|
+
py_exp_str = py_double[q:]
|
|
68
|
+
if py_exp_str[2:3] == "0":
|
|
69
|
+
# Suppress leading zero on exponents
|
|
70
|
+
py_exp_str = py_exp_str[:2] + py_exp_str[3:]
|
|
71
|
+
py_double = py_double[:q]
|
|
72
|
+
py_exp_val = int(py_exp_str[1:])
|
|
73
|
+
|
|
74
|
+
# Split number into first + dot + last
|
|
75
|
+
py_first = py_double
|
|
76
|
+
py_dot = ""
|
|
77
|
+
py_last = ""
|
|
78
|
+
q = py_double.find(".")
|
|
79
|
+
if q > 0:
|
|
80
|
+
py_dot = "."
|
|
81
|
+
py_first = py_double[:q]
|
|
82
|
+
py_last = py_double[q + 1 :]
|
|
83
|
+
|
|
84
|
+
# Always remove trailing .0
|
|
85
|
+
if py_last == "0":
|
|
86
|
+
py_dot = ""
|
|
87
|
+
py_last = ""
|
|
88
|
+
|
|
89
|
+
if 0 < py_exp_val < 21:
|
|
90
|
+
# Integers shown as-is with up to 21 digits
|
|
91
|
+
py_first += py_last
|
|
92
|
+
py_last = ""
|
|
93
|
+
py_dot = ""
|
|
94
|
+
py_exp_str = ""
|
|
95
|
+
q = py_exp_val - len(py_first)
|
|
96
|
+
while q >= 0:
|
|
97
|
+
q -= 1
|
|
98
|
+
py_first += "0"
|
|
99
|
+
elif -7 < py_exp_val < 0:
|
|
100
|
+
# Small numbers shown as 0.etc with e-6 as lower limit
|
|
101
|
+
py_last = py_first + py_last
|
|
102
|
+
py_first = "0"
|
|
103
|
+
py_dot = "."
|
|
104
|
+
py_exp_str = ""
|
|
105
|
+
q = py_exp_val
|
|
106
|
+
while q < -1:
|
|
107
|
+
q += 1
|
|
108
|
+
py_last = "0" + py_last
|
|
109
|
+
|
|
110
|
+
return py_sign + py_first + py_dot + py_last + py_exp_str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# _jcs: JCS-compatible JSON encoder
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
_ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
|
|
118
|
+
_ESCAPE_DCT = {
|
|
119
|
+
"\\": "\\\\",
|
|
120
|
+
'"': '\\"',
|
|
121
|
+
"\b": "\\b",
|
|
122
|
+
"\f": "\\f",
|
|
123
|
+
"\n": "\\n",
|
|
124
|
+
"\r": "\\r",
|
|
125
|
+
"\t": "\\t",
|
|
126
|
+
}
|
|
127
|
+
for _i in range(0x20):
|
|
128
|
+
_ESCAPE_DCT.setdefault(chr(_i), f"\\u{_i:04x}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _encode_basestring(s):
|
|
132
|
+
"""Return a JSON representation of a Python string (non-ASCII preserved)."""
|
|
133
|
+
|
|
134
|
+
def replace(match):
|
|
135
|
+
return _ESCAPE_DCT[match.group(0)]
|
|
136
|
+
|
|
137
|
+
return '"' + _ESCAPE.sub(replace, s) + '"'
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _serialize(obj):
|
|
141
|
+
"""Recursively serialize a Python object to a JCS-canonical JSON string.
|
|
142
|
+
|
|
143
|
+
Keys are sorted by UTF-16BE byte order per RFC 8785 §3.2.3.
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(obj, str):
|
|
146
|
+
return _encode_basestring(obj)
|
|
147
|
+
if obj is None:
|
|
148
|
+
return "null"
|
|
149
|
+
if obj is True:
|
|
150
|
+
return "true"
|
|
151
|
+
if obj is False:
|
|
152
|
+
return "false"
|
|
153
|
+
if isinstance(obj, int):
|
|
154
|
+
# RFC 8785 §3.2.2.3 mandates that JSON integers be emitted as their
|
|
155
|
+
# exact decimal literal. We must NOT route ``int`` through the ES6
|
|
156
|
+
# number formatter, because that helper begins ``float(value)`` which
|
|
157
|
+
# loses precision for any integer whose magnitude exceeds 2**53
|
|
158
|
+
# (the largest exactly-representable IEEE-754 double). Corrupting the
|
|
159
|
+
# value here would silently break cross-language canonical-hash
|
|
160
|
+
# reproducibility (e.g. signature verification against a non-Python
|
|
161
|
+
# signer). ``str(int)`` already yields the canonical literal: no
|
|
162
|
+
# leading zeros, a single optional ``-`` sign, and no ``+``/exponent.
|
|
163
|
+
# ``bool`` is handled above (it is a subclass of ``int``) so it never
|
|
164
|
+
# reaches this branch and still serializes as ``true``/``false``.
|
|
165
|
+
return str(obj)
|
|
166
|
+
if isinstance(obj, float):
|
|
167
|
+
return _convert_to_es6_format(obj)
|
|
168
|
+
if isinstance(obj, (list, tuple)):
|
|
169
|
+
return "[" + ",".join(_serialize(v) for v in obj) + "]"
|
|
170
|
+
if isinstance(obj, dict):
|
|
171
|
+
items = sorted(obj.items(), key=lambda kv: kv[0].encode("utf-16-be"))
|
|
172
|
+
return (
|
|
173
|
+
"{"
|
|
174
|
+
+ ",".join(_encode_basestring(k) + ":" + _serialize(v) for k, v in items)
|
|
175
|
+
+ "}"
|
|
176
|
+
)
|
|
177
|
+
msg = f"Object of type '{type(obj).__name__}' is not JSON serializable"
|
|
178
|
+
raise TypeError(msg)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def canonicalize(obj: object) -> bytes:
|
|
182
|
+
"""Serialize a Python object to RFC 8785 canonical JSON bytes.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
obj: A JSON-serializable Python object (dict, list, str,
|
|
186
|
+
int, float, bool, None).
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
UTF-8 encoded canonical JSON bytes.
|
|
190
|
+
"""
|
|
191
|
+
return _serialize(obj).encode("utf-8")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""RFC 8785 / JCS canonical JSON helpers shared across Validibot packages.
|
|
2
|
+
|
|
3
|
+
The evidence manifest producer, workflow-definition digest, and Pro credential
|
|
4
|
+
signing path all need the same deterministic byte representation. Keeping the
|
|
5
|
+
canonicalizer in ``validibot-shared`` gives community evidence producers,
|
|
6
|
+
commercial signing code, and third-party verifiers one portable contract instead
|
|
7
|
+
of a Pro-owned implementation detail.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from validibot_shared._vendor.jcs import canonicalize as _jcs_canonicalize
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def canonicalize_dict(data: dict[str, Any]) -> bytes:
|
|
21
|
+
"""Canonicalize a plain dictionary via RFC 8785 / JCS.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
data: A JSON-serializable dictionary.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Canonical UTF-8 encoded JSON bytes per RFC 8785.
|
|
28
|
+
"""
|
|
29
|
+
return _jcs_canonicalize(data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sha256_hex_for_dict(data: dict[str, Any]) -> str:
|
|
33
|
+
"""Compute the SHA-256 hex digest of a plain dict's canonical bytes."""
|
|
34
|
+
return hashlib.sha256(canonicalize_dict(data)).hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def canonicalize_model(schema_obj: BaseModel) -> bytes:
|
|
38
|
+
"""Canonicalize a Pydantic model via ``model_dump(mode="json")`` + JCS.
|
|
39
|
+
|
|
40
|
+
``mode="json"`` converts UUIDs, datetimes, and other Pydantic-native values
|
|
41
|
+
to JSON-compatible scalars before canonicalization. ``by_alias=True`` keeps
|
|
42
|
+
wire aliases such as ``@context`` intact.
|
|
43
|
+
"""
|
|
44
|
+
return canonicalize_dict(schema_obj.model_dump(mode="json", by_alias=True))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sha256_hex_for_model(schema_obj: BaseModel) -> str:
|
|
48
|
+
"""Compute the SHA-256 hex digest of a Pydantic model's canonical bytes."""
|
|
49
|
+
return hashlib.sha256(canonicalize_model(schema_obj)).hexdigest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"canonicalize_dict",
|
|
54
|
+
"canonicalize_model",
|
|
55
|
+
"sha256_hex_for_dict",
|
|
56
|
+
"sha256_hex_for_model",
|
|
57
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Shared artifact references and file-port contract vocabulary.
|
|
2
|
+
|
|
3
|
+
These schemas are control-plane objects. They describe files, resources, and
|
|
4
|
+
artifact references that Django materializes into validator envelopes; they do
|
|
5
|
+
not contain file bytes or Django workflow models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
ARTIFACT_REF_SCHEMA_VERSION = "validibot.artifact_ref.v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArtifactKind(str, Enum):
|
|
19
|
+
"""Storage-backed artifact kind."""
|
|
20
|
+
|
|
21
|
+
FILE = "file"
|
|
22
|
+
DIRECTORY = "directory"
|
|
23
|
+
ARCHIVE = "archive"
|
|
24
|
+
DATASET = "dataset"
|
|
25
|
+
REPORT = "report"
|
|
26
|
+
LOG = "log"
|
|
27
|
+
OTHER = "other"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StepIODirection(str, Enum):
|
|
31
|
+
"""Whether a step port is consumed or produced."""
|
|
32
|
+
|
|
33
|
+
INPUT = "input"
|
|
34
|
+
OUTPUT = "output"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StepIOMedium(str, Enum):
|
|
38
|
+
"""Whether a step port carries a JSON value or artifact reference."""
|
|
39
|
+
|
|
40
|
+
VALUE = "value"
|
|
41
|
+
ARTIFACT = "artifact"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EnvelopeChannel(str, Enum):
|
|
45
|
+
"""Wire-level envelope location for a resolved file port."""
|
|
46
|
+
|
|
47
|
+
INPUT_FILES = "input_files"
|
|
48
|
+
RESOURCE_FILES = "resource_files"
|
|
49
|
+
OUTPUT_ARTIFACTS = "output_artifacts"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BindingSourceScope(str, Enum):
|
|
53
|
+
"""Source scopes available to file-port and value bindings."""
|
|
54
|
+
|
|
55
|
+
SUBMISSION_PAYLOAD = "submission_payload"
|
|
56
|
+
SUBMISSION_METADATA = "submission_metadata"
|
|
57
|
+
SUBMISSION_FILE = "submission_file"
|
|
58
|
+
UPSTREAM_STEP = "upstream_step"
|
|
59
|
+
UPSTREAM_ARTIFACT = "upstream_artifact"
|
|
60
|
+
SIGNAL = "signal"
|
|
61
|
+
CONSTANT = "constant"
|
|
62
|
+
WORKFLOW_RESOURCE = "workflow_resource"
|
|
63
|
+
SYSTEM = "system"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DefaultSourceStrategy(str, Enum):
|
|
67
|
+
"""Automatic or suggested source-selection behavior for a file port."""
|
|
68
|
+
|
|
69
|
+
SUBMITTED_FILE_FIRST = "submitted_file_first"
|
|
70
|
+
SUBMITTED_FILE_THEN_DEFAULT_RESOURCE = "submitted_file_then_default_resource"
|
|
71
|
+
WORKFLOW_RESOURCE_DEFAULT = "workflow_resource_default"
|
|
72
|
+
UPSTREAM_ARTIFACT_SUGGESTION = "upstream_artifact_suggestion"
|
|
73
|
+
MANUAL = "manual"
|
|
74
|
+
NONE = "none"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class FilePortContract(BaseModel):
|
|
78
|
+
"""Shared description of a file-like step input or output port."""
|
|
79
|
+
|
|
80
|
+
contract_key: str = Field(
|
|
81
|
+
description="Stable Validibot port key, e.g. primary_model."
|
|
82
|
+
)
|
|
83
|
+
label: str = Field(default="", description="Human-facing port label.")
|
|
84
|
+
direction: StepIODirection = Field(description="Whether this port is input/output.")
|
|
85
|
+
io_medium: StepIOMedium = Field(
|
|
86
|
+
default=StepIOMedium.ARTIFACT,
|
|
87
|
+
description="File ports carry artifact references.",
|
|
88
|
+
)
|
|
89
|
+
envelope_channel: EnvelopeChannel = Field(
|
|
90
|
+
description="Envelope channel this port renders into."
|
|
91
|
+
)
|
|
92
|
+
role: str = Field(
|
|
93
|
+
default="",
|
|
94
|
+
description="Backend-facing role for input_files or output artifacts.",
|
|
95
|
+
)
|
|
96
|
+
resource_type: str = Field(
|
|
97
|
+
default="",
|
|
98
|
+
description="Backend-facing resource type for resource_files.",
|
|
99
|
+
)
|
|
100
|
+
artifact_kind: ArtifactKind = Field(
|
|
101
|
+
default=ArtifactKind.FILE,
|
|
102
|
+
description="Expected kind for artifact references.",
|
|
103
|
+
)
|
|
104
|
+
min_items: int = Field(default=0, ge=0, description="Minimum item count.")
|
|
105
|
+
max_items: int | None = Field(
|
|
106
|
+
default=1,
|
|
107
|
+
ge=1,
|
|
108
|
+
description="Maximum item count; None means deliberately unbounded.",
|
|
109
|
+
)
|
|
110
|
+
accepted_data_formats: list[str] = Field(default_factory=list)
|
|
111
|
+
accepted_media_types: list[str] = Field(default_factory=list)
|
|
112
|
+
allowed_source_scopes: list[BindingSourceScope] = Field(default_factory=list)
|
|
113
|
+
default_source_strategy: DefaultSourceStrategy = DefaultSourceStrategy.NONE
|
|
114
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
115
|
+
|
|
116
|
+
model_config = {"extra": "forbid"}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ArtifactRef(BaseModel):
|
|
120
|
+
"""Small JSON-safe pointer to a run-scoped artifact."""
|
|
121
|
+
|
|
122
|
+
schema_version: Literal["validibot.artifact_ref.v1"] = ARTIFACT_REF_SCHEMA_VERSION
|
|
123
|
+
|
|
124
|
+
artifact_id: str
|
|
125
|
+
run_id: str
|
|
126
|
+
step_run_id: str
|
|
127
|
+
producer_step_key: str
|
|
128
|
+
|
|
129
|
+
contract_key: str
|
|
130
|
+
name: str
|
|
131
|
+
role: str = ""
|
|
132
|
+
kind: ArtifactKind = ArtifactKind.FILE
|
|
133
|
+
|
|
134
|
+
media_type: str = ""
|
|
135
|
+
data_format: str = ""
|
|
136
|
+
filename: str = ""
|
|
137
|
+
size_bytes: int | None = None
|
|
138
|
+
sha256: str = ""
|
|
139
|
+
|
|
140
|
+
uri: str = Field(
|
|
141
|
+
default="",
|
|
142
|
+
description="Internal storage URI; not a public download URL.",
|
|
143
|
+
)
|
|
144
|
+
manifest_uri: str = ""
|
|
145
|
+
manifest_sha256: str = ""
|
|
146
|
+
|
|
147
|
+
producer_validator_type: str = ""
|
|
148
|
+
producer_validator_version: str = ""
|
|
149
|
+
producer_backend_image_digest: str = ""
|
|
150
|
+
|
|
151
|
+
retention_class: str = ""
|
|
152
|
+
labels: list[str] = Field(default_factory=list)
|
|
153
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
154
|
+
|
|
155
|
+
model_config = {"extra": "forbid"}
|
{validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/validations/envelopes.py
RENAMED
|
@@ -205,6 +205,14 @@ class InputFileItem(BaseModel):
|
|
|
205
205
|
description="Validator-specific role (e.g., 'primary-model', 'config')",
|
|
206
206
|
)
|
|
207
207
|
|
|
208
|
+
port_key: str | None = Field(
|
|
209
|
+
default=None,
|
|
210
|
+
description=(
|
|
211
|
+
"Declared Validibot file-port key that produced this envelope item "
|
|
212
|
+
"(e.g., 'primary_model'). Optional for backwards-compatible rollout."
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
208
216
|
uri: str = Field(
|
|
209
217
|
description="Storage URI to the file (gs:// or file:// for self-hosted)"
|
|
210
218
|
)
|
|
@@ -232,6 +240,14 @@ class ResourceFileItem(BaseModel):
|
|
|
232
240
|
description="Resource type (e.g., 'weather', 'library', 'config')"
|
|
233
241
|
)
|
|
234
242
|
|
|
243
|
+
port_key: str | None = Field(
|
|
244
|
+
default=None,
|
|
245
|
+
description=(
|
|
246
|
+
"Declared Validibot file-port key that produced this resource item "
|
|
247
|
+
"(e.g., 'weather_file'). Optional for backwards-compatible rollout."
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
|
|
235
251
|
uri: str = Field(
|
|
236
252
|
description="Storage URI to the file (gs:// or file:// for self-hosted)"
|
|
237
253
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/energyplus/envelopes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/schematron/envelopes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{validibot_shared-0.12.0 → validibot_shared-0.13.0}/validibot_shared/validations/__init__.py
RENAMED
|
File without changes
|