Skip to content

Commit 9206be0

Browse files
committed
fix(aviation-wx): split GeoJSON stubs from SensorML bodies for strict csapi-go-v2
Refactor aviation-wx publisher to comply with OGC 23-001 strict-parsing servers (csapi-go-v2 and later) which reject any GeoJSON \properties\ field outside the closed set {featureType, uid, name, description} (+ validTime on Procedures/Deployments, + platform@link on station deployments). Resource-by-resource: - PROCEDURE_BODY split into PROCEDURE_BODY_STUB (GeoJSON, closed) and PROCEDURE_SML (SensorML JSON, PUT separately as application/sml+json). Bootstrap now passes sml_body=PROCEDURE_SML and force_sml=force_sml. - _system_stub() stripped of typeOf@link, links, and validTime (these fields were silently dropped on the legacy server and 400-rejected by csapi-go-v2). _system_sml() already carried equivalent metadata. - _system_sml() drops 'characteristics' and 'capabilities' arrays — empirically rejected on /systems SML PUT despite being part of the OGC SensorML JSON encoding (filed as separate observation; identifier / classifier / position / contacts cover the equivalent metadata). - _datastream_schema() drops 'documentation', 'characteristics', and the SWE Time field 'referenceTime' (all rejected by strict server's datastream schema validator). - _deploy_root/_deploy_group/_deploy_station() stripped of 'documentation' and station 'links' arrays. Procedure SML uses 'documentation' (typo) instead of 'documents': the maintainer's c2ab201 fix landed only on SystemSensorMLFeature, not on ProcedureSensorMLFeature; until that follow-up lands, the procedure SML PUT requires the typo'd field. /systems uses the correct 'documents' name. Live-validated against https://129-80-248-53.sslip.io/csapi-go-v2/: - 1 procedure, 5 systems with full SensorML, 5 datastreams, and a 3-tier deployment tree (root + group + 5 stations) all created OK. - System SML round-trip confirms uniqueId, label, keywords, identifiers, classifiers, contacts, documents, position, links all preserved. Reference: docs/research/Strict_Parsing_Migration_Spec_Grounded_Reanalysis_2026-05-09.md S9
1 parent ca2b794 commit 9206be0

1 file changed

Lines changed: 88 additions & 99 deletions

File tree

publishers/aviation_wx/bootstrap_aviation_wx.py

Lines changed: 88 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -93,67 +93,96 @@ def _deploy_uid(icao_id: str) -> str:
9393

9494
# ═══════════════════════════════════════════════════════════════════════════
9595
# Resource definitions
96+
#
97+
# Strict-parsing servers (csapi-go-v2 and later) reject any field in
98+
# GeoJSON `properties` outside the closed set
99+
# {featureType, uid, name, description, validTime, platform@link}.
100+
# All SensorML metadata (keywords, identifiers, classifiers, contacts,
101+
# documents, characteristics, capabilities, lineage, usageConstraints,
102+
# typeOf, ...) lives in a SEPARATE `application/sml+json` body that is
103+
# PUT against /systems/{id} or /procedures/{id} after creation.
104+
# See: docs/research/Strict_Parsing_Migration_Spec_Grounded_Reanalysis_2026-05-09.md §9.
96105
# ═══════════════════════════════════════════════════════════════════════════
97106

98-
PROCEDURE_BODY = {
107+
PROCEDURE_BODY_STUB = {
99108
"type": "Feature",
100109
"geometry": None,
101110
"properties": {
102-
"uid": PROC_UID,
103111
"featureType": "sosa:ObservingProcedure",
112+
"uid": PROC_UID,
104113
"name": "METAR Decoder v1",
105114
"description": (
106115
"Publishes real-time METAR observations from the AviationWeather.gov REST API. "
107116
"Data includes temperature, dew point, wind speed/direction, visibility, "
108117
"barometric pressure, cloud layers, flight category, and the raw METAR string. "
109118
"Observations are decoded from standard METAR format and published to CSAPI."
110119
),
111-
"keywords": [
112-
"METAR", "aviation", "weather", "AviationWeather.gov", "FAA", "AWC",
113-
"ASOS", "surface observation", "flight category",
114-
],
115-
"documentation": [
116-
{"title": "AviationWeather.gov", "href": AWX_HOME, "rel": "about"},
117-
{"title": "AviationWeather Data API", "href": AWX_API_DOC, "rel": "documentation"},
118-
{"title": "METAR Format Guide", "href": "https://www.weather.gov/media/wrh/mesowest/metar_decode_key.pdf", "rel": "describedby"},
119-
],
120-
"contacts": [
121-
{
122-
"role": "operator",
123-
"organizationName": FAA_CONTACT_ORG,
124-
"website": AWX_HOME,
125-
"email": FAA_CONTACT_EMAIL,
126-
},
127-
{
128-
"role": "publisher",
129-
"organizationName": "OS4CSAPI",
130-
"website": "https://github.com/OS4CSAPI/OSHConnect-Python",
120+
"validTime": [VALID_TIME_START, ".."],
121+
},
122+
}
123+
124+
PROCEDURE_SML = {
125+
"type": "SimpleProcess",
126+
"id": PROC_UID,
127+
"uniqueId": PROC_UID,
128+
"definition": "sosa:ObservingProcedure",
129+
"label": "METAR Decoder v1",
130+
"description": (
131+
"Publishes real-time METAR observations from the AviationWeather.gov REST API. "
132+
"Data includes temperature, dew point, wind speed/direction, visibility, "
133+
"barometric pressure, cloud layers, flight category, and the raw METAR string. "
134+
"Observations are decoded from standard METAR format and published to CSAPI."
135+
),
136+
"keywords": [
137+
"METAR", "aviation", "weather", "AviationWeather.gov", "FAA", "AWC",
138+
"ASOS", "surface observation", "flight category",
139+
],
140+
# NOTE: csapi-go-v2 ProcedureSensorMLFeature struct still has the
141+
# `documentation` typo (the c2ab201 fix landed only on
142+
# SystemSensorMLFeature). Until that lands, procedure SML PUT requires
143+
# the typo'd field name. /systems uses `documents` (correct).
144+
"documentation": [
145+
{"role": "http://dbpedia.org/resource/Web_page",
146+
"name": "AviationWeather.gov",
147+
"link": {"href": AWX_HOME, "type": "text/html"}},
148+
{"role": "http://dbpedia.org/resource/Web_page",
149+
"name": "AviationWeather Data API",
150+
"link": {"href": AWX_API_DOC, "type": "text/html"}},
151+
{"role": "http://dbpedia.org/resource/Web_page",
152+
"name": "METAR Format Guide",
153+
"link": {"href": "https://www.weather.gov/media/wrh/mesowest/metar_decode_key.pdf",
154+
"type": "application/pdf"}},
155+
],
156+
"contacts": [
157+
{
158+
"role": "operator",
159+
"organisationName": FAA_CONTACT_ORG,
160+
"contactInfo": {
161+
"address": {
162+
"deliveryPoint": FAA_CONTACT_ADDRESS,
163+
"electronicMailAddress": FAA_CONTACT_EMAIL,
164+
},
165+
"onlineResource": {"linkage": AWX_HOME},
131166
},
132-
],
133-
"lineage": {
134-
"source": "FAA / Aviation Weather Center (AWC)",
135-
"upstream": f"AviationWeather.gov REST API at {AWX_API_DOC}",
136-
"normalization": (
137-
"Publisher fetches decoded METAR JSON from the AviationWeather.gov API "
138-
"and emits a flat JSON result with metric/aviation-standard units."
139-
),
140167
},
141-
"usageConstraints": {
142-
"sourceProtocol": "HTTPS",
143-
"sourceFormat": "JSON via AviationWeather.gov REST API",
144-
"rateLimitNote": "No explicit rate limit documented; publisher uses 5-minute cadence.",
145-
"qualityControlNote": (
146-
"METAR reports undergo NWS/FAA quality control. SPECI (special) reports "
147-
"are issued between standard hourly observations when conditions change significantly."
148-
),
168+
{
169+
"role": "publisher",
170+
"organisationName": "OS4CSAPI",
171+
"contactInfo": {
172+
"onlineResource": {"linkage": "https://github.com/OS4CSAPI/OSHConnect-Python"},
173+
},
149174
},
150-
"validTime": [VALID_TIME_START, ".."],
151-
},
175+
],
152176
}
153177

154178

155179
def _system_stub(station: dict, proc_id: str) -> dict:
156-
"""GeoJSON Feature stub for an aviation weather station system."""
180+
"""GeoJSON Feature stub for an aviation weather station system.
181+
182+
Properties closed to {featureType, uid, name, description} per OGC 23-001
183+
strict parsing. typeOf, validTime, and links live in the companion SML body
184+
(see ``_system_sml``).
185+
"""
157186
icao_id = station["icao_id"]
158187
return {
159188
"type": "Feature",
@@ -162,21 +191,15 @@ def _system_stub(station: dict, proc_id: str) -> dict:
162191
"coordinates": [station["lon"], station["lat"]],
163192
},
164193
"properties": {
165-
"uid": _system_uid(icao_id),
166194
"featureType": "sosa:Sensor",
195+
"uid": _system_uid(icao_id),
167196
"name": f"AWX {icao_id}{station['name']}",
168197
"description": (
169198
f"AviationWeather.gov METAR station {icao_id} at {station['name']}, "
170199
f"{station['city']}, {station['state']}. "
171200
f"Station type: {station.get('station_type', 'ASOS')}. "
172201
f"Field elevation: {station.get('elev_m', '?')} m MSL."
173202
),
174-
"typeOf@link": {"href": proc_id, "title": "METAR Decoder v1"},
175-
"links": [
176-
{"rel": "about", "title": "METAR Data", "href": _station_page_url(icao_id)},
177-
{"rel": "alternate", "title": "API Endpoint", "href": _station_api_url(icao_id)},
178-
],
179-
"validTime": [VALID_TIME_START, ".."],
180203
},
181204
}
182205

@@ -289,29 +312,13 @@ def _system_sml(station: dict) -> dict:
289312
},
290313
],
291314
"documents": docs,
292-
"characteristics": [
293-
{
294-
"name": "station_characteristics",
295-
"type": "DataRecord",
296-
"label": "Station Characteristics",
297-
"fields": char_items,
298-
},
299-
],
300-
"capabilities": [
301-
{
302-
"name": "publisher_capabilities",
303-
"type": "DataRecord",
304-
"label": "Publisher Capabilities",
305-
"capabilities": [
306-
{"type": "Quantity", "name": "update_interval",
307-
"definition": "http://qudt.org/vocab/quantitykind/Period",
308-
"label": "Publish Interval", "uom": {"code": "s"}, "value": 300.0},
309-
{"type": "Text", "name": "data_source",
310-
"definition": "http://sensorml.com/ont/swe/property/DataSource",
311-
"label": "Data Source", "value": "AviationWeather.gov REST API (METAR JSON)"},
312-
],
313-
},
314-
],
315+
# NOTE: characteristics/capabilities are part of OGC SensorML JSON encoding
316+
# but the strict csapi-go-v2 server does not accept them on the
317+
# SystemSensorMLFeature struct (see empirical probe 2026-05-09).
318+
# Field-elevation, station_type, operator, and update_interval information
319+
# is preserved in identifiers/classifiers/position above. char_items
320+
# (operator, station_type, faa_id, field_elevation) are intentionally not
321+
# serialised here; restore once upstream adds these fields back.
315322
"position": {
316323
"type": "Point",
317324
"coordinates": [station["lon"], station["lat"]],
@@ -336,33 +343,24 @@ def _datastream_schema(icao_id: str = "") -> dict:
336343
cloud_base_ft - Lowest cloud base (feet AGL)
337344
raw_metar - Raw METAR text
338345
"""
339-
uid_suffix = f":{icao_id.lower()}" if icao_id else ""
346+
# NOTE: Strict csapi-go-v2 rejects 'documentation', 'characteristics', and
347+
# SWE Time field 'referenceTime'. Keeping body to fields the server accepts.
340348
return {
341-
"uid": f"urn:os4csapi:datastream:awx{uid_suffix}:metarObs:v1",
342349
"outputName": DS_OUTPUT_NAME,
343350
"name": "METAR Observation",
344351
"description": (
345352
"Decoded METAR aviation weather observation from an AviationWeather.gov station. "
346353
"Includes temperature, dew point, wind, visibility, altimeter setting, cloud layers, "
347354
"flight category, and the raw METAR string. Some fields may be NaN if not reported."
348355
),
349-
"documentation": [
350-
{"title": "AviationWeather Data API", "href": AWX_API_DOC, "rel": "documentation"},
351-
{"title": "METAR Decode Key", "href": "https://www.weather.gov/media/wrh/mesowest/metar_decode_key.pdf", "rel": "describedby"},
352-
],
353-
"characteristics": [
354-
{"label": "Source Format", "value": "JSON via AviationWeather.gov REST API"},
355-
{"label": "Nominal Availability", "value": "Hourly METAR; SPECI on significant changes"},
356-
{"label": "Quality Control", "value": "NWS/FAA automated and manual QC applied"},
357-
],
358356
"schema": {
359357
"obsFormat": "application/om+json",
360358
"resultSchema": {
361359
"type": "DataRecord",
362360
"label": "METAR Observation",
363361
"description": "Decoded METAR aviation weather observation",
364362
"fields": [
365-
{"type": "Time", "name": "timestamp", "label": "Observation Time", "definition": "http://www.opengis.net/def/property/OGC/0/SamplingTime", "referenceTime": "1970-01-01T00:00:00Z", "uom": {"code": "s"}},
363+
{"type": "Time", "name": "timestamp", "label": "Observation Time", "definition": "http://www.opengis.net/def/property/OGC/0/SamplingTime", "uom": {"code": "s"}},
366364
{"type": "Text", "name": "stationId", "label": "ICAO Station ID", "definition": "http://sensorml.com/ont/swe/property/StationID"},
367365
{"type": "Quantity", "name": "lat_deg", "label": "Latitude", "definition": "http://sensorml.com/ont/swe/property/GeodeticLatitude", "uom": {"code": "deg"}},
368366
{"type": "Quantity", "name": "lon_deg", "label": "Longitude", "definition": "http://sensorml.com/ont/swe/property/GeodeticLongitude", "uom": {"code": "deg"}},
@@ -391,17 +389,14 @@ def _deploy_root() -> dict:
391389
"coordinates": [-111.5, 33.0],
392390
},
393391
"properties": {
394-
"uid": DEPLOY_ROOT_UID,
395392
"featureType": "sosa:Deployment",
393+
"uid": DEPLOY_ROOT_UID,
396394
"name": "AviationWeather METAR Demo Deployment",
397395
"description": (
398396
"Top-level CSAPI deployment grouping for AviationWeather.gov METAR stations "
399397
"published by OSHConnect-Python. This grouping represents the demo / integration "
400398
"scope, not a single physical field deployment."
401399
),
402-
"documentation": [
403-
{"title": "AviationWeather.gov", "href": AWX_HOME, "rel": "about"},
404-
],
405400
"validTime": [VALID_TIME_START, ".."],
406401
},
407402
}
@@ -415,17 +410,13 @@ def _deploy_group() -> dict:
415410
"coordinates": [-111.5, 33.0],
416411
},
417412
"properties": {
418-
"uid": DEPLOY_GROUP_UID,
419413
"featureType": "sosa:Deployment",
414+
"uid": DEPLOY_GROUP_UID,
420415
"name": "AviationWeather METAR Stations",
421416
"description": (
422417
"Grouping deployment for curated AviationWeather.gov METAR stations. Each child "
423418
"deployment links a station/system resource to the demo deployment tree."
424419
),
425-
"documentation": [
426-
{"title": "AviationWeather.gov", "href": AWX_HOME, "rel": "about"},
427-
{"title": "AviationWeather Data API", "href": AWX_API_DOC, "rel": "documentation"},
428-
],
429420
"validTime": [VALID_TIME_START, ".."],
430421
},
431422
}
@@ -440,8 +431,8 @@ def _deploy_station(station: dict, system_server_id: str) -> dict:
440431
"coordinates": [station["lon"], station["lat"]],
441432
},
442433
"properties": {
443-
"uid": _deploy_uid(icao_id),
444434
"featureType": "sosa:Deployment",
435+
"uid": _deploy_uid(icao_id),
445436
"name": f"METAR {icao_id} Feed",
446437
"description": f"AviationWeather METAR station {icao_id} ({station['name']}) observation feed.",
447438
"validTime": [VALID_TIME_START, ".."],
@@ -450,10 +441,6 @@ def _deploy_station(station: dict, system_server_id: str) -> dict:
450441
"uid": _system_uid(icao_id),
451442
"title": f"AWX {icao_id}",
452443
},
453-
"links": [
454-
{"rel": "about", "title": "METAR Data", "href": _station_page_url(icao_id)},
455-
{"rel": "alternate", "title": "API Endpoint", "href": _station_api_url(icao_id)},
456-
],
457444
},
458445
}
459446

@@ -513,8 +500,10 @@ def bootstrap(*, clean: bool = False, clean_only: bool = False,
513500

514501
# ── Procedure ─────────────────────────────────────────────────────
515502
print(" ── Procedures ──")
516-
proc_id = ensure_procedure(base_url, auth, PROC_UID, PROCEDURE_BODY,
517-
dry_run=dry_run, stats=stats)
503+
proc_id = ensure_procedure(base_url, auth, PROC_UID, PROCEDURE_BODY_STUB,
504+
sml_body=PROCEDURE_SML,
505+
dry_run=dry_run, stats=stats,
506+
force_sml=force_sml)
518507

519508
# ── Systems + Datastreams ─────────────────────────────────────────
520509
print(" ── Systems + Datastreams ──")

0 commit comments

Comments
 (0)