validibot-shared 0.1.0__py3-none-any.whl
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/__init__.py +16 -0
- validibot_shared/energyplus/__init__.py +25 -0
- validibot_shared/energyplus/envelopes.py +300 -0
- validibot_shared/energyplus/models.py +140 -0
- validibot_shared/fmi/__init__.py +6 -0
- validibot_shared/fmi/envelopes.py +190 -0
- validibot_shared/fmi/models.py +70 -0
- validibot_shared/py.typed +0 -0
- validibot_shared/validations/__init__.py +0 -0
- validibot_shared/validations/envelopes.py +607 -0
- validibot_shared-0.1.0.dist-info/METADATA +168 -0
- validibot_shared-0.1.0.dist-info/RECORD +15 -0
- validibot_shared-0.1.0.dist-info/WHEEL +4 -0
- validibot_shared-0.1.0.dist-info/licenses/LICENSE +21 -0
- validibot_shared-0.1.0.dist-info/licenses/NOTICE +23 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FMI probe result models.
|
|
3
|
+
|
|
4
|
+
These models define the contract for FMU probing operations - extracting
|
|
5
|
+
metadata from modelDescription.xml to populate validator catalog entries.
|
|
6
|
+
|
|
7
|
+
Probing is done in-process in the Django worker (not in containers) since
|
|
8
|
+
it's just XML parsing with no FMU execution.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FMIVariableMeta(BaseModel):
|
|
19
|
+
"""Metadata extracted from modelDescription.xml."""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(extra="forbid")
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
causality: str
|
|
25
|
+
variability: str | None = None
|
|
26
|
+
value_reference: int = 0
|
|
27
|
+
value_type: str
|
|
28
|
+
unit: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FMIProbeResult(BaseModel):
|
|
32
|
+
"""Result of a short probe run to vet an FMU before approval."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="forbid")
|
|
35
|
+
|
|
36
|
+
status: Literal["success", "error"] = "error"
|
|
37
|
+
variables: list[FMIVariableMeta] = Field(default_factory=list)
|
|
38
|
+
errors: list[str] = Field(default_factory=list)
|
|
39
|
+
messages: list[str] = Field(default_factory=list)
|
|
40
|
+
execution_seconds: float | None = None
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def success(
|
|
44
|
+
cls,
|
|
45
|
+
*,
|
|
46
|
+
variables: list[FMIVariableMeta],
|
|
47
|
+
execution_seconds: float | None = None,
|
|
48
|
+
messages: list[str] | None = None,
|
|
49
|
+
) -> FMIProbeResult:
|
|
50
|
+
return cls(
|
|
51
|
+
status="success",
|
|
52
|
+
variables=variables,
|
|
53
|
+
execution_seconds=execution_seconds,
|
|
54
|
+
messages=messages or [],
|
|
55
|
+
errors=[],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def failure(
|
|
60
|
+
cls,
|
|
61
|
+
*,
|
|
62
|
+
errors: list[str],
|
|
63
|
+
messages: list[str] | None = None,
|
|
64
|
+
) -> FMIProbeResult:
|
|
65
|
+
return cls(
|
|
66
|
+
status="error",
|
|
67
|
+
variables=[],
|
|
68
|
+
errors=errors,
|
|
69
|
+
messages=messages or [],
|
|
70
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schemas for Advanced validator job execution envelopes.
|
|
3
|
+
|
|
4
|
+
These schemas define the contract between Django and Advanced validator
|
|
5
|
+
containers/services. Advanced validators are validations that run in separate
|
|
6
|
+
containers or remote services (e.g., EnergyPlus, FMI), as opposed to Simple
|
|
7
|
+
validators that run directly within Django.
|
|
8
|
+
|
|
9
|
+
This library is used by both the Django app and Advanced validator services to ensure
|
|
10
|
+
type safety and contract consistency across the network boundary.
|
|
11
|
+
|
|
12
|
+
## Deployment Modes
|
|
13
|
+
|
|
14
|
+
This module supports multiple deployment modes:
|
|
15
|
+
|
|
16
|
+
1. **GCP deployment**: Uses gs:// URIs for Google Cloud Storage
|
|
17
|
+
2. **Self-hosted Docker** (default): Uses file:// URIs for local filesystem
|
|
18
|
+
|
|
19
|
+
The envelope schemas are storage-agnostic - they accept any valid URI. The
|
|
20
|
+
actual storage handling is done by the validators' storage_client module.
|
|
21
|
+
|
|
22
|
+
## Architecture Overview
|
|
23
|
+
|
|
24
|
+
This module provides a type-safe interface for communication between the Django
|
|
25
|
+
app and Advanced validator containers:
|
|
26
|
+
|
|
27
|
+
**GCP Mode (async with callbacks):**
|
|
28
|
+
1. Django creates input.json with files, config, callback URL
|
|
29
|
+
2. Django uploads to GCS and triggers validator container
|
|
30
|
+
3. Validator downloads inputs, runs validation, uploads outputs
|
|
31
|
+
4. Validator POSTs minimal callback to Django when complete
|
|
32
|
+
5. Django receives callback and loads full output.json from GCS
|
|
33
|
+
|
|
34
|
+
**Self-hosted Mode (sync execution):**
|
|
35
|
+
1. Django creates input.json with files, config
|
|
36
|
+
2. Django uploads to local storage and runs Docker container synchronously
|
|
37
|
+
3. Validator reads inputs, runs validation, writes outputs to local storage
|
|
38
|
+
4. Docker container exits, Django reads output.json directly
|
|
39
|
+
|
|
40
|
+
## Why Two Separate Fields: input_files vs inputs?
|
|
41
|
+
|
|
42
|
+
- **input_files**: Generic list of GCS file URIs (IDF, EPW, FMU, XML, etc.)
|
|
43
|
+
- NEVER changes in subclasses - all validators need file inputs
|
|
44
|
+
- Role field distinguishes file purposes (e.g., 'primary-model' vs 'weather')
|
|
45
|
+
|
|
46
|
+
- **inputs**: Domain-specific configuration parameters
|
|
47
|
+
- Base class uses dict[str, Any] for flexibility
|
|
48
|
+
- Subclasses override with typed Pydantic models (e.g., EnergyPlusInputs)
|
|
49
|
+
- Examples: timestep settings, output variables, simulation options
|
|
50
|
+
|
|
51
|
+
This separation keeps file handling generic while allowing type-safe config.
|
|
52
|
+
|
|
53
|
+
## Why Separate outputs Field?
|
|
54
|
+
|
|
55
|
+
- **messages/metrics/artifacts**: Generic outputs (errors, warnings, values)
|
|
56
|
+
- Available on all validators for consistent reporting
|
|
57
|
+
|
|
58
|
+
- **outputs**: Domain-specific detailed results
|
|
59
|
+
- Base class uses dict[str, Any] | None for flexibility
|
|
60
|
+
- Subclasses override with typed models (e.g., EnergyPlusOutputs
|
|
61
|
+
with returncode, logs, file paths)
|
|
62
|
+
- Optional because not all validators produce detailed domain-specific data
|
|
63
|
+
|
|
64
|
+
## Subclassing Pattern
|
|
65
|
+
|
|
66
|
+
Domain-specific validators (energyplus, fmi, xml) create typed subclasses:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# In energyplus/envelopes.py
|
|
70
|
+
class EnergyPlusInputs(BaseModel):
|
|
71
|
+
timestep_per_hour: int = 4
|
|
72
|
+
invocation_mode: Literal["python_api", "cli"] = "cli"
|
|
73
|
+
# ... other EnergyPlus-specific config
|
|
74
|
+
|
|
75
|
+
class EnergyPlusInputEnvelope(ValidationInputEnvelope):
|
|
76
|
+
inputs: EnergyPlusInputs # Override dict[str, Any] with typed version!
|
|
77
|
+
|
|
78
|
+
class EnergyPlusOutputs(BaseModel):
|
|
79
|
+
energyplus_returncode: int
|
|
80
|
+
execution_seconds: float
|
|
81
|
+
# ... other EnergyPlus-specific results
|
|
82
|
+
|
|
83
|
+
class EnergyPlusOutputEnvelope(ValidationOutputEnvelope):
|
|
84
|
+
outputs: EnergyPlusOutputs # Override dict[str, Any] with typed version!
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Django deserializes using the correct subclass based on validator.type.
|
|
88
|
+
|
|
89
|
+
## Why ValidationCallback?
|
|
90
|
+
|
|
91
|
+
The callback is a minimal async notification sent from the validator container
|
|
92
|
+
back to the Django app when work completes. It contains only:
|
|
93
|
+
- run_id: Which job finished
|
|
94
|
+
- status: success/failed_validation/failed_runtime/cancelled
|
|
95
|
+
- result_uri: Storage path to full output.json
|
|
96
|
+
|
|
97
|
+
This avoids redundant data transfer since the full output.json is already in storage.
|
|
98
|
+
The callback enables async execution (validator POSTs when done) instead of the
|
|
99
|
+
Django app having to poll job status repeatedly.
|
|
100
|
+
|
|
101
|
+
See docs/adr/2025-12-04-validator-job-interface.md in the validibot repository
|
|
102
|
+
for the full specification.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
from __future__ import annotations
|
|
106
|
+
|
|
107
|
+
from datetime import datetime # noqa: TC003
|
|
108
|
+
from enum import Enum
|
|
109
|
+
from typing import Any, Literal
|
|
110
|
+
|
|
111
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
112
|
+
|
|
113
|
+
# ==============================================================================
|
|
114
|
+
# Shared Enums
|
|
115
|
+
# ==============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Severity(str, Enum):
|
|
119
|
+
"""Severity level for validation messages."""
|
|
120
|
+
|
|
121
|
+
INFO = "INFO"
|
|
122
|
+
WARNING = "WARNING"
|
|
123
|
+
ERROR = "ERROR"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ValidatorType(str, Enum):
|
|
127
|
+
"""
|
|
128
|
+
Canonical validator types used across Django and validator containers.
|
|
129
|
+
|
|
130
|
+
These values align with Django's ValidationType TextChoices and should be
|
|
131
|
+
used anywhere we serialize validator identifiers in envelopes.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
BASIC = "BASIC"
|
|
135
|
+
JSON_SCHEMA = "JSON_SCHEMA"
|
|
136
|
+
XML_SCHEMA = "XML_SCHEMA"
|
|
137
|
+
ENERGYPLUS = "ENERGYPLUS"
|
|
138
|
+
FMI = "FMI"
|
|
139
|
+
CUSTOM_VALIDATOR = "CUSTOM_VALIDATOR"
|
|
140
|
+
AI_ASSIST = "AI_ASSIST"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ==============================================================================
|
|
144
|
+
# Input Envelope (validibot.input.v1)
|
|
145
|
+
# ==============================================================================
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SupportedMimeType(str, Enum):
|
|
149
|
+
"""
|
|
150
|
+
Supported MIME types for file inputs.
|
|
151
|
+
|
|
152
|
+
We only accept specific file types that our validators know how to process.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
# XML documents
|
|
156
|
+
APPLICATION_XML = "application/xml"
|
|
157
|
+
TEXT_XML = "text/xml"
|
|
158
|
+
|
|
159
|
+
# EnergyPlus files
|
|
160
|
+
ENERGYPLUS_IDF = "application/vnd.energyplus.idf" # IDF text format
|
|
161
|
+
ENERGYPLUS_EPJSON = "application/vnd.energyplus.epjson" # epJSON format
|
|
162
|
+
ENERGYPLUS_EPW = "application/vnd.energyplus.epw" # Weather data
|
|
163
|
+
|
|
164
|
+
# FMU files
|
|
165
|
+
FMU = "application/vnd.fmi.fmu" # Functional Mock-up Unit
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class InputFileItem(BaseModel):
|
|
169
|
+
"""
|
|
170
|
+
A file input for the validator.
|
|
171
|
+
|
|
172
|
+
Files are stored in GCS and referenced by URI. The 'role' field allows
|
|
173
|
+
validators to understand what each file is for (e.g., 'primary-model' vs
|
|
174
|
+
'weather' for EnergyPlus).
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
name: str = Field(description="Human-readable name of the file")
|
|
178
|
+
|
|
179
|
+
mime_type: SupportedMimeType = Field(description="MIME type of the file")
|
|
180
|
+
|
|
181
|
+
role: str | None = Field(
|
|
182
|
+
default=None,
|
|
183
|
+
description=(
|
|
184
|
+
"Validator-specific role (e.g., 'primary-model', 'weather', 'config')"
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
uri: str = Field(
|
|
189
|
+
description="Storage URI to the file (gs:// or file:// for self-hosted)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
model_config = {"extra": "forbid"}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ValidatorInfo(BaseModel):
|
|
196
|
+
"""
|
|
197
|
+
Information about the validator being executed.
|
|
198
|
+
|
|
199
|
+
This identifies which validator container to run and which version.
|
|
200
|
+
The 'type' field determines:
|
|
201
|
+
1. Which validator container to run (e.g., 'validibot-validator-energyplus')
|
|
202
|
+
2. Which envelope subclass Django uses for deserialization
|
|
203
|
+
(EnergyPlusInputEnvelope, FMIInputEnvelope, etc.)
|
|
204
|
+
|
|
205
|
+
This class appears in both input and output envelopes to maintain traceability
|
|
206
|
+
of which validator version produced which results.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
id: str = Field(description="Validator UUID from Django database")
|
|
210
|
+
|
|
211
|
+
type: ValidatorType = Field(
|
|
212
|
+
description="Validator type (e.g., 'ENERGYPLUS', 'FMI', 'JSON_SCHEMA')"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
version: str = Field(description="Validator version (e.g., '1.0.0')")
|
|
216
|
+
|
|
217
|
+
model_config = {"extra": "forbid"}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class OrganizationInfo(BaseModel):
|
|
221
|
+
"""
|
|
222
|
+
Information about the organization running the validation.
|
|
223
|
+
|
|
224
|
+
Included for logging, debugging, and future multi-tenancy features.
|
|
225
|
+
The name is redundant with ID but helpful for human-readable logs.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
id: str = Field(description="Organization UUID from Django database")
|
|
229
|
+
|
|
230
|
+
name: str = Field(description="Organization name (for human-readable logs)")
|
|
231
|
+
|
|
232
|
+
model_config = {"extra": "forbid"}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class WorkflowInfo(BaseModel):
|
|
236
|
+
"""
|
|
237
|
+
Information about the workflow and step being executed.
|
|
238
|
+
|
|
239
|
+
Validators are executed as steps within larger workflows. This metadata
|
|
240
|
+
enables tracing validation results back to their workflow context for:
|
|
241
|
+
- Debugging (which workflow triggered this validation?)
|
|
242
|
+
- Auditing (track all validations in a workflow)
|
|
243
|
+
- UI display (show validation status within workflow visualization)
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
id: str = Field(description="Workflow UUID")
|
|
247
|
+
|
|
248
|
+
step_id: str = Field(description="Workflow step UUID")
|
|
249
|
+
|
|
250
|
+
step_name: str | None = Field(default=None, description="Human-readable step name")
|
|
251
|
+
|
|
252
|
+
model_config = {"extra": "forbid"}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ExecutionContext(BaseModel):
|
|
256
|
+
"""
|
|
257
|
+
Execution context and callback information.
|
|
258
|
+
|
|
259
|
+
This provides the validator container with everything it needs to:
|
|
260
|
+
1. Download input files from storage (execution_bundle_uri)
|
|
261
|
+
2. Upload output files to storage (execution_bundle_uri)
|
|
262
|
+
3. Notify Django when complete (callback_url)
|
|
263
|
+
4. Respect timeout constraints (timeout_seconds)
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
callback_id: str | None = Field(
|
|
267
|
+
default=None,
|
|
268
|
+
description=(
|
|
269
|
+
"Unique identifier for this callback, used for idempotency. "
|
|
270
|
+
"Generated at job launch and echoed back in the callback payload. "
|
|
271
|
+
"The callback handler uses this to detect and ignore duplicate deliveries."
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
callback_url: HttpUrl | None = Field(
|
|
276
|
+
default=None,
|
|
277
|
+
description="URL to POST callback when validation completes",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
skip_callback: bool = Field(
|
|
281
|
+
default=False,
|
|
282
|
+
description=(
|
|
283
|
+
"If True, skip POSTing callback to callback_url after completion. "
|
|
284
|
+
"Useful for testing where polling GCS for output.json is preferred."
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
execution_bundle_uri: str = Field(
|
|
289
|
+
description="Storage URI to the execution bundle directory (gs:// or file://)"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
timeout_seconds: int = Field(
|
|
293
|
+
default=3600, description="Maximum execution time in seconds"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
tags: list[str] = Field(default_factory=list, description="Execution tags")
|
|
297
|
+
|
|
298
|
+
model_config = {"extra": "forbid"}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ValidationInputEnvelope(BaseModel):
|
|
302
|
+
"""
|
|
303
|
+
Base input envelope for validator jobs (validibot.input.v1).
|
|
304
|
+
|
|
305
|
+
This is written to storage as input.json by Django before triggering
|
|
306
|
+
the validator container.
|
|
307
|
+
|
|
308
|
+
## How Subclassing Works
|
|
309
|
+
|
|
310
|
+
Domain-specific validators create subclasses that override the 'inputs' field
|
|
311
|
+
with a typed Pydantic model. The base class uses dict[str, Any] for flexibility,
|
|
312
|
+
but subclasses provide type safety:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class EnergyPlusInputEnvelope(ValidationInputEnvelope):
|
|
316
|
+
inputs: EnergyPlusInputs # Typed override!
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This pattern allows:
|
|
320
|
+
- Generic base class for all validators (this class)
|
|
321
|
+
- Type-safe domain-specific subclasses (EnergyPlusInputEnvelope,
|
|
322
|
+
FMIInputEnvelope, etc.)
|
|
323
|
+
- Django to serialize/deserialize using the correct subclass based on
|
|
324
|
+
validator.type
|
|
325
|
+
|
|
326
|
+
## Fields That Never Change in Subclasses
|
|
327
|
+
|
|
328
|
+
- input_files: All validators receive files the same way
|
|
329
|
+
- context: All validators use the same execution context
|
|
330
|
+
- validator/org/workflow: All validators need this metadata
|
|
331
|
+
|
|
332
|
+
## Fields That Subclasses Override
|
|
333
|
+
|
|
334
|
+
- inputs: Domain-specific configuration (dict[str, Any] → EnergyPlusInputs, etc.)
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
schema_version: Literal["validibot.input.v1"] = "validibot.input.v1"
|
|
338
|
+
|
|
339
|
+
run_id: str = Field(description="Unique run identifier (UUID)")
|
|
340
|
+
|
|
341
|
+
validator: ValidatorInfo
|
|
342
|
+
|
|
343
|
+
org: OrganizationInfo
|
|
344
|
+
|
|
345
|
+
workflow: WorkflowInfo
|
|
346
|
+
|
|
347
|
+
input_files: list[InputFileItem] = Field(
|
|
348
|
+
default_factory=list,
|
|
349
|
+
description="File inputs for the validator (GCS URIs with roles)",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
inputs: dict[str, Any] = Field(
|
|
353
|
+
default_factory=dict,
|
|
354
|
+
description=("Domain-specific inputs (subclasses override with typed model)"),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
context: ExecutionContext
|
|
358
|
+
|
|
359
|
+
model_config = {"extra": "forbid"}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ==============================================================================
|
|
363
|
+
# Output Envelope (validibot.output.v1)
|
|
364
|
+
# ==============================================================================
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ValidationStatus(str, Enum):
|
|
368
|
+
"""Validation output status."""
|
|
369
|
+
|
|
370
|
+
SUCCESS = "success" # Validation completed successfully, no errors
|
|
371
|
+
FAILED_VALIDATION = "failed_validation" # Validation found errors (user's fault)
|
|
372
|
+
FAILED_RUNTIME = "failed_runtime" # Runtime error in validator (system fault)
|
|
373
|
+
CANCELLED = "cancelled" # User or system cancelled the job
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class MessageLocation(BaseModel):
|
|
377
|
+
"""Location information for a validation message."""
|
|
378
|
+
|
|
379
|
+
file_role: str | None = Field(
|
|
380
|
+
default=None, description="Input file role (references InputFileItem.role)"
|
|
381
|
+
)
|
|
382
|
+
line: int | None = Field(default=None, description="Line number")
|
|
383
|
+
column: int | None = Field(default=None, description="Column number")
|
|
384
|
+
path: str | None = Field(
|
|
385
|
+
default=None, description="Object path or XPath-like identifier"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
model_config = {"extra": "forbid"}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class ValidationMessage(BaseModel):
|
|
392
|
+
"""A validation finding, warning, or error."""
|
|
393
|
+
|
|
394
|
+
severity: Severity
|
|
395
|
+
|
|
396
|
+
code: str | None = Field(
|
|
397
|
+
default=None, description="Error code (e.g., 'EP001', 'FMU_INIT_ERROR')"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
text: str = Field(description="Human-readable message")
|
|
401
|
+
|
|
402
|
+
location: MessageLocation | None = Field(
|
|
403
|
+
default=None, description="Location of the issue"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
tags: list[str] = Field(default_factory=list, description="Message tags/categories")
|
|
407
|
+
|
|
408
|
+
model_config = {"extra": "forbid"}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class ValidationMetric(BaseModel):
|
|
412
|
+
"""A computed metric from the validation/simulation."""
|
|
413
|
+
|
|
414
|
+
name: str = Field(description="Metric name (e.g., 'zone_temp_max')")
|
|
415
|
+
|
|
416
|
+
value: float | int | str = Field(description="Metric value")
|
|
417
|
+
|
|
418
|
+
unit: str | None = Field(default=None, description="Unit (e.g., 'C', 'kWh', 'm2')")
|
|
419
|
+
|
|
420
|
+
category: str | None = Field(
|
|
421
|
+
default=None, description="Category (e.g., 'comfort', 'energy', 'performance')"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
tags: list[str] = Field(default_factory=list, description="Metric tags")
|
|
425
|
+
|
|
426
|
+
model_config = {"extra": "forbid"}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class ValidationArtifact(BaseModel):
|
|
430
|
+
"""A file artifact produced by the validator."""
|
|
431
|
+
|
|
432
|
+
name: str = Field(description="Artifact name")
|
|
433
|
+
|
|
434
|
+
type: str = Field(
|
|
435
|
+
description=(
|
|
436
|
+
"Artifact type (e.g., 'simulation-db', 'report-html', 'timeseries-csv')"
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
mime_type: str | None = Field(
|
|
441
|
+
default=None, description="MIME type (e.g., 'application/x-sqlite3')"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
uri: str = Field(
|
|
445
|
+
description="Storage URI to the artifact (gs:// or file:// for self-hosted)"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
size_bytes: int | None = Field(default=None, description="File size in bytes")
|
|
449
|
+
|
|
450
|
+
model_config = {"extra": "forbid"}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class RawOutputs(BaseModel):
|
|
454
|
+
"""Information about raw output files."""
|
|
455
|
+
|
|
456
|
+
format: Literal["directory", "archive"] = Field(
|
|
457
|
+
description="Format of raw outputs (directory or archive)"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
manifest_uri: str = Field(
|
|
461
|
+
description="Storage URI to the manifest file (gs:// or file://)"
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
model_config = {"extra": "forbid"}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class ValidationTiming(BaseModel):
|
|
468
|
+
"""Timing information for the validation run."""
|
|
469
|
+
|
|
470
|
+
queued_at: datetime | None = Field(
|
|
471
|
+
default=None, description="When the job was queued (ISO8601)"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
started_at: datetime | None = Field(
|
|
475
|
+
default=None, description="When execution started (ISO8601)"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
finished_at: datetime | None = Field(
|
|
479
|
+
default=None, description="When execution finished (ISO8601)"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
model_config = {"extra": "forbid"}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class ValidationOutputEnvelope(BaseModel):
|
|
486
|
+
"""
|
|
487
|
+
Base output envelope for validator jobs (validibot.output.v1).
|
|
488
|
+
|
|
489
|
+
This is written to storage as output.json by the validator container
|
|
490
|
+
after completion.
|
|
491
|
+
|
|
492
|
+
## How Subclassing Works
|
|
493
|
+
|
|
494
|
+
Domain-specific validators create subclasses that override the 'outputs' field
|
|
495
|
+
with a typed Pydantic model containing detailed domain-specific results:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
class EnergyPlusOutputEnvelope(ValidationOutputEnvelope):
|
|
499
|
+
outputs: EnergyPlusOutputs # Typed override!
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
This pattern allows:
|
|
503
|
+
- Generic base class for all validators (this class)
|
|
504
|
+
- Type-safe domain-specific subclasses (EnergyPlusOutputEnvelope,
|
|
505
|
+
FMIOutputEnvelope, etc.)
|
|
506
|
+
- Django to deserialize using the correct subclass based on validator.type
|
|
507
|
+
|
|
508
|
+
## Generic vs Domain-Specific Outputs
|
|
509
|
+
|
|
510
|
+
All validators populate these **generic** fields:
|
|
511
|
+
- status: SUCCESS/FAILED_VALIDATION/FAILED_RUNTIME/CANCELLED
|
|
512
|
+
- messages: Errors, warnings, info (consistent across all validators)
|
|
513
|
+
- metrics: Computed values like energy use, temperature ranges
|
|
514
|
+
- artifacts: GCS URIs to output files (SQL, CSV reports, HTML visualizations)
|
|
515
|
+
- timing: When the job was queued/started/finished
|
|
516
|
+
|
|
517
|
+
Some validators also populate **domain-specific** outputs:
|
|
518
|
+
- outputs: Detailed execution data (returncode, logs, file paths, etc.)
|
|
519
|
+
- Optional because not all validators produce this level of detail
|
|
520
|
+
|
|
521
|
+
## Why Both metrics and outputs?
|
|
522
|
+
|
|
523
|
+
- **metrics**: High-level computed values for UI display
|
|
524
|
+
(energy use, peak temperature, etc.)
|
|
525
|
+
- Always a flat list of name/value/unit tuples
|
|
526
|
+
- Consistent format across all validators
|
|
527
|
+
- Used for dashboards, comparisons, search
|
|
528
|
+
|
|
529
|
+
- **outputs**: Detailed domain-specific execution data
|
|
530
|
+
- Nested structure with validator-specific fields
|
|
531
|
+
- Examples: EnergyPlus returncode, FMU logs, XML schema validation
|
|
532
|
+
- Used for debugging, detailed analysis, reproducing results
|
|
533
|
+
|
|
534
|
+
## Why raw_outputs?
|
|
535
|
+
|
|
536
|
+
Some validators produce many output files (EnergyPlus can create dozens).
|
|
537
|
+
Instead of creating a ValidationArtifact for each one, we:
|
|
538
|
+
1. Upload all files to GCS
|
|
539
|
+
2. Create a manifest.json listing all files
|
|
540
|
+
3. Set raw_outputs.manifest_uri to point to the manifest
|
|
541
|
+
This keeps the envelope small while preserving access to all files.
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
schema_version: Literal["validibot.output.v1"] = "validibot.output.v1"
|
|
545
|
+
|
|
546
|
+
run_id: str = Field(description="Unique run identifier (matches input)")
|
|
547
|
+
|
|
548
|
+
validator: ValidatorInfo
|
|
549
|
+
|
|
550
|
+
status: ValidationStatus
|
|
551
|
+
|
|
552
|
+
timing: ValidationTiming
|
|
553
|
+
|
|
554
|
+
messages: list[ValidationMessage] = Field(
|
|
555
|
+
default_factory=list, description="Validation findings, warnings, errors"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
metrics: list[ValidationMetric] = Field(
|
|
559
|
+
default_factory=list, description="Computed metrics from the validation"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
artifacts: list[ValidationArtifact] = Field(
|
|
563
|
+
default_factory=list, description="Output files produced by the validator"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
raw_outputs: RawOutputs | None = Field(
|
|
567
|
+
default=None, description="Raw output files information"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
outputs: dict[str, Any] | None = Field(
|
|
571
|
+
default=None,
|
|
572
|
+
description=("Domain-specific outputs (subclasses override with typed model)"),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
model_config = {"extra": "forbid"}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ==============================================================================
|
|
579
|
+
# Callback Payload
|
|
580
|
+
# ==============================================================================
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class ValidationCallback(BaseModel):
|
|
584
|
+
"""
|
|
585
|
+
Callback payload POSTed from validator container to Django.
|
|
586
|
+
|
|
587
|
+
Minimal payload to avoid duplication - Django loads the full output.json
|
|
588
|
+
from storage after receiving this callback.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
run_id: str = Field(description="Run identifier")
|
|
592
|
+
|
|
593
|
+
callback_id: str | None = Field(
|
|
594
|
+
default=None,
|
|
595
|
+
description=(
|
|
596
|
+
"Idempotency key echoed from the input envelope's context.callback_id. "
|
|
597
|
+
"Used by the callback handler to detect and ignore duplicate deliveries."
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
status: ValidationStatus
|
|
602
|
+
|
|
603
|
+
result_uri: str = Field(
|
|
604
|
+
description="Storage URI to output.json (gs:// or file:// for self-hosted)"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
model_config = {"extra": "forbid"}
|