Skip to content

Commit efe176a

Browse files
committed
fix(bootstrap): strip SML-only fields before POST, wrap SML PUT in try/except, cascade root deployment delete
1 parent d0b7ae8 commit efe176a

8 files changed

Lines changed: 65 additions & 42 deletions

File tree

publishers/aviation_wx/bootstrap_aviation_wx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ def clean_all(base_url: str, auth: str, stations: list[dict],
460460
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID,
461461
dry_run=dry_run, stats=stats)
462462
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
463-
dry_run=dry_run, stats=stats)
463+
dry_run=dry_run, stats=stats, cascade=True)
464464

465465
# Systems (datastreams deleted automatically via cascade)
466466
for st in reversed(stations):

publishers/bootstrap_helpers.py

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -302,34 +302,43 @@ def find_datastream(base_url: str, auth: str, system_id: str,
302302
_STRICT_BOOTSTRAP = os.environ.get("OS4CSAPI_STRICT_BOOTSTRAP", "").lower() in ("1", "true", "yes")
303303

304304

305-
def _warn_if_sml_fields_in_stub(stub: dict, label: str) -> None:
306-
"""Loud warning (or exception in strict mode) if a caller passes a 'stub'
307-
body whose `properties` contain SensorML-only fields.
305+
def _sanitize_stub(stub: dict, label: str) -> dict:
306+
"""Return a copy of stub with SensorML-only fields stripped from properties.
308307
309-
These fields will be silently dropped server-side on a geo+json POST.
310-
Callers must split SensorML metadata out into a separate ``sml_body``
311-
and let the helper PUT it with ``Content-Type: application/sml+json``.
308+
Strict CSAPI servers (e.g. csapi-go-v2) reject fields like ``keywords``,
309+
``documentation``, and ``documents`` in the GeoJSON POST body with HTTP 400.
310+
This function removes those fields before the POST so that all servers work.
312311
313-
Set OS4CSAPI_STRICT_BOOTSTRAP=1 to convert the warning to RuntimeError
314-
recommended for tests and CI.
312+
In strict mode (OS4CSAPI_STRICT_BOOTSTRAP=1) a RuntimeError is raised
313+
instead — useful for CI to catch callers that should use sml_body instead.
315314
"""
315+
import copy
316316
if not isinstance(stub, dict):
317-
return
317+
return stub
318318
props = stub.get("properties", stub)
319319
if not isinstance(props, dict):
320-
return
320+
return stub
321321
leaked = sorted(SML_ONLY_FIELDS & set(props.keys()))
322322
if not leaked:
323-
return
323+
return stub
324324
msg = (
325325
f"[ENCODING-CONTRACT] {label}: stub body carries SensorML-only "
326-
f"field(s) under `properties`: {leaked}. These will be silently "
327-
f"dropped (or 400-rejected by strict servers) on the geo+json POST. "
326+
f"field(s) under `properties`: {leaked}. These will be stripped "
327+
f"before POST (strict servers return 400 on unknown fields). "
328328
f"Move them into a separate sml_body argument."
329329
)
330330
if _STRICT_BOOTSTRAP:
331331
raise RuntimeError(msg)
332332
print(f" [WARN] {msg}")
333+
stub = copy.deepcopy(stub)
334+
target = stub.get("properties", stub)
335+
for field in leaked:
336+
target.pop(field, None)
337+
return stub
338+
339+
340+
# Keep old name as alias for any callers outside this module
341+
_warn_if_sml_fields_in_stub = _sanitize_stub
333342

334343

335344
# ═══════════════════════════════════════════════════════════════════════════
@@ -361,17 +370,20 @@ def ensure_procedure(base_url: str, auth: str, uid: str, stub_body: dict,
361370
Callers MUST keep SensorML metadata out of the stub. The
362371
``_warn_if_sml_fields_in_stub`` guardrail catches accidental leakage.
363372
"""
364-
_warn_if_sml_fields_in_stub(stub_body, f"ensure_procedure({uid})")
373+
stub_body = _sanitize_stub(stub_body, f"ensure_procedure({uid})")
365374

366375
existing = find_by_uid(base_url, auth, "procedures", uid)
367376
if existing:
368377
if force_sml and sml_body:
369378
if dry_run:
370379
print(f" [DRY] Would force-PUT SML for procedure {uid} (id={existing})")
371380
else:
372-
api_put(base_url, f"procedures/{existing}", sml_body, auth,
373-
content_type="application/sml+json")
374-
print(f" [SML] Force-PUT SensorML for procedure {uid} (id={existing})")
381+
try:
382+
api_put(base_url, f"procedures/{existing}", sml_body, auth,
383+
content_type="application/sml+json")
384+
print(f" [SML] Force-PUT SensorML for procedure {uid} (id={existing})")
385+
except Exception as exc:
386+
print(f" [WARN] SML PUT skipped for procedure {uid} (id={existing}): {exc}")
375387
if stats:
376388
stats.setdefault("sml_updated", 0)
377389
stats["sml_updated"] += 1
@@ -392,8 +404,11 @@ def ensure_procedure(base_url: str, auth: str, uid: str, stub_body: dict,
392404

393405
# Step 2: PUT SensorML if provided
394406
if new_id and sml_body:
395-
api_put(base_url, f"procedures/{new_id}", sml_body, auth,
396-
content_type="application/sml+json")
407+
try:
408+
api_put(base_url, f"procedures/{new_id}", sml_body, auth,
409+
content_type="application/sml+json")
410+
except Exception as exc:
411+
print(f" [WARN] SML PUT skipped for procedure {uid} (id={new_id}): {exc}")
397412

398413
print(f" [OK] Created procedure {uid} → id={new_id}")
399414
if stats:
@@ -413,17 +428,20 @@ def ensure_system(base_url: str, auth: str, uid: str, stub_body: dict,
413428
When *force_sml* is True and the system already exists, the SML body is
414429
PUT again (useful for correcting previously-broken SML payloads).
415430
"""
416-
_warn_if_sml_fields_in_stub(stub_body, f"ensure_system({uid})")
431+
stub_body = _sanitize_stub(stub_body, f"ensure_system({uid})")
417432

418433
existing = find_by_uid(base_url, auth, "systems", uid)
419434
if existing:
420435
if force_sml and sml_body:
421436
if dry_run:
422437
print(f" [DRY] Would force-PUT SML for system {uid} (id={existing})")
423438
else:
424-
api_put(base_url, f"systems/{existing}", sml_body, auth,
425-
content_type="application/sml+json")
426-
print(f" [SML] Force-PUT SensorML for system {uid} (id={existing})")
439+
try:
440+
api_put(base_url, f"systems/{existing}", sml_body, auth,
441+
content_type="application/sml+json")
442+
print(f" [SML] Force-PUT SensorML for system {uid} (id={existing})")
443+
except Exception as exc:
444+
print(f" [WARN] SML PUT skipped for system {uid} (id={existing}): {exc}")
427445
if stats:
428446
stats.setdefault("sml_updated", 0)
429447
stats["sml_updated"] += 1
@@ -444,8 +462,11 @@ def ensure_system(base_url: str, auth: str, uid: str, stub_body: dict,
444462

445463
# Step 2: PUT SensorML if provided
446464
if new_id and sml_body:
447-
api_put(base_url, f"systems/{new_id}", sml_body, auth,
448-
content_type="application/sml+json")
465+
try:
466+
api_put(base_url, f"systems/{new_id}", sml_body, auth,
467+
content_type="application/sml+json")
468+
except Exception as exc:
469+
print(f" [WARN] SML PUT skipped for system {uid} (id={new_id}): {exc}")
449470

450471
print(f" [OK] Created system {uid} → id={new_id}")
451472
if stats:
@@ -512,7 +533,7 @@ def ensure_deployment(base_url: str, auth: str, uid: str, stub_body: dict,
512533
Callers MUST keep SensorML metadata out of the stub. The
513534
``_warn_if_sml_fields_in_stub`` guardrail catches accidental leakage.
514535
"""
515-
_warn_if_sml_fields_in_stub(stub_body, f"ensure_deployment({uid})")
536+
stub_body = _sanitize_stub(stub_body, f"ensure_deployment({uid})")
516537

517538
# Check top-level deployments first
518539
existing = find_by_uid(base_url, auth, "deployments", uid)
@@ -525,9 +546,12 @@ def ensure_deployment(base_url: str, auth: str, uid: str, stub_body: dict,
525546
if dry_run:
526547
print(f" [DRY] Would force-PUT SML for deployment {uid} (id={existing})")
527548
else:
528-
api_put(base_url, f"deployments/{existing}", sml_body, auth,
529-
content_type="application/sml+json")
530-
print(f" [SML] Force-PUT SensorML for deployment {uid} (id={existing})")
549+
try:
550+
api_put(base_url, f"deployments/{existing}", sml_body, auth,
551+
content_type="application/sml+json")
552+
print(f" [SML] Force-PUT SensorML for deployment {uid} (id={existing})")
553+
except Exception as exc:
554+
print(f" [WARN] SML PUT skipped for deployment {uid} (id={existing}): {exc}")
531555
if stats:
532556
stats.setdefault("sml_updated", 0)
533557
stats["sml_updated"] += 1
@@ -552,8 +576,11 @@ def ensure_deployment(base_url: str, auth: str, uid: str, stub_body: dict,
552576

553577
# Step 2: PUT SensorML against the canonical /deployments/{id} path
554578
if new_id and sml_body:
555-
api_put(base_url, f"deployments/{new_id}", sml_body, auth,
556-
content_type="application/sml+json")
579+
try:
580+
api_put(base_url, f"deployments/{new_id}", sml_body, auth,
581+
content_type="application/sml+json")
582+
except Exception as exc:
583+
print(f" [WARN] SML PUT skipped for deployment {uid} (id={new_id}): {exc}")
557584

558585
print(f" [OK] Created deployment {uid} → id={new_id}")
559586
if stats:

publishers/coops/bootstrap_coops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ def clean_all(base_url: str, auth: str, stations: list[dict],
640640
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID,
641641
dry_run=dry_run, stats=stats)
642642
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
643-
dry_run=dry_run, stats=stats)
643+
dry_run=dry_run, stats=stats, cascade=True)
644644

645645
# Systems (datastreams deleted automatically via cascade)
646646
for st in reversed(stations):

publishers/nws/bootstrap_nws.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -581,9 +581,7 @@ def clean_all(base_url: str, auth: str, stations: list[dict],
581581
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID,
582582
dry_run=dry_run, stats=stats)
583583
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
584-
dry_run=dry_run, stats=stats)
585-
586-
# Systems (datastreams cascade-deleted by server)
584+
dry_run=dry_run, stats=stats, cascade=True)
587585
for st in reversed(stations):
588586
clean_resource(base_url, auth, "systems", _system_uid(st["id"]),
589587
dry_run=dry_run, stats=stats, cascade=True)

publishers/opensky/bootstrap_opensky.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ def clean_all(base_url, auth, *, dry_run=False, stats):
543543
clean_resource(base_url, auth, "deployments", DEPLOY_FEED_UID,
544544
dry_run=dry_run, stats=stats)
545545
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
546-
dry_run=dry_run, stats=stats)
546+
dry_run=dry_run, stats=stats, cascade=True)
547547
clean_resource(base_url, auth, "systems", SYSTEM_UID,
548548
dry_run=dry_run, stats=stats, cascade=True)
549549
clean_resource(base_url, auth, "procedures", PROC_UID,

publishers/usgs_eq/bootstrap_usgs_eq.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ def clean_all(base_url, auth, *, dry_run=False, stats):
459459
clean_resource(base_url, auth, "deployments", DEPLOY_FEED_UID,
460460
dry_run=dry_run, stats=stats)
461461
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
462-
dry_run=dry_run, stats=stats)
462+
dry_run=dry_run, stats=stats, cascade=True)
463463
clean_resource(base_url, auth, "systems", SYSTEM_UID,
464464
dry_run=dry_run, stats=stats, cascade=True)
465465
clean_resource(base_url, auth, "procedures", PROC_UID,

publishers/usgs_nims/bootstrap_usgs_nims.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,7 @@ def clean_all(base_url: str, auth: str, cameras: list[dict],
413413
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID,
414414
dry_run=dry_run, stats=stats)
415415
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
416-
dry_run=dry_run, stats=stats)
417-
418-
# Datastreams on existing systems (find + delete individually)
416+
dry_run=dry_run, stats=stats, cascade=True)
419417
for cam in cameras:
420418
sys_id = find_by_uid(base_url, auth, "systems", _system_uid(cam["nwisId"]))
421419
if not sys_id:

publishers/usgs_water/bootstrap_usgs_water.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ def clean_all(base_url: str, auth: str, stations: list[dict],
776776
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID,
777777
dry_run=dry_run, stats=stats)
778778
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID,
779-
dry_run=dry_run, stats=stats)
779+
dry_run=dry_run, stats=stats, cascade=True)
780780

781781
# Systems (datastreams cascade-deleted by server)
782782
for st in reversed(stations):

0 commit comments

Comments
 (0)