@@ -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 :
0 commit comments