Skip to content

Commit 2ef4e95

Browse files
committed
fix(nws): split procedure and deployment bodies into stub + SensorML
Replaces PROCEDURE_BODY (single mixed-encoding dict) with _procedure_stub and _procedure_sml; strips documentation arrays from _deploy_root and _deploy_group stubs and adds matching _deploy_root_sml / _deploy_group_sml; threads force_sml through procedure and deployment create calls so --force-sml now repairs them in place. NWS is the canonical example for the same refactor that needs to land across the other 9 publishers (E.2 follow-up). Refs: #5
1 parent 346f029 commit 2ef4e95

1 file changed

Lines changed: 168 additions & 47 deletions

File tree

publishers/nws/bootstrap_nws.py

Lines changed: 168 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def _points_url(lat: float, lon: float) -> str:
105105
# Resource definitions
106106
# ═══════════════════════════════════════════════════════════════════════════
107107

108-
PROCEDURE_BODY = {
108+
PROCEDURE_STUB = {
109109
"type": "Feature",
110110
"geometry": None,
111111
"properties": {
@@ -119,48 +119,128 @@ def _points_url(lat: float, lon: float) -> str:
119119
"ASOS/AWOS; nominal station reporting is hourly with special observations as conditions "
120120
"warrant, and API publication may lag upstream MADIS availability by up to about 20 minutes."
121121
),
122+
"validTime": [VALID_TIME_START, ".."],
123+
},
124+
}
125+
126+
127+
def _procedure_sml() -> dict:
128+
"""SensorML body for the NWS Surface Observation procedure.
129+
130+
Encoding: ``application/sml+json``. PUT against ``/procedures/{id}``
131+
after the geo+json stub POST. Field shapes follow the SensorML JSON
132+
encoding (``documents``, ``contacts.organisationName``,
133+
``contactInfo``) so the CSAPI server persists into the corresponding
134+
procedure-table columns.
135+
"""
136+
return {
137+
"type": "SimpleProcess",
138+
"id": PROC_UID,
139+
"uniqueId": PROC_UID,
140+
"definition": "http://www.opengis.net/def/procedure/observation",
141+
"label": "NWS Surface Observation v1",
142+
"description": (
143+
"Observing procedure for NWS ASOS/AWOS surface weather stations exposed through "
144+
"api.weather.gov. Latest station observations are normalized to SI-oriented fields "
145+
"(degC, Pa, km/h, m) and published as flat JSON result objects. Nominal station "
146+
"reporting is hourly with special observations as conditions warrant; API publication "
147+
"may lag upstream MADIS availability by up to about 20 minutes."
148+
),
122149
"keywords": [
123-
"NWS",
124-
"NOAA",
125-
"ASOS",
126-
"AWOS",
127-
"surface weather",
128-
"METAR",
129-
"api.weather.gov",
130-
"aviation weather",
150+
"NWS", "NOAA", "ASOS", "AWOS",
151+
"surface weather", "METAR",
152+
"api.weather.gov", "aviation weather",
131153
],
132-
"documentation": [
133-
{"title": "NWS API Web Service", "href": NWS_API_DOCS, "rel": "documentation"},
134-
{"title": "NWS OpenAPI Specification", "href": NWS_OPENAPI, "rel": "describedby"},
135-
{"title": "NWS ASOS Program", "href": NWS_ASOS_PAGE, "rel": "about"},
136-
{"title": "NWS ASOS Equipment FAQ", "href": NWS_ASOS_EQUIP, "rel": "related"},
137-
{"title": "NWS ASOS Contact", "href": NWS_ASOS_CONTACT, "rel": "contact"},
154+
"identifiers": [
155+
{"definition": "http://sensorml.com/ont/swe/property/ShortName",
156+
"label": "Short Name", "value": "NWS Surface Observation"},
157+
{"definition": "http://sensorml.com/ont/swe/property/LongName",
158+
"label": "Long Name",
159+
"value": "National Weather Service ASOS/AWOS Surface Observation Procedure v1"},
160+
{"definition": "http://sensorml.com/ont/swe/property/UniqueID",
161+
"label": "OS4CSAPI UID", "value": PROC_UID},
162+
],
163+
"classifiers": [
164+
{"definition": "http://sensorml.com/ont/swe/property/IntendedApplication",
165+
"label": "Network", "value": "ASOS / AWOS Surface Weather Observation Network"},
166+
{"definition": "http://sensorml.com/ont/swe/property/SystemRole",
167+
"label": "Operator", "value": "National Weather Service"},
138168
],
139169
"contacts": [
140170
{
141-
"role": "operator",
142-
"organizationName": "National Weather Service",
143-
"website": NWS_API_BASE,
171+
"role": "http://sensorml.com/ont/swe/property/Operator",
172+
"organisationName": "National Weather Service",
173+
"contactInfo": {
174+
"website": NWS_API_BASE,
175+
},
144176
},
145177
{
146-
"role": "support",
147-
"organizationName": "ASOS Operations and Monitoring Center",
148-
"email": NWS_AOMC_EMAIL,
149-
"phone": NWS_AOMC_PHONE_1,
178+
"role": "http://sensorml.com/ont/swe/property/Maintainer",
179+
"organisationName": "ASOS Operations and Monitoring Center",
180+
"contactInfo": {
181+
"website": NWS_ASOS_CONTACT,
182+
"phone": {"voice": NWS_AOMC_PHONE_1},
183+
"email": NWS_AOMC_EMAIL,
184+
"address": {"country": "United States"},
185+
},
186+
},
187+
],
188+
"documents": [
189+
{
190+
"role": "http://dbpedia.org/resource/Documentation",
191+
"name": "NWS API Web Service",
192+
"link": {"href": NWS_API_DOCS, "type": "text/html"},
193+
},
194+
{
195+
"role": "http://dbpedia.org/resource/OpenAPI_Specification",
196+
"name": "NWS OpenAPI Specification",
197+
"link": {"href": NWS_OPENAPI, "type": "application/openapi+json"},
198+
},
199+
{
200+
"role": "http://dbpedia.org/resource/Web_page",
201+
"name": "NWS ASOS Program",
202+
"link": {"href": NWS_ASOS_PAGE, "type": "text/html"},
203+
},
204+
{
205+
"role": "http://dbpedia.org/resource/FAQ",
206+
"name": "NWS ASOS Equipment FAQ",
207+
"link": {"href": NWS_ASOS_EQUIP, "type": "text/html"},
208+
},
209+
{
210+
"role": "http://dbpedia.org/resource/Contact",
211+
"name": "NWS ASOS Contact",
212+
"link": {"href": NWS_ASOS_CONTACT, "type": "text/html"},
213+
},
214+
],
215+
"characteristics": [
216+
{
217+
"label": "Lineage",
218+
"definition": "http://sensorml.com/ont/swe/property/Lineage",
219+
"characteristic": [
220+
{"label": "Source", "value": "NOAA / National Weather Service"},
221+
{"label": "Upstream",
222+
"value": "MADIS-mediated station observations exposed by api.weather.gov"},
223+
{"label": "Normalization",
224+
"value": "Publisher normalizes selected values to SI-oriented fields (degC, Pa, km/h, m)."},
225+
],
226+
},
227+
{
228+
"label": "Usage Constraints",
229+
"definition": "http://sensorml.com/ont/swe/property/UsageConstraints",
230+
"characteristic": [
231+
{"label": "User-Agent Required", "value": "true"},
232+
{"label": "Rate Limit",
233+
"value": "NWS API is open data with unpublished but reasonable rate limits."},
234+
],
150235
},
151236
],
152-
"lineage": {
153-
"source": "NOAA / National Weather Service",
154-
"upstream": "MADIS-mediated station observations exposed by api.weather.gov",
155-
"normalization": "Publisher normalizes selected values to SI-oriented fields (degC, Pa, km/h, m).",
156-
},
157-
"usageConstraints": {
158-
"userAgentRequired": True,
159-
"rateLimitNote": "NWS API is open data with unpublished but reasonable rate limits.",
160-
},
161237
"validTime": [VALID_TIME_START, ".."],
162-
},
163-
}
238+
}
239+
240+
241+
def _procedure_stub() -> dict:
242+
"""Geo+json stub for the procedure (stable copy of PROCEDURE_STUB)."""
243+
return PROCEDURE_STUB
164244

