|
7 | 7 | from __future__ import annotations |
8 | 8 |
|
9 | 9 | from datetime import datetime |
10 | | -from typing import Union, List, Literal |
| 10 | +from typing import Annotated, Union, List, Literal |
11 | 11 |
|
12 | | -from pydantic import BaseModel, Field, SerializeAsAny, field_validator, model_validator, HttpUrl, ConfigDict |
| 12 | +from pydantic import BaseModel, Field, model_validator, HttpUrl, ConfigDict |
13 | 13 |
|
14 | 14 | from .api_utils import Link, URI |
15 | | -from .csapi4py.constants import ObservationFormat |
16 | | -from .encoding import Encoding |
| 15 | +from .encoding import JSONEncoding |
17 | 16 | from .geometry import Geometry |
18 | 17 | from .swe_components import AnyComponent, check_named |
19 | 18 | from .timemanagement import TimeInstant |
@@ -76,8 +75,16 @@ class SWEJSONCommandSchema(CommandSchema): |
76 | 75 | """ |
77 | 76 | model_config = ConfigDict(populate_by_name=True) |
78 | 77 |
|
79 | | - command_format: str = Field("application/swe+json", alias='commandFormat') |
80 | | - encoding: SerializeAsAny[Encoding] = Field(...) |
| 78 | + # Literal pin powers the discriminated `AnyCommandSchema` union below |
| 79 | + # and removes the need for a runtime field_validator. |
| 80 | + command_format: Literal["application/swe+json"] = Field( |
| 81 | + "application/swe+json", alias='commandFormat') |
| 82 | + # Concrete subclass instead of `SerializeAsAny[Encoding]` — `JSONEncoding` |
| 83 | + # is the only Encoding type used in practice, and a concrete type |
| 84 | + # serializes deterministically without `SerializeAsAny`. If/when more |
| 85 | + # encoding types arrive, migrate this to a discriminated Union on |
| 86 | + # `Encoding.type`. |
| 87 | + encoding: JSONEncoding = Field(...) |
81 | 88 | record_schema: AnyComponent = Field(..., alias='recordSchema') |
82 | 89 |
|
83 | 90 | @model_validator(mode="after") |
@@ -140,17 +147,17 @@ class DatastreamRecordSchema(BaseModel): |
140 | 147 | # docs/osh_spec_deviations.md (swe-json-missing-encoding). |
141 | 148 | class SWEDatastreamRecordSchema(DatastreamRecordSchema): |
142 | 149 | model_config = ConfigDict(populate_by_name=True) |
143 | | - encoding: SerializeAsAny[Encoding] = Field(None) |
| 150 | + # Multi-Literal acts as the discriminator value(s) for AnyDatastreamRecordSchema |
| 151 | + # below. Replaces the previous runtime field_validator. |
| 152 | + obs_format: Literal[ |
| 153 | + "application/swe+json", |
| 154 | + "application/swe+csv", |
| 155 | + "application/swe+text", |
| 156 | + "application/swe+binary", |
| 157 | + ] = Field(..., alias='obsFormat') |
| 158 | + encoding: JSONEncoding = Field(None) |
144 | 159 | record_schema: AnyComponent = Field(..., alias='recordSchema') |
145 | 160 |
|
146 | | - @field_validator('obs_format') |
147 | | - @classmethod |
148 | | - def check_check_obs_format(cls, v): |
149 | | - if v not in [ObservationFormat.SWE_JSON.value, ObservationFormat.SWE_CSV.value, |
150 | | - ObservationFormat.SWE_TEXT.value, ObservationFormat.SWE_BINARY.value]: |
151 | | - raise ValueError('obsFormat must be on of the SWE formats') |
152 | | - return v |
153 | | - |
154 | 161 | @model_validator(mode="after") |
155 | 162 | def _root_record_schema_requires_name(self): |
156 | 163 | check_named(self.record_schema, "SWEDatastreamRecordSchema.recordSchema") |
@@ -178,20 +185,15 @@ class OMJSONDatastreamRecordSchema(DatastreamRecordSchema): |
178 | 185 | """ |
179 | 186 | model_config = ConfigDict(populate_by_name=True) |
180 | 187 |
|
181 | | - obs_format: str = Field(ObservationFormat.JSON.value, alias='obsFormat') |
| 188 | + # Multi-Literal — both wire forms are spec-equivalent for OM+JSON. |
| 189 | + obs_format: Literal[ |
| 190 | + "application/om+json", |
| 191 | + "application/json", |
| 192 | + ] = Field("application/om+json", alias='obsFormat') |
182 | 193 | result_schema: AnyComponent = Field(None, alias='resultSchema') |
183 | 194 | parameters_schema: AnyComponent = Field(None, alias='parametersSchema') |
184 | 195 | result_link: dict = Field(None, alias='resultLink') |
185 | 196 |
|
186 | | - @field_validator('obs_format') |
187 | | - @classmethod |
188 | | - def _check_obs_format(cls, v): |
189 | | - if v not in (ObservationFormat.JSON.value, "application/json"): |
190 | | - raise ValueError( |
191 | | - f"obsFormat must be 'application/json' or '{ObservationFormat.JSON.value}'" |
192 | | - ) |
193 | | - return v |
194 | | - |
195 | 197 | @model_validator(mode="after") |
196 | 198 | def _root_schemas_require_name(self): |
197 | 199 | if self.result_schema is not None: |
@@ -339,3 +341,30 @@ class SystemHistoryProperties(BaseModel): |
339 | 341 | valid_time: list = Field(None) |
340 | 342 | parent_system_link: str = Field(None, serialization_alias='parentSystem@link') |
341 | 343 | procedure_link: str = Field(None, serialization_alias='procedure@link') |
| 344 | + |
| 345 | + |
| 346 | +# Discriminated unions replace the earlier `SerializeAsAny[<base>]` pattern |
| 347 | +# on resource models. Pydantic dispatches by the literal value of the |
| 348 | +# discriminator field — `obsFormat` / `commandFormat` — so validate and |
| 349 | +# dump round-trip without polymorphism quirks. |
| 350 | +AnyDatastreamRecordSchema = Annotated[ |
| 351 | + Union[SWEDatastreamRecordSchema, OMJSONDatastreamRecordSchema], |
| 352 | + Field(discriminator='obs_format'), |
| 353 | +] |
| 354 | +"""Public alias for `DatastreamResource.record_schema`. Discriminator: `obs_format`.""" |
| 355 | + |
| 356 | +AnyCommandSchema = Annotated[ |
| 357 | + Union[SWEJSONCommandSchema, JSONCommandSchema], |
| 358 | + Field(discriminator='command_format'), |
| 359 | +] |
| 360 | +"""Public alias for `ControlStreamResource.command_schema`. Discriminator: `command_format`.""" |
| 361 | + |
| 362 | + |
| 363 | +# Defense-in-depth: rebuild every container model that forward-references |
| 364 | +# `AnyComponent`. See the matching block in swe_components.py for the |
| 365 | +# `MockValSer` rationale — same fault recurs here because each schema |
| 366 | +# class threads `AnyComponent` through its body. |
| 367 | +SWEJSONCommandSchema.model_rebuild(force=True) |
| 368 | +JSONCommandSchema.model_rebuild(force=True) |
| 369 | +SWEDatastreamRecordSchema.model_rebuild(force=True) |
| 370 | +OMJSONDatastreamRecordSchema.model_rebuild(force=True) |
0 commit comments