66# ==============================================================================
77from __future__ import annotations
88
9- from typing import List
9+ import json
10+ from typing import List , TYPE_CHECKING
1011
1112from pydantic import BaseModel , ConfigDict , Field , SerializeAsAny , model_validator
1213from shapely import Point
1617from .schema_datamodels import DatastreamRecordSchema , CommandSchema
1718from .timemanagement import TimeInstant , TimePeriod
1819
20+ if TYPE_CHECKING :
21+ from .swe_components import AnyComponent
22+
1923
2024class BoundingBox (BaseModel ):
2125 model_config = ConfigDict (arbitrary_types_allowed = True , populate_by_name = True )
@@ -132,6 +136,59 @@ class SystemResource(BaseModel):
132136 modes : List [Mode ] = Field (None )
133137 method : ProcessMethod = Field (None )
134138
139+ def to_smljson_dict (self ) -> dict :
140+ """Render this system as an `application/sml+json` dict (SensorML JSON encoding).
141+
142+ Sets ``feature_type = "PhysicalSystem"`` to match the SML discriminator
143+ before dumping. Output keys are camelCase per the CS API wire format.
144+ """
145+ self .feature_type = "PhysicalSystem"
146+ return self .model_dump (by_alias = True , exclude_none = True , mode = 'json' )
147+
148+ def to_smljson (self ) -> str :
149+ """JSON-string variant of `to_smljson_dict`."""
150+ return json .dumps (self .to_smljson_dict ())
151+
152+ def to_geojson_dict (self ) -> dict :
153+ """Render this system as an `application/geo+json` dict.
154+
155+ Sets ``feature_type = "Feature"`` to match the GeoJSON discriminator
156+ before dumping. Useful when posting to endpoints that expect the
157+ GeoJSON Feature shape.
158+ """
159+ self .feature_type = "Feature"
160+ return self .model_dump (by_alias = True , exclude_none = True , mode = 'json' )
161+
162+ def to_geojson (self ) -> str :
163+ """JSON-string variant of `to_geojson_dict`."""
164+ return json .dumps (self .to_geojson_dict ())
165+
166+ @classmethod
167+ def from_smljson_dict (cls , data : dict ) -> "SystemResource" :
168+ """Build a `SystemResource` from an `application/sml+json` dict
169+ (e.g., a CS API server response body for a system in SML form)."""
170+ return cls .model_validate (data , by_alias = True )
171+
172+ @classmethod
173+ def from_geojson_dict (cls , data : dict ) -> "SystemResource" :
174+ """Build a `SystemResource` from an `application/geo+json` dict
175+ (e.g., a CS API server response body for a system in GeoJSON form)."""
176+ return cls .model_validate (data , by_alias = True )
177+
178+ @classmethod
179+ def from_csapi_dict (cls , data : dict ) -> "SystemResource" :
180+ """Build a `SystemResource` from a CS API system dict, auto-dispatching
181+ on the ``type`` field: ``"PhysicalSystem"`` → SML+JSON path,
182+ ``"Feature"`` → GeoJSON path. Anything else falls through to a
183+ permissive validate.
184+ """
185+ feature_type = data .get ("type" )
186+ if feature_type == "PhysicalSystem" :
187+ return cls .from_smljson_dict (data )
188+ if feature_type == "Feature" :
189+ return cls .from_geojson_dict (data )
190+ return cls .model_validate (data , by_alias = True )
191+
135192
136193class DatastreamResource (BaseModel ):
137194 """
@@ -175,6 +232,25 @@ def handle_aliases(cls, values):
175232 break
176233 return values
177234
235+ def to_csapi_dict (self ) -> dict :
236+ """Render this datastream as the CS API `application/json` resource
237+ body. The embedded ``schema`` field is dumped polymorphically per
238+ whichever variant (`SWEDatastreamRecordSchema` /
239+ `JSONDatastreamRecordSchema`) it holds.
240+ """
241+ return self .model_dump (by_alias = True , exclude_none = True , mode = 'json' )
242+
243+ def to_csapi_json (self ) -> str :
244+ """JSON-string variant of `to_csapi_dict`."""
245+ return json .dumps (self .to_csapi_dict ())
246+
247+ @classmethod
248+ def from_csapi_dict (cls , data : dict ) -> "DatastreamResource" :
249+ """Build a `DatastreamResource` from a CS API datastream dict
250+ (e.g., a server response body or an entry from a /datastreams
251+ listing)."""
252+ return cls .model_validate (data , by_alias = True )
253+
178254
179255class ObservationResource (BaseModel ):
180256 model_config = ConfigDict (populate_by_name = True , arbitrary_types_allowed = True )
@@ -187,6 +263,84 @@ class ObservationResource(BaseModel):
187263 result : dict = Field (...)
188264 result_link : Link = Field (None , alias = "result@link" )
189265
266+ def to_omjson_dict (self , datastream_id : str | None = None ) -> dict :
267+ """Render this observation as an `application/om+json` dict
268+ (the ``ObservationOMJSONInline`` shape).
269+
270+ :param datastream_id: Optional ID to include as ``datastream@id``
271+ on the output. The CS API typically supplies this from URL
272+ context, so it's not required on the model itself.
273+ """
274+ from .schema_datamodels import ObservationOMJSONInline
275+ kwargs = {"result" : self .result }
276+ if datastream_id is not None :
277+ kwargs ["datastream_id" ] = datastream_id
278+ if self .phenomenon_time :
279+ kwargs ["phenomenon_time" ] = self .phenomenon_time .get_iso_time ()
280+ if self .result_time :
281+ kwargs ["result_time" ] = self .result_time .get_iso_time ()
282+ if self .parameters is not None :
283+ kwargs ["parameters" ] = self .parameters
284+ wrapper = ObservationOMJSONInline (** kwargs )
285+ return wrapper .model_dump (by_alias = True , exclude_none = True , mode = 'json' )
286+
287+ def to_swejson_dict (self , schema : "AnyComponent" = None ) -> dict :
288+ """Render this observation as an `application/swe+json` payload
289+ (the SWE Common JSON encoding of one record).
290+
291+ SWE+JSON encodes a single observation as a flat JSON object whose
292+ keys are the schema field names; ``self.result`` is already that
293+ dict, so this is essentially a passthrough. The optional
294+ ``schema`` argument is accepted for forward compatibility (when
295+ we add field-order / encoding-aware emission).
296+ """
297+ # ``schema`` reserved for future encoding rules (vector-as-arrays,
298+ # JSONEncoding handling, etc.); current behavior is passthrough.
299+ del schema
300+ return dict (self .result ) if self .result is not None else {}
301+
302+ @classmethod
303+ def from_omjson_dict (cls , data : dict ) -> "ObservationResource" :
304+ """Build an `ObservationResource` from an `application/om+json` dict.
305+
306+ Parses through `ObservationOMJSONInline` to validate the OM+JSON
307+ envelope, then strips the ``datastream@id`` / ``foi@id`` envelope
308+ fields (those live on the surrounding context, not the resource)
309+ and returns the inner observation.
310+ """
311+ from .schema_datamodels import ObservationOMJSONInline
312+ wrapper = ObservationOMJSONInline .model_validate (data )
313+ kwargs = {
314+ "result_time" : TimeInstant .from_string (wrapper .result_time ),
315+ "result" : wrapper .result ,
316+ }
317+ if wrapper .phenomenon_time :
318+ kwargs ["phenomenon_time" ] = TimeInstant .from_string (wrapper .phenomenon_time )
319+ if wrapper .parameters is not None :
320+ kwargs ["parameters" ] = wrapper .parameters
321+ return cls (** kwargs )
322+
323+ @classmethod
324+ def from_swejson_dict (cls , data : dict , schema : "AnyComponent" = None ,
325+ result_time : str | None = None ) -> "ObservationResource" :
326+ """Build an `ObservationResource` from an `application/swe+json`
327+ observation payload.
328+
329+ SWE+JSON observations don't carry an envelope (no ``resultTime`` /
330+ ``phenomenonTime`` fields); pass ``result_time`` explicitly when
331+ you have it, otherwise the current UTC time is used.
332+
333+ :param data: The flat SWE+JSON record dict.
334+ :param schema: Optional schema, reserved for future per-field
335+ type coercion. Currently ignored.
336+ :param result_time: ISO 8601 timestamp for ``resultTime``;
337+ defaults to ``TimeInstant.now_as_time_instant().isoformat()``
338+ if omitted.
339+ """
340+ del schema # future use
341+ rt = TimeInstant .from_string (result_time ) if result_time is not None else TimeInstant .now_as_time_instant ()
342+ return cls (result_time = rt , result = dict (data ))
343+
190344
191345class ControlStreamResource (BaseModel ):
192346 model_config = ConfigDict (populate_by_name = True , arbitrary_types_allowed = True )
@@ -206,3 +360,22 @@ class ControlStreamResource(BaseModel):
206360 asynchronous : bool = Field (True , alias = "async" )
207361 command_schema : SerializeAsAny [CommandSchema ] = Field (None , alias = "schema" )
208362 links : List [Link ] = Field (None )
363+
364+ def to_csapi_dict (self ) -> dict :
365+ """Render this control stream as the CS API `application/json`
366+ resource body. The embedded ``schema`` field is dumped
367+ polymorphically per whichever variant
368+ (`SWEJSONCommandSchema` / `JSONCommandSchema`) it holds.
369+ """
370+ return self .model_dump (by_alias = True , exclude_none = True , mode = 'json' )
371+
372+ def to_csapi_json (self ) -> str :
373+ """JSON-string variant of `to_csapi_dict`."""
374+ return json .dumps (self .to_csapi_dict ())
375+
376+ @classmethod
377+ def from_csapi_dict (cls , data : dict ) -> "ControlStreamResource" :
378+ """Build a `ControlStreamResource` from a CS API control-stream dict
379+ (e.g., a server response body or an entry from a /controlstreams
380+ listing)."""
381+ return cls .model_validate (data , by_alias = True )
0 commit comments