165245

166246
def _system_stub(station: dict, proc_id: str) -> dict:
@@ -382,6 +462,20 @@ def _datastream_schema(station_id: str = "") -> dict:
382462
}
383463

384464

465+
_DEPLOY_DOCUMENTS = [
466+
{
467+
"role": "http://dbpedia.org/resource/Documentation",
468+
"name": "NWS API Web Service",
469+
"link": {"href": NWS_API_DOCS, "type": "text/html"},
470+
},
471+
{
472+
"role": "http://dbpedia.org/resource/Web_page",
473+
"name": "NWS ASOS Program",
474+
"link": {"href": NWS_ASOS_PAGE, "type": "text/html"},
475+
},
476+
]
477+
478+
385479
def _deploy_root() -> dict:
386480
return {
387481
"type": "Feature",
@@ -394,15 +488,24 @@ def _deploy_root() -> dict:
394488
"featureType": "sosa:Deployment",
395489
"name": "NWS Weather Demo",
396490
"description": "Demonstration deployment for NWS surface weather observation systems published into CSAPI.",
397-
"documentation": [
398-
{"title": "NWS API Web Service", "href": NWS_API_DOCS, "rel": "documentation"},
399-
{"title": "NWS ASOS Program", "href": NWS_ASOS_PAGE, "rel": "about"},
400-
],
401491
"validTime": [VALID_TIME_START, ".."],
402492
},
403493
}
404494

