spatialdds 0.3.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spatialdds-0.3.1 → spatialdds-0.4.0}/PKG-INFO +28 -4
- {spatialdds-0.3.1 → spatialdds-0.4.0}/README.md +27 -3
- {spatialdds-0.3.1 → spatialdds-0.4.0}/pyproject.toml +1 -1
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/__init__.py +1 -1
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/serve.py +45 -4
- spatialdds-0.4.0/spatialdds/server/commands.py +131 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/gateway.py +26 -6
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/gw_http.py +108 -3
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/PKG-INFO +28 -4
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/SOURCES.txt +1 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/setup.cfg +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/idl/core.idl +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/idl/discovery.idl +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/idl/types.idl +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/builtin_Time.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_KV.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_Vec2.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Edge.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Node.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileKey.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Announce.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Depart.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_KV.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Transform.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_Entity.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/serializer_tables.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/canonical.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/canonjson.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/client.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/assets/duck.gltf +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/assets/fountain-label.txt +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/assets/fountain-marker.gltf +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/fountain-anchors.mcap +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/manifests/content-fountain-label.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/manifests/content-fountain-marker.json +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/errors.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/records.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/__init__.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/idlmodel.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/pin.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/publish.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/webjson.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/server/xcdr2.py +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/dependency_links.txt +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/entry_points.txt +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/requires.txt +0 -0
- {spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spatialdds
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command.
|
|
5
5
|
Author: Open AR Cloud
|
|
6
6
|
License: Apache-2.0
|
|
@@ -356,13 +356,37 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
|
356
356
|
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
357
357
|
site, nowhere else.
|
|
358
358
|
|
|
359
|
-
##
|
|
359
|
+
## New in 0.4.0: a command surface on the server half
|
|
360
|
+
|
|
361
|
+
`spatialdds[server]` now implements Appendix N.6. A command is a POST to the
|
|
362
|
+
lane that accepts one, and an embedder registers what a verb means:
|
|
363
|
+
|
|
364
|
+
| Part | What it is |
|
|
365
|
+
| --- | --- |
|
|
366
|
+
| the seam | `spatialdds.server.commands.Registry().register(fqn, handler)` |
|
|
367
|
+
| a handler gets | the validated, lifted record — the same JSON a GET would serve |
|
|
368
|
+
| a handler returns | `Outcome.accept()` or `Outcome.decline("why")` |
|
|
369
|
+
| the wire | `202` accepted, `409` declined with the reason, `405` where no handler is registered |
|
|
370
|
+
|
|
371
|
+
`python -m spatialdds.serve --demo` registers one so the surface is reachable
|
|
372
|
+
without writing a backend. An endpoint with no handlers is northbound-only and
|
|
373
|
+
every command POST is a clean `405`, which is what N.6 requires and what every
|
|
374
|
+
earlier version did.
|
|
375
|
+
|
|
376
|
+
**The client half has no command method.** This release adds the surface, not
|
|
377
|
+
a helper for calling it — `requests.post(topic_url, json=record)` with the
|
|
378
|
+
operator token is the whole of it, and `docs/WIRE.md` documents the response
|
|
379
|
+
shapes. Said plainly because "commands are supported" invites a reader to look
|
|
380
|
+
for `hit.command(...)`, and there isn't one.
|
|
381
|
+
|
|
382
|
+
## Not in 0.4.0
|
|
360
383
|
|
|
361
384
|
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
362
385
|
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
363
|
-
writes, and no DDS dependency of any kind. The
|
|
386
|
+
writes, no client-side command helper, and no DDS dependency of any kind. The
|
|
387
|
+
wire is HTTP and a WebSocket.
|
|
364
388
|
|
|
365
|
-
0.3.1
|
|
389
|
+
0.3.1 was this page: two code blocks on it had no gate behind them and both
|
|
366
390
|
were broken, and a long description is baked into the artifact, so the page on
|
|
367
391
|
PyPI only heals with a release. Every block here is executed by a gate now,
|
|
368
392
|
and the gate also refuses a relative link, an empty table header and a serve
|
|
@@ -332,13 +332,37 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
|
332
332
|
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
333
333
|
site, nowhere else.
|
|
334
334
|
|
|
335
|
-
##
|
|
335
|
+
## New in 0.4.0: a command surface on the server half
|
|
336
|
+
|
|
337
|
+
`spatialdds[server]` now implements Appendix N.6. A command is a POST to the
|
|
338
|
+
lane that accepts one, and an embedder registers what a verb means:
|
|
339
|
+
|
|
340
|
+
| Part | What it is |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| the seam | `spatialdds.server.commands.Registry().register(fqn, handler)` |
|
|
343
|
+
| a handler gets | the validated, lifted record — the same JSON a GET would serve |
|
|
344
|
+
| a handler returns | `Outcome.accept()` or `Outcome.decline("why")` |
|
|
345
|
+
| the wire | `202` accepted, `409` declined with the reason, `405` where no handler is registered |
|
|
346
|
+
|
|
347
|
+
`python -m spatialdds.serve --demo` registers one so the surface is reachable
|
|
348
|
+
without writing a backend. An endpoint with no handlers is northbound-only and
|
|
349
|
+
every command POST is a clean `405`, which is what N.6 requires and what every
|
|
350
|
+
earlier version did.
|
|
351
|
+
|
|
352
|
+
**The client half has no command method.** This release adds the surface, not
|
|
353
|
+
a helper for calling it — `requests.post(topic_url, json=record)` with the
|
|
354
|
+
operator token is the whole of it, and `docs/WIRE.md` documents the response
|
|
355
|
+
shapes. Said plainly because "commands are supported" invites a reader to look
|
|
356
|
+
for `hit.command(...)`, and there isn't one.
|
|
357
|
+
|
|
358
|
+
## Not in 0.4.0
|
|
336
359
|
|
|
337
360
|
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
338
361
|
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
339
|
-
writes, and no DDS dependency of any kind. The
|
|
362
|
+
writes, no client-side command helper, and no DDS dependency of any kind. The
|
|
363
|
+
wire is HTTP and a WebSocket.
|
|
340
364
|
|
|
341
|
-
0.3.1
|
|
365
|
+
0.3.1 was this page: two code blocks on it had no gate behind them and both
|
|
342
366
|
were broken, and a long description is baked into the artifact, so the page on
|
|
343
367
|
PyPI only heals with a release. Every block here is executed by a gate now,
|
|
344
368
|
and the gate also refuses a relative link, an empty table header and a serve
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "spatialdds"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -37,7 +37,7 @@ __all__ = [
|
|
|
37
37
|
"content_manifest", "geo_anchor", "service_uri", "content_uri",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
__version__ = "0.
|
|
40
|
+
__version__ = "0.4.0"
|
|
41
41
|
#: The SpatialDDS-spec commit whose Appendix N and generated artifacts this
|
|
42
42
|
#: package implements. Package versions and specification versions move
|
|
43
43
|
#: independently; this is the one that says what the wire looks like.
|
|
@@ -42,10 +42,10 @@ def _server():
|
|
|
42
42
|
"""The server half, or one sentence saying how to get it."""
|
|
43
43
|
try:
|
|
44
44
|
from aiohttp import web
|
|
45
|
-
from .server import gateway, gw_http, pin, publish
|
|
45
|
+
from .server import commands, gateway, gw_http, pin, publish
|
|
46
46
|
except ImportError as e: # pragma: no cover
|
|
47
47
|
raise SystemExit(f"spatialdds.serve: {MISSING_SERVER}\n\n({e})")
|
|
48
|
-
return web, gateway, gw_http, pin, publish
|
|
48
|
+
return web, commands, gateway, gw_http, pin, publish
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def build_parser(ap: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
@@ -102,8 +102,33 @@ def build_parser(ap: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
|
102
102
|
return ap
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _demo_command(record: dict):
|
|
106
|
+
"""The `--demo` command handler: both of M.1's outcomes, no side effects.
|
|
107
|
+
|
|
108
|
+
A reference handler that pretended to act would be worse than none: it
|
|
109
|
+
would make the surface look wired to something. So this decides, says
|
|
110
|
+
why, and changes nothing --- which is all a demo registry can honestly do,
|
|
111
|
+
having no backend behind it.
|
|
112
|
+
|
|
113
|
+
`method == "DECLINE_ME"` is declined on purpose. Without a reachable
|
|
114
|
+
decline, the only exercised path would be acceptance, and M.1's whole
|
|
115
|
+
point is that a refusal is a first-class observable outcome; a surface
|
|
116
|
+
that can only say yes has not implemented it.
|
|
117
|
+
"""
|
|
118
|
+
from .server.commands import Outcome # the server half is optional
|
|
119
|
+
if record.get("method") == "DECLINE_ME":
|
|
120
|
+
return Outcome.decline(
|
|
121
|
+
"the demo handler declines this method by arrangement, so a "
|
|
122
|
+
"caller can see M.1's decline path without a backend: a refusal "
|
|
123
|
+
"carries a reason and is not an absence")
|
|
124
|
+
return Outcome.accept(
|
|
125
|
+
"accepted by the --demo handler, which records nothing: this registry "
|
|
126
|
+
"has no backend, and a handler that claimed to act would make the "
|
|
127
|
+
"surface look wired to something it is not")
|
|
128
|
+
|
|
129
|
+
|
|
105
130
|
def main(argv: list[str] | None = None) -> int:
|
|
106
|
-
web, gateway, gw_http, pin, publish = _server()
|
|
131
|
+
web, commands, gateway, gw_http, pin, publish = _server()
|
|
107
132
|
ap = build_parser(argparse.ArgumentParser(
|
|
108
133
|
prog="python -m spatialdds.serve",
|
|
109
134
|
description="Stand up a SpatialDDS registry."))
|
|
@@ -130,6 +155,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
130
155
|
if a.mcap is not None and not a.mcap.is_file():
|
|
131
156
|
raise SystemExit(f"no such recording: {a.mcap}")
|
|
132
157
|
|
|
158
|
+
# The N.6 demo handler. `--demo` exists so the surface is exercisable
|
|
159
|
+
# without a backend, and a command surface nobody can POST to is a route
|
|
160
|
+
# with no evidence behind it -- the conformance suite's first clause had
|
|
161
|
+
# never been exercised by any run in this program precisely because there
|
|
162
|
+
# was nothing to exercise it against.
|
|
163
|
+
#
|
|
164
|
+
# It accepts a `GeoAnchor` command and declines one verb, so both of M.1's
|
|
165
|
+
# outcomes are reachable from a bare `--demo`: acceptance, and a decline
|
|
166
|
+
# carrying a reason. It does nothing to the registry, because `--demo` has
|
|
167
|
+
# no backend to do anything with -- which is the honest shape for a
|
|
168
|
+
# reference handler and the reason its reason string says so.
|
|
169
|
+
cmd_registry = commands.Registry()
|
|
170
|
+
if a.demo:
|
|
171
|
+
cmd_registry.register("spatial.core.GeoAnchor", _demo_command)
|
|
172
|
+
|
|
133
173
|
token = a.token or secrets.token_urlsafe(24)
|
|
134
174
|
st = gateway.build_state(
|
|
135
175
|
a.mcap, a.manifests if a.manifests and a.manifests.is_dir() else None)
|
|
@@ -284,7 +324,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
284
324
|
try:
|
|
285
325
|
web.run_app(gw_http.make_app(st, base, token, a.publish, jr,
|
|
286
326
|
a.assets if a.assets
|
|
287
|
-
and a.assets.is_dir() else None
|
|
327
|
+
and a.assets.is_dir() else None,
|
|
328
|
+
command_registry=cmd_registry),
|
|
288
329
|
host=a.host, port=a.port, print=None)
|
|
289
330
|
except OSError as e:
|
|
290
331
|
# An already-bound port is the one failure where everything downstream
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""N.6 commands: the gateway owns the HTTP leg, a backend supplies the verbs.
|
|
2
|
+
|
|
3
|
+
Appendix N.6 is four sentences long and delegates its conventions to M.1, so
|
|
4
|
+
most of this module is the reasoning that fills the gap between them. The
|
|
5
|
+
ruling this implements: **N.6 is a gateway surface whose semantics a backend
|
|
6
|
+
supplies.** The gateway owns the route, the auth, the validation and the
|
|
7
|
+
response shape; a backend registers a handler per command type and decides
|
|
8
|
+
what a verb means. An endpoint whose backend registers nothing stays
|
|
9
|
+
northbound-only, and its POST stays exactly as conformant as it was before
|
|
10
|
+
this module existed.
|
|
11
|
+
|
|
12
|
+
**Why there is a seam at all.** The first consumer of this surface is
|
|
13
|
+
`SpatialDDS-demo`'s web bridge, which embeds the gateway against a live DDS
|
|
14
|
+
bus: a `goto` arriving as a POST has to leave as a keyed `ModelCommand` on a
|
|
15
|
+
DDS topic, and nothing in this repository knows how to do that or should. So
|
|
16
|
+
the deliverable is not only the route but the boundary — the narrowest thing
|
|
17
|
+
an embedder can implement to own the verbs without owning the HTTP.
|
|
18
|
+
|
|
19
|
+
**What N.6 fixes and what it leaves open.** It fixes that a command is a POST,
|
|
20
|
+
that the response carries the outcome *including an explicit decline*, and
|
|
21
|
+
that an endpoint without a command lane refuses with `405` or `404` and never
|
|
22
|
+
accepts a command it cannot act on. It says nothing about status codes for the
|
|
23
|
+
outcomes themselves, nothing about how a client discovers that commands are
|
|
24
|
+
accepted, and nothing about completion tracking. Those are decided here and
|
|
25
|
+
documented in `docs/WIRE.md`; none of them is invented where the text speaks.
|
|
26
|
+
|
|
27
|
+
**M.1's vocabulary, which N.6 points at.** A decline is "a first-class,
|
|
28
|
+
observable outcome, not an absence", carrying a terminal state and a
|
|
29
|
+
human-readable reason. The value of the pattern is that an observer can
|
|
30
|
+
distinguish *refused* from *not-yet-answered* from *not-for-me*. That is why
|
|
31
|
+
`Outcome` carries a reason on the decline path and why a declined command is
|
|
32
|
+
not reported with a success status: a caller that checks only the status must
|
|
33
|
+
not read a refusal as an acceptance, which would turn M.1's first-class
|
|
34
|
+
outcome back into the absence it exists to replace.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import Any, Callable
|
|
40
|
+
|
|
41
|
+
#: The manifest member that advertises a lane as accepting commands.
|
|
42
|
+
#:
|
|
43
|
+
#: **Implementation-defined, and flagged as a specification gap.** N.7's
|
|
44
|
+
#: topic-entry table is a MUST-carry list of six fields plus two optional
|
|
45
|
+
#: ones, and none of them can say "this lane accepts commands" --- while N.6
|
|
46
|
+
#: says "where an endpoint exposes a command surface" and gives a client no
|
|
47
|
+
#: way to find out whether it does. A client would otherwise have to POST and
|
|
48
|
+
#: read the refusal, which is discovery by trial.
|
|
49
|
+
#:
|
|
50
|
+
#: So this member exists, under this repository's own name, documented in
|
|
51
|
+
#: `docs/WIRE.md`, and filed for 1.9 rather than treated as settled. N.7's
|
|
52
|
+
#: list is a floor rather than a ceiling, so carrying an extra member is
|
|
53
|
+
#: conformant; a *client* may not depend on it until the specification adopts
|
|
54
|
+
#: one, and nothing in this repository reads it except the manifest's own
|
|
55
|
+
#: reader and the conformance suite.
|
|
56
|
+
ADVERTISEMENT = "accepts_commands"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Outcome:
|
|
61
|
+
"""What a backend did with a command.
|
|
62
|
+
|
|
63
|
+
Two states and a reason, which is M.1's vocabulary and not a richer one:
|
|
64
|
+
there is no `PENDING` because N.6 requires no completion tracking and
|
|
65
|
+
inventing it would put this gateway ahead of the specification it
|
|
66
|
+
implements. A backend that needs asynchronous completion reports
|
|
67
|
+
acceptance here and publishes the completion on its own lane, which is
|
|
68
|
+
what the DDS side already does.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
accepted: bool
|
|
72
|
+
#: Required on a decline, and M.1 is why: "a decline is a first-class,
|
|
73
|
+
#: observable outcome, not an absence." A refusal with no reason is an
|
|
74
|
+
#: absence with a status code.
|
|
75
|
+
reason: str = ""
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
if not self.accepted and not self.reason:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"a declined command must carry a reason: M.1 makes a decline "
|
|
81
|
+
"a first-class outcome, and a refusal nobody can read is the "
|
|
82
|
+
"absence it exists to replace")
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def accept(cls, reason: str = "") -> "Outcome":
|
|
86
|
+
return cls(True, reason)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def decline(cls, reason: str) -> "Outcome":
|
|
90
|
+
return cls(False, reason)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#: A handler receives the validated, lifted record --- the same JSON shape a
|
|
94
|
+
#: GET of that type would serve, after N.1 canonicalization and schema
|
|
95
|
+
#: validation --- and returns an `Outcome`. It never sees the request, the
|
|
96
|
+
#: token or the socket: an embedder owns verbs, not HTTP.
|
|
97
|
+
Handler = Callable[[dict[str, Any]], Outcome]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Registry:
|
|
101
|
+
"""The handlers an embedder has registered, keyed by type FQN.
|
|
102
|
+
|
|
103
|
+
Keyed by FQN rather than by topic so that a backend which accepts
|
|
104
|
+
`spatial.owm.ModelCommand` accepts it wherever a scene advertises that
|
|
105
|
+
type, which is how every other type-keyed table in this package works
|
|
106
|
+
(`DURABILITY`, `PUBLISHABLE`, the schema index).
|
|
107
|
+
|
|
108
|
+
Empty is the normal state. A gateway serving a recording has no verbs to
|
|
109
|
+
offer, and N.6 says a conformance suite MUST NOT require a command
|
|
110
|
+
surface to exist.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
self._handlers: dict[str, Handler] = {}
|
|
115
|
+
|
|
116
|
+
def register(self, fqn: str, handler: Handler) -> None:
|
|
117
|
+
"""Take commands of one type. Re-registering replaces, deliberately:
|
|
118
|
+
an embedder reconfiguring a lane should not have to tear down."""
|
|
119
|
+
self._handlers[fqn] = handler
|
|
120
|
+
|
|
121
|
+
def accepts(self, fqn: str) -> bool:
|
|
122
|
+
return fqn in self._handlers
|
|
123
|
+
|
|
124
|
+
def handler(self, fqn: str) -> Handler | None:
|
|
125
|
+
return self._handlers.get(fqn)
|
|
126
|
+
|
|
127
|
+
def types(self) -> list[str]:
|
|
128
|
+
return sorted(self._handlers)
|
|
129
|
+
|
|
130
|
+
def __bool__(self) -> bool:
|
|
131
|
+
return bool(self._handlers)
|
|
@@ -31,11 +31,11 @@ import json
|
|
|
31
31
|
import math
|
|
32
32
|
from dataclasses import dataclass, field
|
|
33
33
|
from pathlib import Path
|
|
34
|
-
from typing import Any, Iterable
|
|
34
|
+
from typing import Any, Callable, Iterable
|
|
35
35
|
|
|
36
36
|
from mcap.reader import make_reader
|
|
37
37
|
|
|
38
|
-
from . import idlmodel, pin, webjson, xcdr2
|
|
38
|
+
from . import commands, idlmodel, pin, webjson, xcdr2
|
|
39
39
|
|
|
40
40
|
PUBLIC = "public-cacheable"
|
|
41
41
|
OPERATOR = "operator-scoped"
|
|
@@ -553,7 +553,9 @@ def declared_service(st: State, scene: str) -> dict | None:
|
|
|
553
553
|
|
|
554
554
|
|
|
555
555
|
def service_manifest(st: State, scene: str, base: str,
|
|
556
|
-
declared: dict | None = None
|
|
556
|
+
declared: dict | None = None,
|
|
557
|
+
accepts_commands: Callable[[str], bool] | None = None
|
|
558
|
+
) -> dict:
|
|
557
559
|
"""The §8.2.3 service manifest for one scene.
|
|
558
560
|
|
|
559
561
|
`service.connection` is deliberately absent: it carries DDS connection
|
|
@@ -587,6 +589,21 @@ def service_manifest(st: State, scene: str, base: str,
|
|
|
587
589
|
"access": res.access,
|
|
588
590
|
"url": base + path,
|
|
589
591
|
})
|
|
592
|
+
# N.6 asks a client to POST a command to the lane that accepts one,
|
|
593
|
+
# and N.7's topic entry has no member that says which lane that is:
|
|
594
|
+
# its table is six MUST-carry fields plus `qos_profile` and
|
|
595
|
+
# `liveliness`, none of which can carry "verbs welcome here". Without
|
|
596
|
+
# something, a client discovers the command surface by POSTing and
|
|
597
|
+
# reading the refusal, which is discovery by trial.
|
|
598
|
+
#
|
|
599
|
+
# So this member is added, under this repository's own name, and
|
|
600
|
+
# filed as a specification gap rather than treated as settled -- see
|
|
601
|
+
# docs/WIRE.md and the 1.9 candidates entry. N.7's list is a floor
|
|
602
|
+
# and not a ceiling, so an extra member is conformant; it is omitted
|
|
603
|
+
# entirely rather than written `false`, so a manifest from an endpoint
|
|
604
|
+
# with no verbs is byte-identical to one from before this existed.
|
|
605
|
+
if accepts_commands and accepts_commands(res.fqn):
|
|
606
|
+
topics_block[-1][commands.ADVERTISEMENT] = True
|
|
590
607
|
man: dict[str, Any] = {
|
|
591
608
|
"id": f"spatialdds://spatialdds-web.local/{scene}/service/gateway",
|
|
592
609
|
"profile": "spatial.manifest/1.8",
|
|
@@ -659,7 +676,8 @@ def service_manifest(st: State, scene: str, base: str,
|
|
|
659
676
|
|
|
660
677
|
def search(st: State, base: str, geohash: str | None = None,
|
|
661
678
|
kinds: list[str] | None = None,
|
|
662
|
-
coverage: list[dict] | None = None
|
|
679
|
+
coverage: list[dict] | None = None,
|
|
680
|
+
accepts_commands: Callable[[str], bool] | None = None) -> dict:
|
|
663
681
|
"""The §3.3.0 search response: `{"results": [...], "next_page_token": ""}`."""
|
|
664
682
|
wanted: list[tuple[float, float, float, float]] = []
|
|
665
683
|
if geohash:
|
|
@@ -679,10 +697,12 @@ def search(st: State, base: str, geohash: str | None = None,
|
|
|
679
697
|
for scene in st.scenes:
|
|
680
698
|
declared = declared_services(st, scene)
|
|
681
699
|
if declared:
|
|
682
|
-
manifests.extend(service_manifest(st, scene, base, d
|
|
700
|
+
manifests.extend(service_manifest(st, scene, base, d,
|
|
701
|
+
accepts_commands)
|
|
683
702
|
for d in declared)
|
|
684
703
|
else:
|
|
685
|
-
manifests.append(service_manifest(st, scene, base
|
|
704
|
+
manifests.append(service_manifest(st, scene, base,
|
|
705
|
+
accepts_commands=accepts_commands))
|
|
686
706
|
for man in manifests:
|
|
687
707
|
if kinds and man["service"]["kind"] not in kinds:
|
|
688
708
|
continue
|
|
@@ -36,7 +36,7 @@ from typing import Any
|
|
|
36
36
|
|
|
37
37
|
from aiohttp import WSMsgType, web
|
|
38
38
|
|
|
39
|
-
from . import gateway, publish, webjson
|
|
39
|
+
from . import commands, gateway, publish, webjson
|
|
40
40
|
|
|
41
41
|
WELL_KNOWN = "/.well-known/spatialdds"
|
|
42
42
|
|
|
@@ -211,7 +211,8 @@ async def search(request: web.Request) -> web.Response:
|
|
|
211
211
|
kinds = [k] if k else []
|
|
212
212
|
try:
|
|
213
213
|
doc = gateway.search(st, base, geohash=geohash, kinds=kinds,
|
|
214
|
-
coverage=coverage
|
|
214
|
+
coverage=coverage,
|
|
215
|
+
accepts_commands=request.app["commands"].accepts)
|
|
215
216
|
except ValueError as e:
|
|
216
217
|
raise web.HTTPBadRequest(
|
|
217
218
|
body=_body({"type": PROBLEM_NS + "bad-query",
|
|
@@ -481,6 +482,98 @@ async def _broadcast(request: web.Request, topic: str, event: dict) -> None:
|
|
|
481
482
|
q.put_nowait(event)
|
|
482
483
|
|
|
483
484
|
|
|
485
|
+
async def post_command(request: web.Request) -> web.Response:
|
|
486
|
+
"""N.6: a command is a POST to the resource it acts on.
|
|
487
|
+
|
|
488
|
+
The resource is the lane, not the instance, and the addressee rides in the
|
|
489
|
+
body. That is M.1's shape -- "a shared keyed command topic, keyed by the
|
|
490
|
+
addressee" -- and it is also the URL the manifest advertises, which is the
|
|
491
|
+
URL a client holding a manifest already has. Posting to an instance path
|
|
492
|
+
would make a client build a URL by string surgery on a key, which is the
|
|
493
|
+
thing N.7's `url` member exists to abolish.
|
|
494
|
+
|
|
495
|
+
Four outcomes, and the status carries which:
|
|
496
|
+
|
|
497
|
+
* **202** the backend accepted it. Acceptance, not completion: N.6
|
|
498
|
+
requires no completion tracking, so this gateway invents none.
|
|
499
|
+
* **409** the backend declined, with M.1's reason in the problem document.
|
|
500
|
+
Not a 200: a caller that checks only the status must not read a refusal
|
|
501
|
+
as an acceptance, which would turn M.1's first-class outcome back into
|
|
502
|
+
the absence it exists to replace.
|
|
503
|
+
* **405** no handler for this type, or this lane is not a command lane.
|
|
504
|
+
N.6 names 405 or 404 for an endpoint with nothing to accept a verb for,
|
|
505
|
+
and the body is still an N.8 problem document so the absence is stated
|
|
506
|
+
rather than left to a framework's default page.
|
|
507
|
+
* **400/401** the body is not valid for the type, or no bearer credential.
|
|
508
|
+
Commands are writes, so N.4's operator-scoped path applies.
|
|
509
|
+
"""
|
|
510
|
+
st: gateway.State = request.app["state"]
|
|
511
|
+
reg: commands.Registry = request.app["commands"]
|
|
512
|
+
m = request.match_info
|
|
513
|
+
path = f"/{m['scene']}/{m['stream']}/{m['type']}/{m['version']}"
|
|
514
|
+
res = st.resources.get(path)
|
|
515
|
+
|
|
516
|
+
# Refuse before authenticating when there is nothing here to command: a
|
|
517
|
+
# 401 would tell an anonymous caller that a lane exists, and the lane's
|
|
518
|
+
# existence is public information that this endpoint simply does not have.
|
|
519
|
+
if res is None:
|
|
520
|
+
return _rejected(publish.Rejected(
|
|
521
|
+
405, "no-command-lane", "No command lane here",
|
|
522
|
+
f"This endpoint serves no resource at {path}, so there is no verb "
|
|
523
|
+
f"to accept. N.6: an endpoint with no command lane refuses "
|
|
524
|
+
f"cleanly and never accepts a command it cannot act on."), request)
|
|
525
|
+
if not reg.accepts(res.fqn):
|
|
526
|
+
return _rejected(publish.Rejected(
|
|
527
|
+
405, "no-command-lane", "No command lane here",
|
|
528
|
+
f"This endpoint serves {res.fqn} read-only: no backend has "
|
|
529
|
+
f"registered a handler for it, so there is nothing that could act "
|
|
530
|
+
f"on a verb. N.6 makes that a clean refusal rather than a silent "
|
|
531
|
+
f"acceptance.",
|
|
532
|
+
accepts=reg.types()), request)
|
|
533
|
+
|
|
534
|
+
if not _authorized(request, request.app["token"]):
|
|
535
|
+
return _unauthorized(str(request.rel_url))
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
body = await request.json()
|
|
539
|
+
except Exception as e:
|
|
540
|
+
return _rejected(publish.Rejected(
|
|
541
|
+
400, "not-json", "Body is not JSON",
|
|
542
|
+
f"The body must be N.1 canonical JSON: {e}"), request)
|
|
543
|
+
try:
|
|
544
|
+
publish.validate_schema(res.fqn, body)
|
|
545
|
+
data, _raw = publish.canonicalize(st, res.fqn, body)
|
|
546
|
+
except publish.Rejected as r:
|
|
547
|
+
return _rejected(r, request)
|
|
548
|
+
|
|
549
|
+
handler = reg.handler(res.fqn)
|
|
550
|
+
assert handler is not None # reg.accepts() was true above
|
|
551
|
+
try:
|
|
552
|
+
outcome = handler(data)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
# A backend that raises is a backend that did not decide. Reporting
|
|
555
|
+
# 500 is honest; reporting a decline would put words in its mouth.
|
|
556
|
+
return _problem(500, "handler-failed", "The command handler failed",
|
|
557
|
+
f"The backend registered for {res.fqn} raised "
|
|
558
|
+
f"{type(e).__name__}: {e}",
|
|
559
|
+
instance=str(request.rel_url))
|
|
560
|
+
|
|
561
|
+
if outcome.accepted:
|
|
562
|
+
doc: dict[str, Any] = {"outcome": "accepted", "type": res.fqn}
|
|
563
|
+
if outcome.reason:
|
|
564
|
+
doc["reason"] = outcome.reason
|
|
565
|
+
# Built directly rather than through `_json_response`, which is for
|
|
566
|
+
# resources: it answers 200 and honours If-None-Match, and a command
|
|
567
|
+
# outcome is neither cacheable nor re-fetchable. `no-store` because
|
|
568
|
+
# N.4 forbids any lifetime on an operator-scoped surface.
|
|
569
|
+
return web.Response(body=_body(doc), status=202,
|
|
570
|
+
content_type="application/json",
|
|
571
|
+
headers={"Cache-Control": "no-store"})
|
|
572
|
+
return _rejected(publish.Rejected(
|
|
573
|
+
409, "command-declined", "The command was declined",
|
|
574
|
+
outcome.reason, outcome="declined", type=res.fqn), request)
|
|
575
|
+
|
|
576
|
+
|
|
484
577
|
async def put_instance(request: web.Request) -> web.Response:
|
|
485
578
|
"""PUT on the same instance URL a GET already serves."""
|
|
486
579
|
if not request.app["publish"]:
|
|
@@ -705,7 +798,9 @@ async def root(request: web.Request) -> web.Response:
|
|
|
705
798
|
def make_app(state: gateway.State, base: str, token: str | None,
|
|
706
799
|
publish_enabled: bool = False,
|
|
707
800
|
journal: "publish.Journal | None" = None,
|
|
708
|
-
assets: Path | None = None
|
|
801
|
+
assets: Path | None = None,
|
|
802
|
+
command_registry: "commands.Registry | None" = None
|
|
803
|
+
) -> web.Application:
|
|
709
804
|
app = web.Application()
|
|
710
805
|
app["state"] = state
|
|
711
806
|
app["base"] = base.rstrip("/")
|
|
@@ -715,6 +810,10 @@ def make_app(state: gateway.State, base: str, token: str | None,
|
|
|
715
810
|
# default page, and so that the surface's absence is something the
|
|
716
811
|
# endpoint states rather than something a caller infers from a 405.
|
|
717
812
|
app["publish"] = publish_enabled
|
|
813
|
+
# The N.6 seam. Empty is the normal state: a gateway serving a recording
|
|
814
|
+
# has no verbs, and N.6 says a suite MUST NOT require a command surface
|
|
815
|
+
# to exist. An embedder registers handlers on this.
|
|
816
|
+
app["commands"] = command_registry or commands.Registry()
|
|
718
817
|
app["journal"] = journal
|
|
719
818
|
app["subs"] = {}
|
|
720
819
|
app["assets"] = assets
|
|
@@ -732,6 +831,12 @@ def make_app(state: gateway.State, base: str, token: str | None,
|
|
|
732
831
|
# `spatialdds/` removed, collection then instance.
|
|
733
832
|
web.get("/{scene}/{stream}/{type}/{version}", collection),
|
|
734
833
|
web.get("/{scene}/{stream}/{type}/{version}/{key}", instance),
|
|
834
|
+
# N.6: a command POST on the lane's own collection URL -- the one
|
|
835
|
+
# the manifest advertises, so a client follows a link rather than
|
|
836
|
+
# building a path. Registered unconditionally, like the write
|
|
837
|
+
# surface below and for the same reason: a refusal should be a stated
|
|
838
|
+
# N.8 problem document, not a framework's default 405 page.
|
|
839
|
+
web.post("/{scene}/{stream}/{type}/{version}", post_command),
|
|
735
840
|
# The write surface, on the same URLs the GETs serve.
|
|
736
841
|
web.put("/{scene}/{stream}/{type}/{version}/{key}", put_instance),
|
|
737
842
|
web.delete("/{scene}/{stream}/{type}/{version}/{key}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spatialdds
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command.
|
|
5
5
|
Author: Open AR Cloud
|
|
6
6
|
License: Apache-2.0
|
|
@@ -356,13 +356,37 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
|
356
356
|
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
357
357
|
site, nowhere else.
|
|
358
358
|
|
|
359
|
-
##
|
|
359
|
+
## New in 0.4.0: a command surface on the server half
|
|
360
|
+
|
|
361
|
+
`spatialdds[server]` now implements Appendix N.6. A command is a POST to the
|
|
362
|
+
lane that accepts one, and an embedder registers what a verb means:
|
|
363
|
+
|
|
364
|
+
| Part | What it is |
|
|
365
|
+
| --- | --- |
|
|
366
|
+
| the seam | `spatialdds.server.commands.Registry().register(fqn, handler)` |
|
|
367
|
+
| a handler gets | the validated, lifted record — the same JSON a GET would serve |
|
|
368
|
+
| a handler returns | `Outcome.accept()` or `Outcome.decline("why")` |
|
|
369
|
+
| the wire | `202` accepted, `409` declined with the reason, `405` where no handler is registered |
|
|
370
|
+
|
|
371
|
+
`python -m spatialdds.serve --demo` registers one so the surface is reachable
|
|
372
|
+
without writing a backend. An endpoint with no handlers is northbound-only and
|
|
373
|
+
every command POST is a clean `405`, which is what N.6 requires and what every
|
|
374
|
+
earlier version did.
|
|
375
|
+
|
|
376
|
+
**The client half has no command method.** This release adds the surface, not
|
|
377
|
+
a helper for calling it — `requests.post(topic_url, json=record)` with the
|
|
378
|
+
operator token is the whole of it, and `docs/WIRE.md` documents the response
|
|
379
|
+
shapes. Said plainly because "commands are supported" invites a reader to look
|
|
380
|
+
for `hit.command(...)`, and there isn't one.
|
|
381
|
+
|
|
382
|
+
## Not in 0.4.0
|
|
360
383
|
|
|
361
384
|
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
362
385
|
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
363
|
-
writes, and no DDS dependency of any kind. The
|
|
386
|
+
writes, no client-side command helper, and no DDS dependency of any kind. The
|
|
387
|
+
wire is HTTP and a WebSocket.
|
|
364
388
|
|
|
365
|
-
0.3.1
|
|
389
|
+
0.3.1 was this page: two code blocks on it had no gate behind them and both
|
|
366
390
|
were broken, and a long description is baked into the artifact, so the page on
|
|
367
391
|
PyPI only heals with a release. Every block here is executed by a gate now,
|
|
368
392
|
and the gate also refuses a relative link, an empty table header and a serve
|
|
@@ -136,6 +136,7 @@ spatialdds/demo/assets/fountain-marker.gltf
|
|
|
136
136
|
spatialdds/demo/manifests/content-fountain-label.json
|
|
137
137
|
spatialdds/demo/manifests/content-fountain-marker.json
|
|
138
138
|
spatialdds/server/__init__.py
|
|
139
|
+
spatialdds/server/commands.py
|
|
139
140
|
spatialdds/server/gateway.py
|
|
140
141
|
spatialdds/server/gw_http.py
|
|
141
142
|
spatialdds/server/idlmodel.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_AssetRef.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_FrameRef.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_MetaKV.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobRef.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FramedPose.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoPose.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileKey.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileMeta.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TilePatch.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Announce.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Depart.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Transform.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_ZoneState.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_Edge.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json
RENAMED
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_FusedState.json
RENAMED
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spatialdds-0.3.1 → spatialdds-0.4.0}/spatialdds/demo/manifests/content-fountain-marker.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|