spatialdds 0.3.0__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.0 → spatialdds-0.4.0}/PKG-INFO +81 -25
- {spatialdds-0.3.0 → spatialdds-0.4.0}/README.md +80 -24
- {spatialdds-0.3.0 → spatialdds-0.4.0}/pyproject.toml +1 -1
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/__init__.py +1 -1
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/serve.py +45 -4
- spatialdds-0.4.0/spatialdds/server/commands.py +131 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/gateway.py +26 -6
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/gw_http.py +108 -3
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds.egg-info/PKG-INFO +81 -25
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds.egg-info/SOURCES.txt +1 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/setup.cfg +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/idl/core.idl +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/idl/discovery.idl +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/idl/types.idl +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/builtin_Time.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_KV.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_common_Vec2.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Edge.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_Node.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileKey.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Announce.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Depart.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_KV.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_Transform.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_Entity.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/artifacts/serializer_tables.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/canonical.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/canonjson.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/client.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/assets/duck.gltf +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/assets/fountain-label.txt +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/assets/fountain-marker.gltf +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/fountain-anchors.mcap +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/manifests/content-fountain-label.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/demo/manifests/content-fountain-marker.json +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/errors.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/records.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/__init__.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/idlmodel.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/pin.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/publish.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/webjson.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds/server/xcdr2.py +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds.egg-info/dependency_links.txt +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds.egg-info/entry_points.txt +0 -0
- {spatialdds-0.3.0 → spatialdds-0.4.0}/spatialdds.egg-info/requires.txt +0 -0
- {spatialdds-0.3.0 → 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
|
|
@@ -43,6 +43,10 @@ not have to assemble member by member.
|
|
|
43
43
|
specification versions move independently; `spatialdds.SPEC_COMMIT` is the one
|
|
44
44
|
that says what the wire looks like.
|
|
45
45
|
|
|
46
|
+
The source, the reference gateway, the recordings and every gate behind the
|
|
47
|
+
claims on this page are at [https://github.com/OpenArCloud/spatialdds-web](https://github.com/OpenArCloud/spatialdds-web). Every code block below is executed
|
|
48
|
+
by one of those gates on every run, against an endpoint started for it.
|
|
49
|
+
|
|
46
50
|
## Your own registry, in one command
|
|
47
51
|
|
|
48
52
|
```sh
|
|
@@ -72,17 +76,26 @@ Appendix N's schemas make every member of a record required, so an `Announce`
|
|
|
72
76
|
is eighteen members, a `ContentAnnounce` sixteen, a `GeoAnchor` nine. The
|
|
73
77
|
builders take the arguments you actually have:
|
|
74
78
|
|
|
79
|
+
<!-- bar: verbs -->
|
|
75
80
|
```python
|
|
76
81
|
import spatialdds as sd
|
|
82
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret", scene="mytown")
|
|
83
|
+
a = ep.publish_service(service_id="mytown-vps", kind="VPS",
|
|
84
|
+
coverage=sd.circle(30.2849, -97.7393, 500))
|
|
85
|
+
b = ep.publish_content(
|
|
86
|
+
content_id="duck", title="A duck", position=(30.2849, -97.7393),
|
|
87
|
+
asset=sd.asset_ref("https://cdn.example/duck.gltf",
|
|
88
|
+
media_type="model/gltf+json"))
|
|
89
|
+
c = ep.publish_anchor("anchor/duck-perch", lat=30.2849, lon=-97.7393,
|
|
90
|
+
method="VisualFix", sigma_enu_m=(0.25, 0.25, 0.5),
|
|
91
|
+
content="duck")
|
|
92
|
+
print("service", a.created, "| content", b.created, "| anchor", c.created)
|
|
93
|
+
print("manifest", b.manifest.doc["id"])
|
|
94
|
+
```
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
ep.publish_content(content_id="duck", title="Duck", position=(30.2849, -97.7393),
|
|
82
|
-
asset="https://cdn.example/duck.gltf", sha256=digest)
|
|
83
|
-
ep.publish_anchor("anchor/duck-perch", lat=30.2849, lon=-97.7393,
|
|
84
|
-
method="VisualFix", sigma_enu_m=(0.25, 0.25, 0.5),
|
|
85
|
-
content="duck")
|
|
96
|
+
```console
|
|
97
|
+
service True | content True | anchor True
|
|
98
|
+
manifest spatialdds://demo.spatialdds.org/mytown/content/duck
|
|
86
99
|
```
|
|
87
100
|
|
|
88
101
|
`publish_service` and `publish_content` are two PUTs each -- the manifest
|
|
@@ -91,9 +104,12 @@ with the manifest's own result on `.manifest`.
|
|
|
91
104
|
|
|
92
105
|
A full service contract is the same one call, with the claims stated:
|
|
93
106
|
|
|
107
|
+
<!-- bar: contract -->
|
|
94
108
|
```python
|
|
109
|
+
import spatialdds as sd
|
|
110
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret", scene="mytown")
|
|
95
111
|
area = sd.circle(30.2849, -97.7393, 500)
|
|
96
|
-
ep.publish_service(
|
|
112
|
+
w = ep.publish_service(
|
|
97
113
|
service_id="mytown-vps", kind="VPS", coverage=area,
|
|
98
114
|
caps=sd.capabilities(
|
|
99
115
|
supported=[sd.profile("spatial.core", minor=8)],
|
|
@@ -101,19 +117,28 @@ ep.publish_service(
|
|
|
101
117
|
manifest=sd.service_manifest(
|
|
102
118
|
"mytown-vps", "VPS", authority=ep.authority, scene="mytown",
|
|
103
119
|
profiles=["Core", "AR+Geo"],
|
|
104
|
-
request={
|
|
105
|
-
response={
|
|
106
|
-
limits={
|
|
107
|
-
auth={
|
|
120
|
+
request={"features_topic": "feat.keyframe"},
|
|
121
|
+
response={"minimal": "geo.fix"},
|
|
122
|
+
limits={"max_fps": 10},
|
|
123
|
+
auth={"scheme": "bearer",
|
|
124
|
+
"issuer": "https://registry.mytown.example"},
|
|
108
125
|
coverage=area, cells=sd.cells(area)))
|
|
126
|
+
print("profiles", w.manifest.doc["profiles"])
|
|
127
|
+
print("limits ", w.manifest.doc["limits"])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
profiles ['Core', 'AR+Geo']
|
|
132
|
+
limits {'max_fps': 10}
|
|
109
133
|
```
|
|
110
134
|
|
|
111
135
|
Every one of those is something a service operator knows and a library cannot
|
|
112
|
-
guess. [
|
|
113
|
-
whole served document;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
guess. [The mytown walkthrough](https://github.com/OpenArCloud/spatialdds-web/blob/main/docs/MYTOWN.md) publishes that
|
|
137
|
+
and prints the whole served document;
|
|
138
|
+
[`gates/builders.py`](https://github.com/OpenArCloud/spatialdds-web/blob/main/gates/builders.py) rebuilds that
|
|
139
|
+
repository's hand-authored `austin-vps` and duck records through these calls
|
|
140
|
+
and compares them member by member, which is what makes "every member is
|
|
141
|
+
reachable from an argument" a check rather than a claim.
|
|
117
142
|
|
|
118
143
|
**Two forms for an asset, because there are two situations.**
|
|
119
144
|
`sd.asset(path_or_bytes, media_type=...)` reads the data and computes the hash
|
|
@@ -122,7 +147,8 @@ and the byte count itself: a hash nobody typed is a hash a reader can trust.
|
|
|
122
147
|
else hosts, and the hash comes from whoever has the bytes. Pass no hash and
|
|
123
148
|
the reference carries no integrity claim, which costs a reader one thing:
|
|
124
149
|
`fetch_asset()` raises rather than reporting a check it could not perform
|
|
125
|
-
(finding 31).
|
|
150
|
+
([finding 31](https://github.com/OpenArCloud/spatialdds-web/blob/main/WEB-BINDING-FINDINGS.md)).
|
|
151
|
+
`publish_content` takes either, or a path, or bytes, or a URL.
|
|
126
152
|
|
|
127
153
|
The builders are also usable on their own: `sd.service_announce`,
|
|
128
154
|
`sd.service_manifest`, `sd.content_announce`, `sd.content_manifest`,
|
|
@@ -258,7 +284,7 @@ call goes away and the bar is one line.
|
|
|
258
284
|
|
|
259
285
|
## The surface
|
|
260
286
|
|
|
261
|
-
| | |
|
|
287
|
+
| call | what it does |
|
|
262
288
|
| --- | --- |
|
|
263
289
|
| `sd.connect(base, token=None, verify_integrity=True, timeout=15.0, lifted=True, scene="default", authority=None)` | an `Endpoint`; nothing is fetched until you ask |
|
|
264
290
|
| `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
|
|
@@ -309,7 +335,7 @@ One family, built from the endpoint's own N.8 problem documents — `status`,
|
|
|
309
335
|
`type`, `detail` carried, not re-invented. Everything below derives from
|
|
310
336
|
`SpatialDDSError`, so one `except` catches the lot.
|
|
311
337
|
|
|
312
|
-
| | |
|
|
338
|
+
| exception | raised when |
|
|
313
339
|
| --- | --- |
|
|
314
340
|
| `Unreachable` | the endpoint did not answer; names the URL and the serve line |
|
|
315
341
|
| `Unauthorized` | 401; says to pass `token=` and where the token is printed |
|
|
@@ -330,13 +356,43 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
|
330
356
|
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
331
357
|
site, nowhere else.
|
|
332
358
|
|
|
333
|
-
##
|
|
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
|
|
334
383
|
|
|
335
384
|
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
336
385
|
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
337
|
-
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.
|
|
388
|
+
|
|
389
|
+
0.3.1 was this page: two code blocks on it had no gate behind them and both
|
|
390
|
+
were broken, and a long description is baked into the artifact, so the page on
|
|
391
|
+
PyPI only heals with a release. Every block here is executed by a gate now,
|
|
392
|
+
and the gate also refuses a relative link, an empty table header and a serve
|
|
393
|
+
line naming a flag that does not exist.
|
|
338
394
|
|
|
339
|
-
`sd.asset` changed meaning in 0.3.0, which is why
|
|
395
|
+
`sd.asset` changed meaning in 0.3.0, which is why that version moved: it now
|
|
340
396
|
reads bytes and computes a hash, where in 0.2.0 it took a URL and a hash you
|
|
341
397
|
supplied. That second form is `sd.asset_ref`. A published release has to stay
|
|
342
398
|
reproducible from a public commit, so the API could not change under the same
|
|
@@ -19,6 +19,10 @@ not have to assemble member by member.
|
|
|
19
19
|
specification versions move independently; `spatialdds.SPEC_COMMIT` is the one
|
|
20
20
|
that says what the wire looks like.
|
|
21
21
|
|
|
22
|
+
The source, the reference gateway, the recordings and every gate behind the
|
|
23
|
+
claims on this page are at [https://github.com/OpenArCloud/spatialdds-web](https://github.com/OpenArCloud/spatialdds-web). Every code block below is executed
|
|
24
|
+
by one of those gates on every run, against an endpoint started for it.
|
|
25
|
+
|
|
22
26
|
## Your own registry, in one command
|
|
23
27
|
|
|
24
28
|
```sh
|
|
@@ -48,17 +52,26 @@ Appendix N's schemas make every member of a record required, so an `Announce`
|
|
|
48
52
|
is eighteen members, a `ContentAnnounce` sixteen, a `GeoAnchor` nine. The
|
|
49
53
|
builders take the arguments you actually have:
|
|
50
54
|
|
|
55
|
+
<!-- bar: verbs -->
|
|
51
56
|
```python
|
|
52
57
|
import spatialdds as sd
|
|
58
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret", scene="mytown")
|
|
59
|
+
a = ep.publish_service(service_id="mytown-vps", kind="VPS",
|
|
60
|
+
coverage=sd.circle(30.2849, -97.7393, 500))
|
|
61
|
+
b = ep.publish_content(
|
|
62
|
+
content_id="duck", title="A duck", position=(30.2849, -97.7393),
|
|
63
|
+
asset=sd.asset_ref("https://cdn.example/duck.gltf",
|
|
64
|
+
media_type="model/gltf+json"))
|
|
65
|
+
c = ep.publish_anchor("anchor/duck-perch", lat=30.2849, lon=-97.7393,
|
|
66
|
+
method="VisualFix", sigma_enu_m=(0.25, 0.25, 0.5),
|
|
67
|
+
content="duck")
|
|
68
|
+
print("service", a.created, "| content", b.created, "| anchor", c.created)
|
|
69
|
+
print("manifest", b.manifest.doc["id"])
|
|
70
|
+
```
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
ep.publish_content(content_id="duck", title="Duck", position=(30.2849, -97.7393),
|
|
58
|
-
asset="https://cdn.example/duck.gltf", sha256=digest)
|
|
59
|
-
ep.publish_anchor("anchor/duck-perch", lat=30.2849, lon=-97.7393,
|
|
60
|
-
method="VisualFix", sigma_enu_m=(0.25, 0.25, 0.5),
|
|
61
|
-
content="duck")
|
|
72
|
+
```console
|
|
73
|
+
service True | content True | anchor True
|
|
74
|
+
manifest spatialdds://demo.spatialdds.org/mytown/content/duck
|
|
62
75
|
```
|
|
63
76
|
|
|
64
77
|
`publish_service` and `publish_content` are two PUTs each -- the manifest
|
|
@@ -67,9 +80,12 @@ with the manifest's own result on `.manifest`.
|
|
|
67
80
|
|
|
68
81
|
A full service contract is the same one call, with the claims stated:
|
|
69
82
|
|
|
83
|
+
<!-- bar: contract -->
|
|
70
84
|
```python
|
|
85
|
+
import spatialdds as sd
|
|
86
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret", scene="mytown")
|
|
71
87
|
area = sd.circle(30.2849, -97.7393, 500)
|
|
72
|
-
ep.publish_service(
|
|
88
|
+
w = ep.publish_service(
|
|
73
89
|
service_id="mytown-vps", kind="VPS", coverage=area,
|
|
74
90
|
caps=sd.capabilities(
|
|
75
91
|
supported=[sd.profile("spatial.core", minor=8)],
|
|
@@ -77,19 +93,28 @@ ep.publish_service(
|
|
|
77
93
|
manifest=sd.service_manifest(
|
|
78
94
|
"mytown-vps", "VPS", authority=ep.authority, scene="mytown",
|
|
79
95
|
profiles=["Core", "AR+Geo"],
|
|
80
|
-
request={
|
|
81
|
-
response={
|
|
82
|
-
limits={
|
|
83
|
-
auth={
|
|
96
|
+
request={"features_topic": "feat.keyframe"},
|
|
97
|
+
response={"minimal": "geo.fix"},
|
|
98
|
+
limits={"max_fps": 10},
|
|
99
|
+
auth={"scheme": "bearer",
|
|
100
|
+
"issuer": "https://registry.mytown.example"},
|
|
84
101
|
coverage=area, cells=sd.cells(area)))
|
|
102
|
+
print("profiles", w.manifest.doc["profiles"])
|
|
103
|
+
print("limits ", w.manifest.doc["limits"])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
profiles ['Core', 'AR+Geo']
|
|
108
|
+
limits {'max_fps': 10}
|
|
85
109
|
```
|
|
86
110
|
|
|
87
111
|
Every one of those is something a service operator knows and a library cannot
|
|
88
|
-
guess. [
|
|
89
|
-
whole served document;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
guess. [The mytown walkthrough](https://github.com/OpenArCloud/spatialdds-web/blob/main/docs/MYTOWN.md) publishes that
|
|
113
|
+
and prints the whole served document;
|
|
114
|
+
[`gates/builders.py`](https://github.com/OpenArCloud/spatialdds-web/blob/main/gates/builders.py) rebuilds that
|
|
115
|
+
repository's hand-authored `austin-vps` and duck records through these calls
|
|
116
|
+
and compares them member by member, which is what makes "every member is
|
|
117
|
+
reachable from an argument" a check rather than a claim.
|
|
93
118
|
|
|
94
119
|
**Two forms for an asset, because there are two situations.**
|
|
95
120
|
`sd.asset(path_or_bytes, media_type=...)` reads the data and computes the hash
|
|
@@ -98,7 +123,8 @@ and the byte count itself: a hash nobody typed is a hash a reader can trust.
|
|
|
98
123
|
else hosts, and the hash comes from whoever has the bytes. Pass no hash and
|
|
99
124
|
the reference carries no integrity claim, which costs a reader one thing:
|
|
100
125
|
`fetch_asset()` raises rather than reporting a check it could not perform
|
|
101
|
-
(finding 31).
|
|
126
|
+
([finding 31](https://github.com/OpenArCloud/spatialdds-web/blob/main/WEB-BINDING-FINDINGS.md)).
|
|
127
|
+
`publish_content` takes either, or a path, or bytes, or a URL.
|
|
102
128
|
|
|
103
129
|
The builders are also usable on their own: `sd.service_announce`,
|
|
104
130
|
`sd.service_manifest`, `sd.content_announce`, `sd.content_manifest`,
|
|
@@ -234,7 +260,7 @@ call goes away and the bar is one line.
|
|
|
234
260
|
|
|
235
261
|
## The surface
|
|
236
262
|
|
|
237
|
-
| | |
|
|
263
|
+
| call | what it does |
|
|
238
264
|
| --- | --- |
|
|
239
265
|
| `sd.connect(base, token=None, verify_integrity=True, timeout=15.0, lifted=True, scene="default", authority=None)` | an `Endpoint`; nothing is fetched until you ask |
|
|
240
266
|
| `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
|
|
@@ -285,7 +311,7 @@ One family, built from the endpoint's own N.8 problem documents — `status`,
|
|
|
285
311
|
`type`, `detail` carried, not re-invented. Everything below derives from
|
|
286
312
|
`SpatialDDSError`, so one `except` catches the lot.
|
|
287
313
|
|
|
288
|
-
| | |
|
|
314
|
+
| exception | raised when |
|
|
289
315
|
| --- | --- |
|
|
290
316
|
| `Unreachable` | the endpoint did not answer; names the URL and the serve line |
|
|
291
317
|
| `Unauthorized` | 401; says to pass `token=` and where the token is printed |
|
|
@@ -306,13 +332,43 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
|
306
332
|
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
307
333
|
site, nowhere else.
|
|
308
334
|
|
|
309
|
-
##
|
|
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
|
|
310
359
|
|
|
311
360
|
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
312
361
|
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
313
|
-
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.
|
|
364
|
+
|
|
365
|
+
0.3.1 was this page: two code blocks on it had no gate behind them and both
|
|
366
|
+
were broken, and a long description is baked into the artifact, so the page on
|
|
367
|
+
PyPI only heals with a release. Every block here is executed by a gate now,
|
|
368
|
+
and the gate also refuses a relative link, an empty table header and a serve
|
|
369
|
+
line naming a flag that does not exist.
|
|
314
370
|
|
|
315
|
-
`sd.asset` changed meaning in 0.3.0, which is why
|
|
371
|
+
`sd.asset` changed meaning in 0.3.0, which is why that version moved: it now
|
|
316
372
|
reads bytes and computes a hash, where in 0.2.0 it took a URL and a hash you
|
|
317
373
|
supplied. That second form is `sd.asset_ref`. A published release has to stay
|
|
318
374
|
reproducible from a public commit, so the API could not change under the same
|
|
@@ -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
|