405495

496+
def _deploy_root_sml() -> dict:
497+
"""SensorML body for the root demo deployment node."""
498+
return {
499+
"type": "Deployment",
500+
"id": DEPLOY_ROOT_UID,
501+
"uniqueId": DEPLOY_ROOT_UID,
502+
"label": "NWS Weather Demo",
503+
"description": "Demonstration deployment for NWS surface weather observation systems published into CSAPI.",
504+
"documents": list(_DEPLOY_DOCUMENTS),
505+
"validTime": [VALID_TIME_START, ".."],
506+
}
507+
508+
406509
def _deploy_group() -> dict:
407510
return {
408511
"type": "Feature",
@@ -415,15 +518,25 @@ def _deploy_group() -> dict:
415518
"featureType": "sosa:Deployment",
416519
"name": "NWS Weather Stations",
417520
"description": "NWS ASOS/AWOS surface observation stations across the United States used for the OS4CSAPI demo.",
418-
"documentation": [
419-
{"title": "NWS API Web Service", "href": NWS_API_DOCS, "rel": "documentation"},
420-
{"title": "NWS ASOS Program", "href": NWS_ASOS_PAGE, "rel": "about"},
421-
],
422521
"validTime": [VALID_TIME_START, ".."],
423522
},
424523
}
425524

426525

526+
def _deploy_group_sml() -> dict:
527+
"""SensorML body for the NWS Weather Stations deployment grouping."""
528+
return {
529+
"type": "Deployment",
530+
"id": DEPLOY_GROUP_UID,
531+
"uniqueId": DEPLOY_GROUP_UID,
532+
"label": "NWS Weather Stations",
533+
"description": "NWS ASOS/AWOS surface observation stations across the United States used for the OS4CSAPI demo.",
534+
"keywords": ["NWS", "NOAA", "ASOS", "AWOS", "surface weather"],
535+
"documents": list(_DEPLOY_DOCUMENTS),
536+
"validTime": [VALID_TIME_START, ".."],
537+
}
538+
539+
427540
def _deploy_station(station: dict, system_server_id: str) -> dict:
428541
return {
429542
"type": "Feature",
@@ -508,8 +621,10 @@ def bootstrap(*, clean: bool = False, clean_only: bool = False,
508621

509622
# ── Procedure ─────────────────────────────────────────────────────
510623
print(" ── Procedures ──")
511-
proc_id = ensure_procedure(base_url, auth, PROC_UID, PROCEDURE_BODY,
512-
dry_run=dry_run, stats=stats)
624+
proc_id = ensure_procedure(base_url, auth, PROC_UID,
625+
_procedure_stub(), _procedure_sml(),
626+
dry_run=dry_run, stats=stats,
627+
force_sml=force_sml)
513628

514629
# ── Systems + Datastreams ─────────────────────────────────────────
515630
print(" ── Systems + Datastreams ──")
@@ -534,15 +649,21 @@ def bootstrap(*, clean: bool = False, clean_only: bool = False,
534649

535650
# ── Deployment tree ───────────────────────────────────────────────
536651
print(" ── Deployments ──")
537-
root_id = ensure_deployment(base_url, auth, DEPLOY_ROOT_UID, _deploy_root(),
538-
dry_run=dry_run, stats=stats)
539-
group_id = ensure_deployment(base_url, auth, DEPLOY_GROUP_UID, _deploy_group(),
652+
root_id = ensure_deployment(base_url, auth, DEPLOY_ROOT_UID,
653+
_deploy_root(), _deploy_root_sml(),
654+
dry_run=dry_run, stats=stats,
655+
force_sml=force_sml)
656+
group_id = ensure_deployment(base_url, auth, DEPLOY_GROUP_UID,
657+
_deploy_group(), _deploy_group_sml(),
540658
parent_id=root_id,
541-
dry_run=dry_run, stats=stats)
659+
dry_run=dry_run, stats=stats,
660+
force_sml=force_sml)
542661

543662
for st in stations:
544663
sys_id = system_ids.get(st["id"])
545664
if sys_id or dry_run:
665+
# _deploy_station carries no SensorML-only fields under properties;
666+
# geo+json stub is sufficient.
546667
ensure_deployment(base_url, auth, _deploy_uid(st["id"]),
547668
_deploy_station(st, sys_id or "pending"),
548669
parent_id=group_id,

0 commit comments

Comments
 (0)