validibot-shared 0.14.0__tar.gz → 0.16.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.14.0 → validibot_shared-0.16.0}/PKG-INFO +18 -6
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/README.md +17 -5
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/pyproject.toml +1 -1
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_artifacts.py +23 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_energyplus_envelopes.py +13 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_fmu_envelopes.py +15 -1
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_schematron_envelopes.py +12 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_shacl_envelopes.py +28 -7
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_validations_envelopes.py +144 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/energyplus/envelopes.py +2 -2
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/energyplus/models.py +4 -4
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/fmu/envelopes.py +17 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/fmu/models.py +1 -1
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/schematron/envelopes.py +20 -3
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/shacl/envelopes.py +22 -5
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/validations/artifacts.py +20 -4
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/validations/envelopes.py +116 -12
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/.gitignore +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/LICENSE +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/NOTICE +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_canonicalization.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_energyplus_models.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_evidence_manifest.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_fmu_models.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_package_init.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/tests/test_schematron_svrl.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/_vendor/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/_vendor/jcs.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/canonicalization.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/energyplus/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/evidence/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/evidence/manifest.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/fmu/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/py.typed +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/schematron/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/schematron/svrl.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/shacl/__init__.py +0 -0
- {validibot_shared-0.14.0 → validibot_shared-0.16.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.16.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
|
|
@@ -166,8 +166,8 @@ Supporting models include:
|
|
|
166
166
|
|
|
167
167
|
| Class | Purpose |
|
|
168
168
|
|-------|---------|
|
|
169
|
-
| `InputFileItem` | File reference with URI, MIME type, and
|
|
170
|
-
| `ResourceFileItem` | Managed auxiliary file reference with URI
|
|
169
|
+
| `InputFileItem` | File reference with URI, MIME type, role, exact size, SHA-256, and immutable storage version |
|
|
170
|
+
| `ResourceFileItem` | Managed auxiliary file reference with safe name, URI, resource type, and byte identity |
|
|
171
171
|
| `ValidatorInfo` | Validator identification (ID, type, version) |
|
|
172
172
|
| `ExecutionContext` | Callback URL, execution bundle URI, timeout |
|
|
173
173
|
| `ValidationMessage` | Individual finding (error, warning, info) |
|
|
@@ -199,8 +199,10 @@ for bytes. For example, EnergyPlus timestep settings belong in
|
|
|
199
199
|
`EnergyPlusInputs`; the IDF/epJSON model and EPW weather file belong in file
|
|
200
200
|
ports rendered to `input_files` / `resource_files`.
|
|
201
201
|
|
|
202
|
-
Backends should read files by role
|
|
203
|
-
|
|
202
|
+
Backends should read files by role and `port_key` when available, not by
|
|
203
|
+
assuming `input_files[0]` forever. Every file item commits to an exact size,
|
|
204
|
+
SHA-256, and provider-specific immutable storage version; runtimes must verify
|
|
205
|
+
those fields while streaming before a validator parses or executes the bytes.
|
|
204
206
|
|
|
205
207
|
`ArtifactRef` and `FilePortContract` live in
|
|
206
208
|
`validibot_shared.validations.artifacts`. `InputFileItem` and
|
|
@@ -256,6 +258,7 @@ validibot_shared/
|
|
|
256
258
|
```python
|
|
257
259
|
from validibot_shared.energyplus import EnergyPlusInputEnvelope, EnergyPlusInputs
|
|
258
260
|
from validibot_shared.validations.envelopes import (
|
|
261
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
259
262
|
ExecutionContext,
|
|
260
263
|
InputFileItem,
|
|
261
264
|
OrganizationInfo,
|
|
@@ -284,12 +287,21 @@ envelope = EnergyPlusInputEnvelope(
|
|
|
284
287
|
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
285
288
|
role="primary-model",
|
|
286
289
|
uri="gs://bucket/model.idf",
|
|
290
|
+
size_bytes=12345,
|
|
291
|
+
sha256="0123456789abcdef" * 4,
|
|
292
|
+
storage_version="1700000000000000",
|
|
287
293
|
),
|
|
288
294
|
],
|
|
289
295
|
inputs=EnergyPlusInputs(timestep_per_hour=4),
|
|
290
296
|
context=ExecutionContext(
|
|
291
297
|
callback_url="https://api.example.com/callback",
|
|
292
|
-
execution_bundle_uri="gs://bucket/run-123/",
|
|
298
|
+
execution_bundle_uri="gs://bucket/runs/org-123/run-123/attempts/attempt-123/",
|
|
299
|
+
execution_attempt_id="attempt-123",
|
|
300
|
+
step_run_id="step-run-789",
|
|
301
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
302
|
+
expected_output_uri=(
|
|
303
|
+
"gs://bucket/runs/org-123/run-123/attempts/attempt-123/output.json"
|
|
304
|
+
),
|
|
293
305
|
),
|
|
294
306
|
)
|
|
295
307
|
|
|
@@ -133,8 +133,8 @@ Supporting models include:
|
|
|
133
133
|
|
|
134
134
|
| Class | Purpose |
|
|
135
135
|
|-------|---------|
|
|
136
|
-
| `InputFileItem` | File reference with URI, MIME type, and
|
|
137
|
-
| `ResourceFileItem` | Managed auxiliary file reference with URI
|
|
136
|
+
| `InputFileItem` | File reference with URI, MIME type, role, exact size, SHA-256, and immutable storage version |
|
|
137
|
+
| `ResourceFileItem` | Managed auxiliary file reference with safe name, URI, resource type, and byte identity |
|
|
138
138
|
| `ValidatorInfo` | Validator identification (ID, type, version) |
|
|
139
139
|
| `ExecutionContext` | Callback URL, execution bundle URI, timeout |
|
|
140
140
|
| `ValidationMessage` | Individual finding (error, warning, info) |
|
|
@@ -166,8 +166,10 @@ for bytes. For example, EnergyPlus timestep settings belong in
|
|
|
166
166
|
`EnergyPlusInputs`; the IDF/epJSON model and EPW weather file belong in file
|
|
167
167
|
ports rendered to `input_files` / `resource_files`.
|
|
168
168
|
|
|
169
|
-
Backends should read files by role
|
|
170
|
-
|
|
169
|
+
Backends should read files by role and `port_key` when available, not by
|
|
170
|
+
assuming `input_files[0]` forever. Every file item commits to an exact size,
|
|
171
|
+
SHA-256, and provider-specific immutable storage version; runtimes must verify
|
|
172
|
+
those fields while streaming before a validator parses or executes the bytes.
|
|
171
173
|
|
|
172
174
|
`ArtifactRef` and `FilePortContract` live in
|
|
173
175
|
`validibot_shared.validations.artifacts`. `InputFileItem` and
|
|
@@ -223,6 +225,7 @@ validibot_shared/
|
|
|
223
225
|
```python
|
|
224
226
|
from validibot_shared.energyplus import EnergyPlusInputEnvelope, EnergyPlusInputs
|
|
225
227
|
from validibot_shared.validations.envelopes import (
|
|
228
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
226
229
|
ExecutionContext,
|
|
227
230
|
InputFileItem,
|
|
228
231
|
OrganizationInfo,
|
|
@@ -251,12 +254,21 @@ envelope = EnergyPlusInputEnvelope(
|
|
|
251
254
|
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
252
255
|
role="primary-model",
|
|
253
256
|
uri="gs://bucket/model.idf",
|
|
257
|
+
size_bytes=12345,
|
|
258
|
+
sha256="0123456789abcdef" * 4,
|
|
259
|
+
storage_version="1700000000000000",
|
|
254
260
|
),
|
|
255
261
|
],
|
|
256
262
|
inputs=EnergyPlusInputs(timestep_per_hour=4),
|
|
257
263
|
context=ExecutionContext(
|
|
258
264
|
callback_url="https://api.example.com/callback",
|
|
259
|
-
execution_bundle_uri="gs://bucket/run-123/",
|
|
265
|
+
execution_bundle_uri="gs://bucket/runs/org-123/run-123/attempts/attempt-123/",
|
|
266
|
+
execution_attempt_id="attempt-123",
|
|
267
|
+
step_run_id="step-run-789",
|
|
268
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
269
|
+
expected_output_uri=(
|
|
270
|
+
"gs://bucket/runs/org-123/run-123/attempts/attempt-123/output.json"
|
|
271
|
+
),
|
|
260
272
|
),
|
|
261
273
|
)
|
|
262
274
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "validibot-shared"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.16.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,6 +37,7 @@ def test_artifact_ref_serializes_as_json_safe_control_plane_object():
|
|
|
37
37
|
filename="model.epjson",
|
|
38
38
|
size_bytes=1234,
|
|
39
39
|
sha256="a" * 64,
|
|
40
|
+
storage_version="1700000000000000",
|
|
40
41
|
uri="gs://bucket/runs/run-1/model.epjson",
|
|
41
42
|
producer_validator_type="BUILDINGSYNC_TO_ENERGYPLUS",
|
|
42
43
|
producer_validator_version="1",
|
|
@@ -50,6 +51,28 @@ def test_artifact_ref_serializes_as_json_safe_control_plane_object():
|
|
|
50
51
|
assert dumped["kind"] == "file"
|
|
51
52
|
assert dumped["producer_step_key"] == "build_model"
|
|
52
53
|
assert dumped["uri"].startswith("gs://")
|
|
54
|
+
assert dumped["storage_version"] == "1700000000000000"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("missing", ["size_bytes", "sha256", "storage_version", "uri"])
|
|
58
|
+
def test_artifact_ref_requires_complete_storage_identity(missing):
|
|
59
|
+
"""Artifact consumers must never receive a partial or mutable file pointer."""
|
|
60
|
+
payload = {
|
|
61
|
+
"artifact_id": "artifact-1",
|
|
62
|
+
"run_id": "run-1",
|
|
63
|
+
"step_run_id": "step-run-1",
|
|
64
|
+
"producer_step_key": "build_model",
|
|
65
|
+
"contract_key": "generated_model",
|
|
66
|
+
"name": "model.epjson",
|
|
67
|
+
"size_bytes": 1234,
|
|
68
|
+
"sha256": "a" * 64,
|
|
69
|
+
"storage_version": "1700000000000000",
|
|
70
|
+
"uri": "gs://bucket/runs/run-1/model.epjson",
|
|
71
|
+
}
|
|
72
|
+
payload.pop(missing)
|
|
73
|
+
|
|
74
|
+
with pytest.raises(ValidationError):
|
|
75
|
+
ArtifactRef(**payload)
|
|
53
76
|
|
|
54
77
|
|
|
55
78
|
def test_file_port_contract_serializes_shared_vocabulary():
|
|
@@ -10,6 +10,7 @@ from validibot_shared.energyplus.envelopes import (
|
|
|
10
10
|
EnergyPlusOutputs,
|
|
11
11
|
)
|
|
12
12
|
from validibot_shared.validations.envelopes import (
|
|
13
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
13
14
|
ExecutionContext,
|
|
14
15
|
InputFileItem,
|
|
15
16
|
SupportedMimeType,
|
|
@@ -41,11 +42,18 @@ def _base_input_envelope_kwargs():
|
|
|
41
42
|
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
42
43
|
role="primary-model",
|
|
43
44
|
uri="gs://bucket/model.idf",
|
|
45
|
+
size_bytes=123,
|
|
46
|
+
sha256="1" * 64,
|
|
47
|
+
storage_version="1700000000000000",
|
|
44
48
|
)
|
|
45
49
|
],
|
|
46
50
|
"context": ExecutionContext(
|
|
47
51
|
callback_url="https://example.com/callback",
|
|
48
52
|
execution_bundle_uri="gs://bucket/run-1/",
|
|
53
|
+
execution_attempt_id="attempt-1",
|
|
54
|
+
step_run_id="step-run-1",
|
|
55
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
56
|
+
expected_output_uri="gs://bucket/run-1/output.json",
|
|
49
57
|
timeout_seconds=600,
|
|
50
58
|
tags=["smoke"],
|
|
51
59
|
),
|
|
@@ -125,6 +133,11 @@ def test_energyplus_output_envelope_accepts_typed_outputs():
|
|
|
125
133
|
"""EnergyPlusOutputEnvelope should accept typed outputs."""
|
|
126
134
|
envelope = EnergyPlusOutputEnvelope(
|
|
127
135
|
run_id="run-2",
|
|
136
|
+
step_run_id="step-run-2",
|
|
137
|
+
execution_attempt_id="attempt-2",
|
|
138
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
139
|
+
input_envelope_sha256="a" * 64,
|
|
140
|
+
output_uri="gs://bucket/run-2/output.json",
|
|
128
141
|
validator={
|
|
129
142
|
"id": "val-1",
|
|
130
143
|
"type": ValidatorType.ENERGYPLUS,
|
|
@@ -9,7 +9,11 @@ from validibot_shared.fmu.envelopes import (
|
|
|
9
9
|
FMUSimulationConfig,
|
|
10
10
|
build_fmu_input_envelope,
|
|
11
11
|
)
|
|
12
|
-
from validibot_shared.validations.envelopes import
|
|
12
|
+
from validibot_shared.validations.envelopes import (
|
|
13
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
14
|
+
ExecutionContext,
|
|
15
|
+
ValidatorType,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
# Test constants
|
|
15
19
|
TEST_EXECUTION_SECONDS = 2.5
|
|
@@ -25,6 +29,10 @@ def _base_kwargs():
|
|
|
25
29
|
"context": ExecutionContext(
|
|
26
30
|
callback_url="https://example.com/cb",
|
|
27
31
|
execution_bundle_uri="gs://bucket/run-123/",
|
|
32
|
+
execution_attempt_id="attempt-123",
|
|
33
|
+
step_run_id="step-run-123",
|
|
34
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
35
|
+
expected_output_uri="gs://bucket/run-123/output.json",
|
|
28
36
|
),
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -90,9 +98,15 @@ def test_build_fmu_input_envelope_constructs_expected_payload():
|
|
|
90
98
|
step_id="step-1",
|
|
91
99
|
step_name="Simulate",
|
|
92
100
|
fmu_uri="gs://bucket/model.fmu",
|
|
101
|
+
fmu_size_bytes=456,
|
|
102
|
+
fmu_sha256="1" * 64,
|
|
103
|
+
fmu_storage_version="1700000000000000",
|
|
93
104
|
input_values={"u1": 1.0},
|
|
94
105
|
callback_url="https://example.com/callback",
|
|
95
106
|
execution_bundle_uri="gs://bucket/run-1/",
|
|
107
|
+
execution_attempt_id="attempt-1",
|
|
108
|
+
step_run_id="step-run-1",
|
|
109
|
+
expected_output_uri="gs://bucket/run-1/output.json",
|
|
96
110
|
output_variables=["y"],
|
|
97
111
|
)
|
|
98
112
|
|
|
@@ -24,6 +24,7 @@ from validibot_shared.schematron.envelopes import (
|
|
|
24
24
|
build_schematron_input_envelope,
|
|
25
25
|
)
|
|
26
26
|
from validibot_shared.validations.envelopes import (
|
|
27
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
27
28
|
SupportedMimeType,
|
|
28
29
|
ValidationStatus,
|
|
29
30
|
ValidatorType,
|
|
@@ -121,6 +122,11 @@ def test_engine_failure_shapes_round_trip():
|
|
|
121
122
|
):
|
|
122
123
|
envelope = SchematronOutputEnvelope(
|
|
123
124
|
run_id="run-1",
|
|
125
|
+
step_run_id="step-run-1",
|
|
126
|
+
execution_attempt_id="attempt-1",
|
|
127
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
128
|
+
input_envelope_sha256="a" * 64,
|
|
129
|
+
output_uri="gs://bucket/run-1/output.json",
|
|
124
130
|
validator={"id": "v1", "type": ValidatorType.SCHEMATRON, "version": "1"},
|
|
125
131
|
status=status,
|
|
126
132
|
timing={},
|
|
@@ -190,9 +196,15 @@ def test_build_schematron_input_envelope_assembles_the_xml_submission():
|
|
|
190
196
|
step_id="step-1",
|
|
191
197
|
step_name="Peppol rules",
|
|
192
198
|
submission_uri="gs://bucket/run-1/submission.xml",
|
|
199
|
+
submission_size_bytes=321,
|
|
200
|
+
submission_sha256="1" * 64,
|
|
201
|
+
submission_storage_version="1700000000000000",
|
|
193
202
|
inputs=_inputs(),
|
|
194
203
|
callback_url="https://example.com/callback",
|
|
195
204
|
execution_bundle_uri="gs://bucket/run-1/",
|
|
205
|
+
execution_attempt_id="attempt-1",
|
|
206
|
+
step_run_id="step-run-1",
|
|
207
|
+
expected_output_uri="gs://bucket/run-1/output.json",
|
|
196
208
|
)
|
|
197
209
|
|
|
198
210
|
assert isinstance(envelope, SchematronInputEnvelope)
|
|
@@ -13,8 +13,8 @@ We therefore assert three things:
|
|
|
13
13
|
(Django serializes the input; the container serializes the output).
|
|
14
14
|
2. ``extra="forbid"`` is honoured so a drifting producer/consumer fails loudly
|
|
15
15
|
rather than silently dropping fields across the trust boundary.
|
|
16
|
-
3. The ``SHACLOutputs``
|
|
17
|
-
``
|
|
16
|
+
3. The ``SHACLOutputs`` output-value fields stay aligned with what Django's
|
|
17
|
+
``extract_output_values`` expects (the ``o.*`` catalog keys).
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import pytest
|
|
@@ -31,13 +31,14 @@ from validibot_shared.shacl.envelopes import (
|
|
|
31
31
|
mime_type_for_rdf_format,
|
|
32
32
|
)
|
|
33
33
|
from validibot_shared.validations.envelopes import (
|
|
34
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
34
35
|
ExecutionContext,
|
|
35
36
|
SupportedMimeType,
|
|
36
37
|
ValidationStatus,
|
|
37
38
|
ValidatorType,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
|
-
# The o.*
|
|
41
|
+
# The o.* output-value keys Django's SHACL catalog declares. If SHACLOutputs ever
|
|
41
42
|
# drops one of these, the Django extractor would silently lose a CEL signal —
|
|
42
43
|
# this list is the canary.
|
|
43
44
|
EXPECTED_SIGNAL_KEYS = {
|
|
@@ -65,6 +66,10 @@ def _base_kwargs():
|
|
|
65
66
|
"context": ExecutionContext(
|
|
66
67
|
callback_url="https://example.com/cb",
|
|
67
68
|
execution_bundle_uri="gs://bucket/run-123/",
|
|
69
|
+
execution_attempt_id="attempt-123",
|
|
70
|
+
step_run_id="step-run-123",
|
|
71
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
72
|
+
expected_output_uri="gs://bucket/run-123/output.json",
|
|
68
73
|
),
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -154,9 +159,15 @@ def test_build_shacl_input_envelope_constructs_expected_payload():
|
|
|
154
159
|
step_id="step-1",
|
|
155
160
|
step_name="Validate graph",
|
|
156
161
|
submission_uri="gs://bucket/submission.jsonld",
|
|
162
|
+
submission_size_bytes=789,
|
|
163
|
+
submission_sha256="1" * 64,
|
|
164
|
+
submission_storage_version="1700000000000000",
|
|
157
165
|
inputs=inputs,
|
|
158
166
|
callback_url="https://example.com/callback",
|
|
159
167
|
execution_bundle_uri="gs://bucket/run-1/",
|
|
168
|
+
execution_attempt_id="attempt-1",
|
|
169
|
+
step_run_id="step-run-1",
|
|
170
|
+
expected_output_uri="gs://bucket/run-1/output.json",
|
|
160
171
|
skip_callback=True,
|
|
161
172
|
)
|
|
162
173
|
|
|
@@ -197,21 +208,21 @@ def test_sparql_assertion_spec_defaults():
|
|
|
197
208
|
|
|
198
209
|
|
|
199
210
|
# ── Output envelope ─────────────────────────────────────────────────────────
|
|
200
|
-
# The output envelope is what the container writes back. Its
|
|
211
|
+
# The output envelope is what the container writes back. Its output-value fields feed
|
|
201
212
|
# Django's CEL/Basic assertion evaluation; its assertion tallies are folded into
|
|
202
213
|
# the final AssertionStats.
|
|
203
214
|
|
|
204
215
|
|
|
205
216
|
def test_shacl_outputs_signal_keys_cover_catalog():
|
|
206
|
-
"""Every catalog
|
|
217
|
+
"""Every catalog output-value key must exist as a field on ``SHACLOutputs``.
|
|
207
218
|
|
|
208
|
-
Django's ``
|
|
219
|
+
Django's ``extract_output_values`` reads these keys off the envelope; a
|
|
209
220
|
missing field here would surface as a null CEL signal at runtime with no
|
|
210
221
|
error. This test pins the alignment at the contract layer.
|
|
211
222
|
"""
|
|
212
223
|
field_names = set(SHACLOutputs.model_fields)
|
|
213
224
|
missing = EXPECTED_SIGNAL_KEYS - field_names
|
|
214
|
-
assert not missing, f"SHACLOutputs missing
|
|
225
|
+
assert not missing, f"SHACLOutputs missing output-value fields: {missing}"
|
|
215
226
|
|
|
216
227
|
|
|
217
228
|
def test_shacl_output_envelope_round_trips():
|
|
@@ -223,6 +234,11 @@ def test_shacl_output_envelope_round_trips():
|
|
|
223
234
|
"""
|
|
224
235
|
envelope = SHACLOutputEnvelope(
|
|
225
236
|
run_id="run-123",
|
|
237
|
+
step_run_id="step-run-123",
|
|
238
|
+
execution_attempt_id="attempt-123",
|
|
239
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
240
|
+
input_envelope_sha256="a" * 64,
|
|
241
|
+
output_uri="gs://bucket/run-123/output.json",
|
|
226
242
|
validator={"id": "val-1", "type": ValidatorType.SHACL, "version": "1"},
|
|
227
243
|
status=ValidationStatus.FAILED_VALIDATION,
|
|
228
244
|
timing={},
|
|
@@ -259,6 +275,11 @@ def test_shacl_output_envelope_allows_none_outputs():
|
|
|
259
275
|
"""
|
|
260
276
|
envelope = SHACLOutputEnvelope(
|
|
261
277
|
run_id="run-123",
|
|
278
|
+
step_run_id="step-run-123",
|
|
279
|
+
execution_attempt_id="attempt-123",
|
|
280
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
281
|
+
input_envelope_sha256="a" * 64,
|
|
282
|
+
output_uri="gs://bucket/run-123/output.json",
|
|
262
283
|
validator={"id": "val-1", "type": ValidatorType.SHACL, "version": "1"},
|
|
263
284
|
status=ValidationStatus.FAILED_RUNTIME,
|
|
264
285
|
timing={},
|
|
@@ -5,13 +5,16 @@ from datetime import UTC, datetime
|
|
|
5
5
|
import pytest
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
|
+
from validibot_shared.canonicalization import sha256_hex_for_model
|
|
8
9
|
from validibot_shared.validations.envelopes import (
|
|
10
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
9
11
|
ExecutionContext,
|
|
10
12
|
InputFileItem,
|
|
11
13
|
MessageLocation,
|
|
12
14
|
ResourceFileItem,
|
|
13
15
|
Severity,
|
|
14
16
|
SupportedMimeType,
|
|
17
|
+
ValidationArtifact,
|
|
15
18
|
ValidationCallback,
|
|
16
19
|
ValidationInputEnvelope,
|
|
17
20
|
ValidationMessage,
|
|
@@ -37,15 +40,33 @@ def _base_input_envelope_kwargs():
|
|
|
37
40
|
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
38
41
|
role="primary-model",
|
|
39
42
|
uri="gs://bucket/model.idf",
|
|
43
|
+
size_bytes=123,
|
|
44
|
+
sha256="1" * 64,
|
|
45
|
+
storage_version="1700000000000000",
|
|
40
46
|
)
|
|
41
47
|
],
|
|
42
48
|
"context": ExecutionContext(
|
|
43
49
|
callback_url="https://example.com/callback",
|
|
44
50
|
execution_bundle_uri="gs://bucket/run-42/",
|
|
51
|
+
execution_attempt_id="attempt-42",
|
|
52
|
+
step_run_id="step-run-42",
|
|
53
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
54
|
+
expected_output_uri="gs://bucket/run-42/output.json",
|
|
45
55
|
),
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
|
|
59
|
+
def _output_identity_kwargs():
|
|
60
|
+
"""Return the immutable execution-attempt identity required on outputs."""
|
|
61
|
+
return {
|
|
62
|
+
"step_run_id": "step-run-99",
|
|
63
|
+
"execution_attempt_id": "attempt-99",
|
|
64
|
+
"attempt_contract_version": ATTEMPT_CONTRACT_VERSION,
|
|
65
|
+
"input_envelope_sha256": "a" * 64,
|
|
66
|
+
"output_uri": "gs://bucket/run-99/output.json",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
49
70
|
def test_validation_input_envelope_defaults_schema_version():
|
|
50
71
|
"""ValidationInputEnvelope should have default schema version."""
|
|
51
72
|
envelope = ValidationInputEnvelope(**_base_input_envelope_kwargs())
|
|
@@ -56,6 +77,49 @@ def test_validation_input_envelope_defaults_schema_version():
|
|
|
56
77
|
assert envelope.inputs == {}
|
|
57
78
|
|
|
58
79
|
|
|
80
|
+
def test_strict_attempt_fixture_has_the_cross_repo_canonical_digest():
|
|
81
|
+
"""All producers and consumers must hash the same attempt fixture bytes."""
|
|
82
|
+
envelope = ValidationInputEnvelope(
|
|
83
|
+
run_id="run-fixture",
|
|
84
|
+
validator={
|
|
85
|
+
"id": "validator-fixture",
|
|
86
|
+
"type": ValidatorType.FMU,
|
|
87
|
+
"version": "1",
|
|
88
|
+
},
|
|
89
|
+
org={"id": "org-fixture", "name": "Fixture Org"},
|
|
90
|
+
workflow={
|
|
91
|
+
"id": "workflow-fixture",
|
|
92
|
+
"step_id": "step-fixture",
|
|
93
|
+
"step_name": "Fixture Step",
|
|
94
|
+
},
|
|
95
|
+
input_files=[
|
|
96
|
+
{
|
|
97
|
+
"name": "model.fmu",
|
|
98
|
+
"mime_type": SupportedMimeType.FMU,
|
|
99
|
+
"role": "fmu",
|
|
100
|
+
"port_key": "fmu_model",
|
|
101
|
+
"uri": "gs://fixture/runs/run-fixture/model.fmu",
|
|
102
|
+
"size_bytes": 12,
|
|
103
|
+
"sha256": "1" * 64,
|
|
104
|
+
"storage_version": "1700000000000000",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
inputs={"alpha": 1},
|
|
108
|
+
context={
|
|
109
|
+
"execution_attempt_id": "attempt-fixture",
|
|
110
|
+
"step_run_id": "step-run-fixture",
|
|
111
|
+
"attempt_contract_version": ATTEMPT_CONTRACT_VERSION,
|
|
112
|
+
"expected_output_uri": "gs://fixture/runs/run-fixture/output.json",
|
|
113
|
+
"execution_bundle_uri": "gs://fixture/runs/run-fixture/",
|
|
114
|
+
"skip_callback": True,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
assert sha256_hex_for_model(envelope) == (
|
|
119
|
+
"e17c5dae05c58f4d6034806e3f5e7a7602013d03f27ec811a97a9fc49f9d88d5"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
59
123
|
def test_input_file_item_accepts_optional_port_key():
|
|
60
124
|
"""InputFileItem should carry declared file-port identity when available."""
|
|
61
125
|
item = InputFileItem(
|
|
@@ -64,6 +128,9 @@ def test_input_file_item_accepts_optional_port_key():
|
|
|
64
128
|
role="primary-model",
|
|
65
129
|
port_key="primary_model",
|
|
66
130
|
uri="gs://bucket/model.idf",
|
|
131
|
+
size_bytes=123,
|
|
132
|
+
sha256="1" * 64,
|
|
133
|
+
storage_version="1700000000000000",
|
|
67
134
|
)
|
|
68
135
|
|
|
69
136
|
assert item.port_key == "primary_model"
|
|
@@ -73,9 +140,13 @@ def test_resource_file_item_accepts_optional_port_key():
|
|
|
73
140
|
"""ResourceFileItem should carry declared file-port identity when available."""
|
|
74
141
|
item = ResourceFileItem(
|
|
75
142
|
id="resource-1",
|
|
143
|
+
name="weather.epw",
|
|
76
144
|
type="energyplus_weather",
|
|
77
145
|
port_key="weather_file",
|
|
78
146
|
uri="gs://bucket/weather.epw",
|
|
147
|
+
size_bytes=456,
|
|
148
|
+
sha256="2" * 64,
|
|
149
|
+
storage_version="1700000000000001",
|
|
79
150
|
)
|
|
80
151
|
|
|
81
152
|
assert item.port_key == "weather_file"
|
|
@@ -89,10 +160,81 @@ def test_input_file_item_forbids_extra_fields():
|
|
|
89
160
|
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
90
161
|
role="primary-model",
|
|
91
162
|
uri="gs://bucket/model.idf",
|
|
163
|
+
size_bytes=123,
|
|
164
|
+
sha256="1" * 64,
|
|
165
|
+
storage_version="1700000000000000",
|
|
92
166
|
extra_field="not-allowed",
|
|
93
167
|
)
|
|
94
168
|
|
|
95
169
|
|
|
170
|
+
@pytest.mark.parametrize("missing", ["size_bytes", "sha256", "storage_version"])
|
|
171
|
+
def test_input_file_item_requires_every_integrity_field(missing):
|
|
172
|
+
"""A producer cannot emit a weaker file contract by omitting identity."""
|
|
173
|
+
payload = {
|
|
174
|
+
"name": "model.idf",
|
|
175
|
+
"mime_type": SupportedMimeType.ENERGYPLUS_IDF,
|
|
176
|
+
"role": "primary-model",
|
|
177
|
+
"uri": "gs://bucket/model.idf",
|
|
178
|
+
"size_bytes": 123,
|
|
179
|
+
"sha256": "1" * 64,
|
|
180
|
+
"storage_version": "1700000000000000",
|
|
181
|
+
}
|
|
182
|
+
payload.pop(missing)
|
|
183
|
+
|
|
184
|
+
with pytest.raises(ValidationError):
|
|
185
|
+
InputFileItem(**payload)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize("name", ["", ".", "..", "../model.idf", "a/b.idf", "a\\b.idf"])
|
|
189
|
+
def test_file_item_names_must_be_safe_leaf_names(name):
|
|
190
|
+
"""Logical names cannot redirect backend materialization outside its workspace."""
|
|
191
|
+
with pytest.raises(ValidationError):
|
|
192
|
+
InputFileItem(
|
|
193
|
+
name=name,
|
|
194
|
+
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
195
|
+
uri="gs://bucket/model.idf",
|
|
196
|
+
size_bytes=123,
|
|
197
|
+
sha256="1" * 64,
|
|
198
|
+
storage_version="1700000000000000",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_file_item_rejects_malformed_integrity_values():
|
|
203
|
+
"""Negative sizes, non-SHA digests, and empty versions fail at parsing."""
|
|
204
|
+
with pytest.raises(ValidationError):
|
|
205
|
+
InputFileItem(
|
|
206
|
+
name="model.idf",
|
|
207
|
+
mime_type=SupportedMimeType.ENERGYPLUS_IDF,
|
|
208
|
+
uri="gs://bucket/model.idf",
|
|
209
|
+
size_bytes=-1,
|
|
210
|
+
sha256="not-a-sha256",
|
|
211
|
+
storage_version="",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_validation_artifact_requires_byte_and_storage_identity():
|
|
216
|
+
"""Output artifacts use the same mandatory integrity contract as inputs."""
|
|
217
|
+
artifact = ValidationArtifact(
|
|
218
|
+
name="report.csv",
|
|
219
|
+
type="timeseries-csv",
|
|
220
|
+
mime_type="text/csv",
|
|
221
|
+
uri="gs://bucket/outputs/report.csv",
|
|
222
|
+
size_bytes=456,
|
|
223
|
+
sha256="2" * 64,
|
|
224
|
+
storage_version="1700000000000001",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
assert artifact.size_bytes == 456
|
|
228
|
+
assert artifact.sha256 == "2" * 64
|
|
229
|
+
|
|
230
|
+
with pytest.raises(ValidationError):
|
|
231
|
+
ValidationArtifact(
|
|
232
|
+
name="report.csv",
|
|
233
|
+
type="timeseries-csv",
|
|
234
|
+
uri="gs://bucket/outputs/report.csv",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
96
238
|
def test_validation_message_location_optional_fields():
|
|
97
239
|
"""MessageLocation should support optional fields."""
|
|
98
240
|
location = MessageLocation(
|
|
@@ -114,6 +256,7 @@ def test_validation_output_envelope_defaults_and_status_enum():
|
|
|
114
256
|
|
|
115
257
|
envelope = ValidationOutputEnvelope(
|
|
116
258
|
run_id="run-99",
|
|
259
|
+
**_output_identity_kwargs(),
|
|
117
260
|
validator={
|
|
118
261
|
"id": "val-1",
|
|
119
262
|
"type": ValidatorType.ENERGYPLUS,
|
|
@@ -136,6 +279,7 @@ def test_validation_output_envelope_rejects_invalid_status():
|
|
|
136
279
|
with pytest.raises(ValidationError):
|
|
137
280
|
ValidationOutputEnvelope(
|
|
138
281
|
run_id="run-100",
|
|
282
|
+
**_output_identity_kwargs(),
|
|
139
283
|
validator={
|
|
140
284
|
"id": "val-1",
|
|
141
285
|
"type": ValidatorType.ENERGYPLUS,
|
{validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/energyplus/envelopes.py
RENAMED
|
@@ -81,9 +81,9 @@ class EnergyPlusInputs(BaseModel):
|
|
|
81
81
|
Using a typed Pydantic model provides compile-time type checking and
|
|
82
82
|
runtime validation of all configuration parameters.
|
|
83
83
|
|
|
84
|
-
Note: The validator always returns a fixed set of output
|
|
84
|
+
Note: The validator always returns a fixed set of output values defined
|
|
85
85
|
in its catalog. Users don't need to specify which outputs they want -
|
|
86
|
-
they get all defined
|
|
86
|
+
they get all defined outputs and write assertions against the ones they
|
|
87
87
|
care about.
|
|
88
88
|
"""
|
|
89
89
|
|
|
@@ -47,12 +47,12 @@ class EnergyPlusSimulationMetrics(BaseModel):
|
|
|
47
47
|
"""
|
|
48
48
|
Extracted EnergyPlus simulation metrics.
|
|
49
49
|
|
|
50
|
-
These are the core output
|
|
51
|
-
Field names here are the canonical EnergyPlus
|
|
52
|
-
Validibot. The core Django app maps these names to
|
|
50
|
+
These are the core output values extracted from EnergyPlus simulation results.
|
|
51
|
+
Field names here are the canonical EnergyPlus output names expected by core
|
|
52
|
+
Validibot. The core Django app maps these names to StepIODefinition rows.
|
|
53
53
|
|
|
54
54
|
The validator extracts these values from the EnergyPlus SQL database
|
|
55
|
-
(eplusout.sql) and they become available as output
|
|
55
|
+
(eplusout.sql) and they become available as output values for assertions.
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
58
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -19,6 +19,7 @@ from typing import Any
|
|
|
19
19
|
from pydantic import BaseModel, Field
|
|
20
20
|
|
|
21
21
|
from validibot_shared.validations.envelopes import (
|
|
22
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
22
23
|
ExecutionContext,
|
|
23
24
|
InputFileItem,
|
|
24
25
|
SupportedMimeType,
|
|
@@ -128,9 +129,15 @@ def build_fmu_input_envelope(
|
|
|
128
129
|
step_id: str,
|
|
129
130
|
step_name: str | None,
|
|
130
131
|
fmu_uri: str,
|
|
132
|
+
fmu_size_bytes: int,
|
|
133
|
+
fmu_sha256: str,
|
|
134
|
+
fmu_storage_version: str,
|
|
131
135
|
input_values: dict[str, Any],
|
|
132
136
|
callback_url: str,
|
|
133
137
|
execution_bundle_uri: str,
|
|
138
|
+
execution_attempt_id: str,
|
|
139
|
+
step_run_id: str,
|
|
140
|
+
expected_output_uri: str,
|
|
134
141
|
simulation: FMUSimulationConfig | None = None,
|
|
135
142
|
output_variables: list[str] | None = None,
|
|
136
143
|
) -> FMUInputEnvelope:
|
|
@@ -146,6 +153,9 @@ def build_fmu_input_envelope(
|
|
|
146
153
|
step_id: Workflow step ID
|
|
147
154
|
step_name: Optional step name
|
|
148
155
|
fmu_uri: FMU storage URI (gs://... or local path in dev)
|
|
156
|
+
fmu_size_bytes: Expected exact FMU size and streaming ceiling
|
|
157
|
+
fmu_sha256: Expected SHA-256 of the FMU bytes
|
|
158
|
+
fmu_storage_version: Immutable provider version for the FMU object
|
|
149
159
|
input_values: Resolved inputs keyed by native FMU variable name
|
|
150
160
|
callback_url: URL to POST callback
|
|
151
161
|
execution_bundle_uri: Base URI/path for this run's files
|
|
@@ -160,6 +170,9 @@ def build_fmu_input_envelope(
|
|
|
160
170
|
mime_type=SupportedMimeType.FMU,
|
|
161
171
|
role="fmu",
|
|
162
172
|
uri=fmu_uri,
|
|
173
|
+
size_bytes=fmu_size_bytes,
|
|
174
|
+
sha256=fmu_sha256,
|
|
175
|
+
storage_version=fmu_storage_version,
|
|
163
176
|
)
|
|
164
177
|
]
|
|
165
178
|
|
|
@@ -170,6 +183,10 @@ def build_fmu_input_envelope(
|
|
|
170
183
|
)
|
|
171
184
|
|
|
172
185
|
context = ExecutionContext(
|
|
186
|
+
execution_attempt_id=execution_attempt_id,
|
|
187
|
+
step_run_id=step_run_id,
|
|
188
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
189
|
+
expected_output_uri=expected_output_uri,
|
|
173
190
|
callback_url=callback_url,
|
|
174
191
|
execution_bundle_uri=execution_bundle_uri,
|
|
175
192
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
FMU probe result models.
|
|
3
3
|
|
|
4
4
|
These models define the contract for FMU probing operations - extracting
|
|
5
|
-
metadata from modelDescription.xml. Probe results feed into
|
|
5
|
+
metadata from modelDescription.xml. Probe results feed into StepIODefinition
|
|
6
6
|
rows in the core Django app.
|
|
7
7
|
|
|
8
8
|
Probing is done in-process in the Django worker (not in containers) since
|
{validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/schematron/envelopes.py
RENAMED
|
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|
|
30
30
|
from pydantic import BaseModel, Field
|
|
31
31
|
|
|
32
32
|
from validibot_shared.validations.envelopes import (
|
|
33
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
33
34
|
ExecutionContext,
|
|
34
35
|
InputFileItem,
|
|
35
36
|
SupportedMimeType,
|
|
@@ -125,9 +126,9 @@ class SchematronFinding(BaseModel):
|
|
|
125
126
|
class SchematronOutputs(BaseModel):
|
|
126
127
|
"""Schematron results: engine status, SVRL summary, and provenance.
|
|
127
128
|
|
|
128
|
-
The
|
|
129
|
+
The output-value fields (``passed`` … ``engine``) mirror the catalog entries in
|
|
129
130
|
the Django Schematron ``ValidatorConfig`` — Django's
|
|
130
|
-
``
|
|
131
|
+
``extract_output_values`` pulls exactly those keys for CEL assertion
|
|
131
132
|
evaluation.
|
|
132
133
|
|
|
133
134
|
D9 contract: when ``engine_status != "ok"`` the run never evaluated the
|
|
@@ -153,7 +154,7 @@ class SchematronOutputs(BaseModel):
|
|
|
153
154
|
),
|
|
154
155
|
)
|
|
155
156
|
|
|
156
|
-
# ── o.*
|
|
157
|
+
# ── o.* output values (must stay aligned with the ValidatorConfig catalog) ──
|
|
157
158
|
passed: bool | None = Field(
|
|
158
159
|
default=None,
|
|
159
160
|
description=(
|
|
@@ -241,9 +242,15 @@ def build_schematron_input_envelope(
|
|
|
241
242
|
step_id: str,
|
|
242
243
|
step_name: str | None,
|
|
243
244
|
submission_uri: str,
|
|
245
|
+
submission_size_bytes: int,
|
|
246
|
+
submission_sha256: str,
|
|
247
|
+
submission_storage_version: str,
|
|
244
248
|
inputs: SchematronInputs,
|
|
245
249
|
callback_url: str,
|
|
246
250
|
execution_bundle_uri: str,
|
|
251
|
+
execution_attempt_id: str,
|
|
252
|
+
step_run_id: str,
|
|
253
|
+
expected_output_uri: str,
|
|
247
254
|
callback_id: str | None = None,
|
|
248
255
|
skip_callback: bool = False,
|
|
249
256
|
) -> SchematronInputEnvelope:
|
|
@@ -255,6 +262,9 @@ def build_schematron_input_envelope(
|
|
|
255
262
|
org_id / org_name: Organization identity.
|
|
256
263
|
workflow_id / step_id / step_name: Workflow context.
|
|
257
264
|
submission_uri: Storage URI for the XML submission (gs:// or file://).
|
|
265
|
+
submission_size_bytes: Expected exact submission size and byte ceiling.
|
|
266
|
+
submission_sha256: Expected SHA-256 of the XML submission.
|
|
267
|
+
submission_storage_version: Immutable provider version for the object.
|
|
258
268
|
inputs: Fully-resolved ``SchematronInputs`` (inline rules + limits).
|
|
259
269
|
callback_url: URL the container POSTs to on completion.
|
|
260
270
|
execution_bundle_uri: Base URI for this run's bundle.
|
|
@@ -267,10 +277,17 @@ def build_schematron_input_envelope(
|
|
|
267
277
|
mime_type=SupportedMimeType.APPLICATION_XML,
|
|
268
278
|
role="primary-model",
|
|
269
279
|
uri=submission_uri,
|
|
280
|
+
size_bytes=submission_size_bytes,
|
|
281
|
+
sha256=submission_sha256,
|
|
282
|
+
storage_version=submission_storage_version,
|
|
270
283
|
),
|
|
271
284
|
]
|
|
272
285
|
|
|
273
286
|
context = ExecutionContext(
|
|
287
|
+
execution_attempt_id=execution_attempt_id,
|
|
288
|
+
step_run_id=step_run_id,
|
|
289
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
290
|
+
expected_output_uri=expected_output_uri,
|
|
274
291
|
callback_id=callback_id,
|
|
275
292
|
callback_url=callback_url,
|
|
276
293
|
execution_bundle_uri=execution_bundle_uri,
|
|
@@ -15,7 +15,7 @@ These schemas define the contract between Django and the SHACL container:
|
|
|
15
15
|
the database (rulesets, ``RulesetAssertion`` rows, settings) before dispatch —
|
|
16
16
|
the container receives only this serialized boundary.
|
|
17
17
|
- **Output envelope**: the SHACL findings (as ``ValidationMessage`` rows), the
|
|
18
|
-
``o.*``
|
|
18
|
+
``o.*`` output-value dict (for CEL/Basic output assertions evaluated back in Django),
|
|
19
19
|
the serialized ``sh:ValidationReport`` for evidence download, and the
|
|
20
20
|
container-side SPARQL-ASK assertion counts (folded into the final assertion
|
|
21
21
|
totals by ``AdvancedValidator.post_execute_validate``).
|
|
@@ -31,6 +31,7 @@ from typing import Any
|
|
|
31
31
|
from pydantic import BaseModel, Field
|
|
32
32
|
|
|
33
33
|
from validibot_shared.validations.envelopes import (
|
|
34
|
+
ATTEMPT_CONTRACT_VERSION,
|
|
34
35
|
ExecutionContext,
|
|
35
36
|
InputFileItem,
|
|
36
37
|
SupportedMimeType,
|
|
@@ -192,11 +193,11 @@ class SHACLFinding(BaseModel):
|
|
|
192
193
|
|
|
193
194
|
|
|
194
195
|
class SHACLOutputs(BaseModel):
|
|
195
|
-
"""SHACL validation results and the ``o.*``
|
|
196
|
+
"""SHACL validation results and the ``o.*`` output-value dict.
|
|
196
197
|
|
|
197
|
-
The
|
|
198
|
+
The output-value fields (``parse_ok`` … ``shacl_total_count``) mirror the catalog
|
|
198
199
|
entries declared in the Django SHACL ``ValidatorConfig`` — Django's
|
|
199
|
-
``
|
|
200
|
+
``extract_output_values`` pulls exactly those keys for CEL/Basic assertion
|
|
200
201
|
evaluation. The remaining fields carry evidence (the serialized report) and
|
|
201
202
|
the container-side SPARQL-ASK assertion counts.
|
|
202
203
|
"""
|
|
@@ -212,7 +213,7 @@ class SHACLOutputs(BaseModel):
|
|
|
212
213
|
),
|
|
213
214
|
)
|
|
214
215
|
|
|
215
|
-
# ── o.*
|
|
216
|
+
# ── o.* output values (must stay aligned with the SHACL ValidatorConfig catalog) ──
|
|
216
217
|
parse_ok: bool = Field(description="Whether the submitted RDF parsed.")
|
|
217
218
|
parse_serialization: str = Field(description="rdflib format used to parse.")
|
|
218
219
|
triple_count: int = Field(default=0, ge=0)
|
|
@@ -289,9 +290,15 @@ def build_shacl_input_envelope(
|
|
|
289
290
|
step_id: str,
|
|
290
291
|
step_name: str | None,
|
|
291
292
|
submission_uri: str,
|
|
293
|
+
submission_size_bytes: int,
|
|
294
|
+
submission_sha256: str,
|
|
295
|
+
submission_storage_version: str,
|
|
292
296
|
inputs: SHACLInputs,
|
|
293
297
|
callback_url: str,
|
|
294
298
|
execution_bundle_uri: str,
|
|
299
|
+
execution_attempt_id: str,
|
|
300
|
+
step_run_id: str,
|
|
301
|
+
expected_output_uri: str,
|
|
295
302
|
callback_id: str | None = None,
|
|
296
303
|
skip_callback: bool = False,
|
|
297
304
|
) -> SHACLInputEnvelope:
|
|
@@ -303,6 +310,9 @@ def build_shacl_input_envelope(
|
|
|
303
310
|
org_id / org_name: Organization identity.
|
|
304
311
|
workflow_id / step_id / step_name: Workflow context.
|
|
305
312
|
submission_uri: Storage URI for the RDF submission (gs:// or file://).
|
|
313
|
+
submission_size_bytes: Expected exact submission size and byte ceiling.
|
|
314
|
+
submission_sha256: Expected SHA-256 of the RDF submission.
|
|
315
|
+
submission_storage_version: Immutable provider version for the object.
|
|
306
316
|
inputs: Fully-resolved ``SHACLInputs`` (shapes/ontology/settings/asks).
|
|
307
317
|
callback_url: URL the container POSTs to on completion.
|
|
308
318
|
execution_bundle_uri: Base URI for this run's bundle.
|
|
@@ -315,10 +325,17 @@ def build_shacl_input_envelope(
|
|
|
315
325
|
mime_type=mime_type_for_rdf_format(inputs.rdf_format),
|
|
316
326
|
role="primary-model",
|
|
317
327
|
uri=submission_uri,
|
|
328
|
+
size_bytes=submission_size_bytes,
|
|
329
|
+
sha256=submission_sha256,
|
|
330
|
+
storage_version=submission_storage_version,
|
|
318
331
|
),
|
|
319
332
|
]
|
|
320
333
|
|
|
321
334
|
context = ExecutionContext(
|
|
335
|
+
execution_attempt_id=execution_attempt_id,
|
|
336
|
+
step_run_id=step_run_id,
|
|
337
|
+
attempt_contract_version=ATTEMPT_CONTRACT_VERSION,
|
|
338
|
+
expected_output_uri=expected_output_uri,
|
|
322
339
|
callback_id=callback_id,
|
|
323
340
|
callback_url=callback_url,
|
|
324
341
|
execution_bundle_uri=execution_bundle_uri,
|
{validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/validations/artifacts.py
RENAMED
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from typing import Any, Literal
|
|
12
12
|
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
13
|
+
from pydantic import BaseModel, Field, field_validator
|
|
14
14
|
|
|
15
15
|
ARTIFACT_REF_SCHEMA_VERSION = "validibot.artifact_ref.v1"
|
|
16
16
|
|
|
@@ -134,11 +134,12 @@ class ArtifactRef(BaseModel):
|
|
|
134
134
|
media_type: str = ""
|
|
135
135
|
data_format: str = ""
|
|
136
136
|
filename: str = ""
|
|
137
|
-
size_bytes: int
|
|
138
|
-
sha256: str = ""
|
|
137
|
+
size_bytes: int = Field(ge=0)
|
|
138
|
+
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
139
|
+
storage_version: str = Field(min_length=1, max_length=512)
|
|
139
140
|
|
|
140
141
|
uri: str = Field(
|
|
141
|
-
|
|
142
|
+
min_length=1,
|
|
142
143
|
description="Internal storage URI; not a public download URL.",
|
|
143
144
|
)
|
|
144
145
|
manifest_uri: str = ""
|
|
@@ -153,3 +154,18 @@ class ArtifactRef(BaseModel):
|
|
|
153
154
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
154
155
|
|
|
155
156
|
model_config = {"extra": "forbid"}
|
|
157
|
+
|
|
158
|
+
@field_validator("name")
|
|
159
|
+
@classmethod
|
|
160
|
+
def _validate_safe_name(cls, value: str) -> str:
|
|
161
|
+
"""Artifact refs carry logical names, never materialization paths."""
|
|
162
|
+
if (
|
|
163
|
+
not value
|
|
164
|
+
or value in {".", ".."}
|
|
165
|
+
or "/" in value
|
|
166
|
+
or "\\" in value
|
|
167
|
+
or "\x00" in value
|
|
168
|
+
):
|
|
169
|
+
msg = f"Artifact name must be a safe logical leaf name: {value!r}"
|
|
170
|
+
raise ValueError(msg)
|
|
171
|
+
return value
|
{validibot_shared-0.14.0 → validibot_shared-0.16.0}/validibot_shared/validations/envelopes.py
RENAMED
|
@@ -117,7 +117,9 @@ from datetime import datetime # noqa: TC003
|
|
|
117
117
|
from enum import Enum
|
|
118
118
|
from typing import Any, Literal
|
|
119
119
|
|
|
120
|
-
from pydantic import BaseModel, Field, HttpUrl
|
|
120
|
+
from pydantic import BaseModel, Field, HttpUrl, field_validator
|
|
121
|
+
|
|
122
|
+
ATTEMPT_CONTRACT_VERSION = "validibot.attempt.v1"
|
|
121
123
|
|
|
122
124
|
# ==============================================================================
|
|
123
125
|
# Shared Enums
|
|
@@ -186,7 +188,52 @@ class SupportedMimeType(str, Enum):
|
|
|
186
188
|
RDF_N_QUADS = "application/n-quads"
|
|
187
189
|
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
def _safe_leaf_name(value: str) -> str:
|
|
192
|
+
"""Return ``value`` when it is one safe logical filename.
|
|
193
|
+
|
|
194
|
+
Storage URIs carry provider paths; item names are deliberately only leaf
|
|
195
|
+
names so a backend can materialize them below its own attempt workspace.
|
|
196
|
+
"""
|
|
197
|
+
if (
|
|
198
|
+
not value
|
|
199
|
+
or value in {".", ".."}
|
|
200
|
+
or "/" in value
|
|
201
|
+
or "\\" in value
|
|
202
|
+
or "\x00" in value
|
|
203
|
+
):
|
|
204
|
+
msg = f"File item name must be a safe logical leaf name: {value!r}"
|
|
205
|
+
raise ValueError(msg)
|
|
206
|
+
return value
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class IntegrityBoundFile(BaseModel):
|
|
210
|
+
"""Required byte identity shared by every file-bearing envelope item.
|
|
211
|
+
|
|
212
|
+
``size_bytes`` is both the expected exact length and the runtime's hard
|
|
213
|
+
streaming ceiling. ``sha256`` identifies the bytes the producer committed
|
|
214
|
+
to. ``storage_version`` pins the provider-specific immutable object
|
|
215
|
+
identity (for example a GCS generation or ``sha256:<digest>`` for an
|
|
216
|
+
attempt-local read-only file).
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
size_bytes: int = Field(
|
|
220
|
+
ge=0,
|
|
221
|
+
description="Expected exact file size and hard streaming ceiling.",
|
|
222
|
+
)
|
|
223
|
+
sha256: str = Field(
|
|
224
|
+
pattern=r"^[0-9a-f]{64}$",
|
|
225
|
+
description="Lowercase SHA-256 hex digest of the exact file bytes.",
|
|
226
|
+
)
|
|
227
|
+
storage_version: str = Field(
|
|
228
|
+
min_length=1,
|
|
229
|
+
max_length=512,
|
|
230
|
+
description="Provider-specific immutable object/version identity.",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
model_config = {"extra": "forbid"}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class InputFileItem(IntegrityBoundFile):
|
|
190
237
|
"""
|
|
191
238
|
A user-submitted file input for the validator.
|
|
192
239
|
|
|
@@ -196,7 +243,7 @@ class InputFileItem(BaseModel):
|
|
|
196
243
|
Note: Auxiliary files like weather files are in resource_files, not input_files.
|
|
197
244
|
"""
|
|
198
245
|
|
|
199
|
-
name: str = Field(description="
|
|
246
|
+
name: str = Field(description="Safe logical leaf name of the file")
|
|
200
247
|
|
|
201
248
|
mime_type: SupportedMimeType = Field(description="MIME type of the file")
|
|
202
249
|
|
|
@@ -209,7 +256,7 @@ class InputFileItem(BaseModel):
|
|
|
209
256
|
default=None,
|
|
210
257
|
description=(
|
|
211
258
|
"Declared Validibot file-port key that produced this envelope item "
|
|
212
|
-
"(e.g., 'primary_model').
|
|
259
|
+
"(e.g., 'primary_model')."
|
|
213
260
|
),
|
|
214
261
|
)
|
|
215
262
|
|
|
@@ -217,10 +264,14 @@ class InputFileItem(BaseModel):
|
|
|
217
264
|
description="Storage URI to the file (gs:// or file:// for self-hosted)"
|
|
218
265
|
)
|
|
219
266
|
|
|
220
|
-
|
|
267
|
+
@field_validator("name")
|
|
268
|
+
@classmethod
|
|
269
|
+
def _validate_safe_name(cls, value: str) -> str:
|
|
270
|
+
"""Reject paths so runtimes choose the destination directory."""
|
|
271
|
+
return _safe_leaf_name(value)
|
|
221
272
|
|
|
222
273
|
|
|
223
|
-
class ResourceFileItem(
|
|
274
|
+
class ResourceFileItem(IntegrityBoundFile):
|
|
224
275
|
"""
|
|
225
276
|
A resource file needed by the validator.
|
|
226
277
|
|
|
@@ -236,6 +287,8 @@ class ResourceFileItem(BaseModel):
|
|
|
236
287
|
|
|
237
288
|
id: str = Field(description="Resource file UUID from Django database")
|
|
238
289
|
|
|
290
|
+
name: str = Field(description="Safe logical leaf name of the resource")
|
|
291
|
+
|
|
239
292
|
type: str = Field(
|
|
240
293
|
description="Resource type (e.g., 'weather', 'library', 'config')"
|
|
241
294
|
)
|
|
@@ -244,7 +297,7 @@ class ResourceFileItem(BaseModel):
|
|
|
244
297
|
default=None,
|
|
245
298
|
description=(
|
|
246
299
|
"Declared Validibot file-port key that produced this resource item "
|
|
247
|
-
"(e.g., 'weather_file').
|
|
300
|
+
"(e.g., 'weather_file')."
|
|
248
301
|
),
|
|
249
302
|
)
|
|
250
303
|
|
|
@@ -252,6 +305,12 @@ class ResourceFileItem(BaseModel):
|
|
|
252
305
|
description="Storage URI to the file (gs:// or file:// for self-hosted)"
|
|
253
306
|
)
|
|
254
307
|
|
|
308
|
+
@field_validator("name")
|
|
309
|
+
@classmethod
|
|
310
|
+
def _validate_safe_name(cls, value: str) -> str:
|
|
311
|
+
"""Reject resource names that could escape the runtime workspace."""
|
|
312
|
+
return _safe_leaf_name(value)
|
|
313
|
+
|
|
255
314
|
model_config = {"extra": "forbid"}
|
|
256
315
|
|
|
257
316
|
|
|
@@ -326,6 +385,25 @@ class ExecutionContext(BaseModel):
|
|
|
326
385
|
4. Respect timeout constraints (timeout_seconds)
|
|
327
386
|
"""
|
|
328
387
|
|
|
388
|
+
execution_attempt_id: str = Field(
|
|
389
|
+
min_length=1,
|
|
390
|
+
description="ExecutionAttempt UUID selected by the trusted application.",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
step_run_id: str = Field(
|
|
394
|
+
min_length=1,
|
|
395
|
+
description="ValidationStepRun identity this attempt executes.",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
attempt_contract_version: Literal["validibot.attempt.v1"] = Field(
|
|
399
|
+
description="Strict attempt I/O contract version.",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
expected_output_uri: str = Field(
|
|
403
|
+
min_length=1,
|
|
404
|
+
description="Exact output-envelope identity selected before dispatch.",
|
|
405
|
+
)
|
|
406
|
+
|
|
329
407
|
callback_id: str | None = Field(
|
|
330
408
|
default=None,
|
|
331
409
|
description=(
|
|
@@ -498,10 +576,10 @@ class ValidationMetric(BaseModel):
|
|
|
498
576
|
model_config = {"extra": "forbid"}
|
|
499
577
|
|
|
500
578
|
|
|
501
|
-
class ValidationArtifact(
|
|
579
|
+
class ValidationArtifact(IntegrityBoundFile):
|
|
502
580
|
"""A file artifact produced by the validator."""
|
|
503
581
|
|
|
504
|
-
name: str = Field(description="
|
|
582
|
+
name: str = Field(description="Safe logical artifact leaf name")
|
|
505
583
|
|
|
506
584
|
type: str = Field(
|
|
507
585
|
description=(
|
|
@@ -517,9 +595,11 @@ class ValidationArtifact(BaseModel):
|
|
|
517
595
|
description="Storage URI to the artifact (gs:// or file:// for self-hosted)"
|
|
518
596
|
)
|
|
519
597
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
598
|
+
@field_validator("name")
|
|
599
|
+
@classmethod
|
|
600
|
+
def _validate_safe_name(cls, value: str) -> str:
|
|
601
|
+
"""Reject artifact names that could be interpreted as paths."""
|
|
602
|
+
return _safe_leaf_name(value)
|
|
523
603
|
|
|
524
604
|
|
|
525
605
|
class RawOutputs(BaseModel):
|
|
@@ -617,6 +697,30 @@ class ValidationOutputEnvelope(BaseModel):
|
|
|
617
697
|
|
|
618
698
|
run_id: str = Field(description="Unique run identifier (matches input)")
|
|
619
699
|
|
|
700
|
+
step_run_id: str = Field(
|
|
701
|
+
min_length=1,
|
|
702
|
+
description="ValidationStepRun identity echoed from the input contract.",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
execution_attempt_id: str = Field(
|
|
706
|
+
min_length=1,
|
|
707
|
+
description="ExecutionAttempt identity echoed from the input contract.",
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
attempt_contract_version: Literal["validibot.attempt.v1"] = Field(
|
|
711
|
+
description="Strict attempt I/O contract version echoed from input.",
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
input_envelope_sha256: str = Field(
|
|
715
|
+
pattern=r"^[0-9a-f]{64}$",
|
|
716
|
+
description="Canonical SHA-256 of the exact parsed input envelope.",
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
output_uri: str = Field(
|
|
720
|
+
min_length=1,
|
|
721
|
+
description="Exact output-envelope URI selected by the input contract.",
|
|
722
|
+
)
|
|
723
|
+
|
|
620
724
|
validator: ValidatorInfo
|
|
621
725
|
|
|
622
726
|
status: ValidationStatus
|
|
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
|
|
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.14.0 → validibot_shared-0.16.0}/validibot_shared/validations/__init__.py
RENAMED
|
File without changes
|