@@ -312,49 +312,55 @@ def test_swe_datastream_root_invalid_name_pattern_raises():
312312# Quantity: [type, definition, label, uom]
313313# Boolean: [type, definition, label]
314314# Text: [type, definition, label]
315- # Vector: [type, definition, referenceFrame, label, coordinates]
315+ # Vector: [type, definition, referenceFrame, coordinates]
316316# DataRecord:[type, fields]
317- # Geometry: [type, srs, definition, label]
317+ # Geometry: [type, srs, definition]
318+ #
319+ # `label` is optional everywhere — SWE Common 3 inherits it from
320+ # AbstractDataComponent as optional. OSH emits labelless components
321+ # in the wild (e.g. the SensorLocation Vector); a required `label`
322+ # here would break record-schema parsing during discovery.
318323
319324
320325def test_quantity_requires_uom ():
321326 with pytest .raises (ValidationError , match = "uom" ):
322327 QuantitySchema (label = "X" , definition = "http://example.org/x" )
323328
324329
325- def test_quantity_requires_label ():
326- with pytest . raises ( ValidationError , match = "label" ):
327- QuantitySchema ( definition = "http://example.org/x" , uom = { "code" : "m" })
330+ def test_quantity_label_is_optional ():
331+ q = QuantitySchema ( definition = "http://example.org/x" , uom = { "code" : "m" })
332+ assert q . label is None
328333
329334
330335def test_quantity_requires_definition ():
331336 with pytest .raises (ValidationError , match = "definition" ):
332337 QuantitySchema (label = "X" , uom = {"code" : "m" })
333338
334339
335- def test_boolean_requires_label_and_definition ():
336- with pytest .raises (ValidationError , match = "label" ):
337- BooleanSchema (definition = "http://example.org/b" )
340+ def test_boolean_label_optional_definition_required ():
341+ BooleanSchema (definition = "http://example.org/b" ) # no label — OK
338342 with pytest .raises (ValidationError , match = "definition" ):
339343 BooleanSchema (label = "X" )
340344
341345
342- def test_text_requires_label_and_definition ():
343- with pytest .raises (ValidationError , match = "label" ):
344- TextSchema (definition = "http://example.org/t" )
346+ def test_text_label_optional_definition_required ():
347+ TextSchema (definition = "http://example.org/t" ) # no label — OK
345348 with pytest .raises (ValidationError , match = "definition" ):
346349 TextSchema (label = "X" )
347350
348351
349- def test_vector_requires_label_definition_referenceframe_coordinates ():
352+ def test_vector_requires_definition_referenceframe_coordinates ():
353+ # `label` is intentionally NOT in the required set: SWE Common 3 inherits
354+ # it from AbstractDataComponent as optional, and OSH emits labelless
355+ # Vectors (e.g. SensorLocation). See test_vector_label_is_optional…
350356 base = dict (
351357 label = "V" , definition = "http://example.org/v" ,
352358 referenceFrame = "http://example.org/frames/ENU" ,
353359 coordinates = [QuantitySchema (name = "x" , label = "X" ,
354360 definition = "http://example.org/x" ,
355361 uom = {"code" : "m" })],
356362 )
357- for missing in ("label" , " definition" , "referenceFrame" , "coordinates" ):
363+ for missing in ("definition" , "referenceFrame" , "coordinates" ):
358364 kwargs = {k : v for k , v in base .items () if k != missing }
359365 with pytest .raises (ValidationError ):
360366 VectorSchema (** kwargs )
@@ -365,15 +371,23 @@ def test_datarecord_requires_fields():
365371 DataRecordSchema (name = "r" )
366372
367373
368- def test_geometry_requires_srs_definition_label ():
374+ def test_geometry_requires_srs_and_definition ():
375+ # `label` deliberately omitted from required set — SWE Common 3
376+ # inherits it from AbstractDataComponent as optional.
369377 base = dict (label = "G" , definition = "http://example.org/g" ,
370378 srs = "http://www.opengis.net/def/crs/EPSG/0/4326" )
371- for missing in ("label" , " definition" , "srs" ):
379+ for missing in ("definition" , "srs" ):
372380 kwargs = {k : v for k , v in base .items () if k != missing }
373381 with pytest .raises (ValidationError ):
374382 GeometrySchema (** kwargs )
375383
376384
385+ def test_geometry_label_is_optional ():
386+ g = GeometrySchema (definition = "http://example.org/g" ,
387+ srs = "http://www.opengis.net/def/crs/EPSG/0/4326" )
388+ assert g .label is None
389+
390+
377391# --- B.2 discriminator routing ---------------------------------------------
378392
379393DISCRIMINATOR_CASES = [
@@ -566,6 +580,69 @@ def test_vector_accepts_quantity_in_coordinates():
566580 })
567581
568582
583+ def test_vector_label_is_optional_per_swe_common3 ():
584+ # SWE Common 3 Vector inherits AbstractDataComponent.label as optional;
585+ # OSH's SensorLocation datastream emits a labelless Vector. A required
586+ # `label` here would break SWE+JSON schema discovery for any datastream
587+ # carrying a Vector — see the discover_datastreams cascade.
588+ v = VectorSchema .model_validate ({
589+ "type" : "Vector" ,
590+ "name" : "location" ,
591+ "definition" : "http://www.opengis.net/def/property/OGC/0/SensorLocation" ,
592+ "referenceFrame" : "http://www.opengis.net/def/crs/EPSG/0/4979" ,
593+ "coordinates" : [_quantity_field ("x" )],
594+ })
595+ assert v .label is None
596+
597+
598+ def test_swe_datastream_schema_parses_osh_sensor_location_shape ():
599+ # End-to-end shape mirroring `GET /datastreams/{id}/schema` for OSH's
600+ # built-in `sensorLocation` output (CS API SWE+JSON form).
601+ payload = {
602+ "obsFormat" : "application/swe+json" ,
603+ "recordSchema" : {
604+ "type" : "DataRecord" ,
605+ "name" : "sensorLocation" ,
606+ "id" : "SENSOR_LOCATION" ,
607+ "label" : "Sensor Location" ,
608+ "fields" : [
609+ {
610+ "type" : "Time" ,
611+ "name" : "time" ,
612+ "definition" : "http://www.opengis.net/def/property/OGC/0/SamplingTime" ,
613+ "label" : "Sampling Time" ,
614+ "referenceFrame" : "http://www.opengis.net/def/trs/BIPM/0/UTC" ,
615+ "uom" : {"href" : "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" },
616+ },
617+ {
618+ "type" : "Vector" ,
619+ "name" : "location" ,
620+ "definition" : "http://www.opengis.net/def/property/OGC/0/SensorLocation" ,
621+ "referenceFrame" : "http://www.opengis.net/def/crs/EPSG/0/4979" ,
622+ "localFrame" : "#REF_FRAME_LOCAL" ,
623+ "coordinates" : [
624+ {"type" : "Quantity" , "name" : "lat" , "label" : "Geodetic Latitude" ,
625+ "definition" : "http://sensorml.com/ont/swe/property/GeodeticLatitude" ,
626+ "axisID" : "Lat" , "uom" : {"code" : "deg" }},
627+ {"type" : "Quantity" , "name" : "lon" , "label" : "Longitude" ,
628+ "definition" : "http://sensorml.com/ont/swe/property/Longitude" ,
629+ "axisID" : "Lon" , "uom" : {"code" : "deg" }},
630+ {"type" : "Quantity" , "name" : "alt" , "label" : "Ellipsoidal Height" ,
631+ "definition" : "http://sensorml.com/ont/swe/property/HeightAboveEllipsoid" ,
632+ "axisID" : "h" , "uom" : {"code" : "m" }},
633+ ],
634+ },
635+ ],
636+ },
637+ }
638+ sw = SWEDatastreamRecordSchema .from_swejson_dict (payload )
639+ vec = sw .record_schema .fields [1 ]
640+ assert vec .type == "Vector"
641+ assert vec .label is None
642+ assert vec .reference_frame == "http://www.opengis.net/def/crs/EPSG/0/4979"
643+ assert [c .name for c in vec .coordinates ] == ["lat" , "lon" , "alt" ]
644+
645+
569646# --- B.6 DataRecord.fields minItems: 1 -------------------------------------
570647
571648def test_datarecord_empty_fields_rejected ():
0 commit comments