spatialdds 0.1.0__tar.gz → 0.3.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/PKG-INFO +349 -0
- spatialdds-0.3.0/README.md +325 -0
- spatialdds-0.3.0/pyproject.toml +68 -0
- spatialdds-0.3.0/spatialdds/__init__.py +90 -0
- spatialdds-0.3.0/spatialdds/artifacts/idl/core.idl +340 -0
- spatialdds-0.3.0/spatialdds/artifacts/idl/discovery.idl +238 -0
- spatialdds-0.3.0/spatialdds/artifacts/idl/types.idl +156 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/builtin_Time.json +22 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +235 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +197 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +314 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +20 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +335 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +306 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +19 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +298 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +355 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +73 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +56 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_common_KV.json +19 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +45 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_common_Vec2.json +19 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +29 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +38 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +23 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +19 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_Edge.json +113 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +283 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +231 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +223 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +254 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +157 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +113 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_Node.json +245 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +214 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +95 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +29 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +54 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +66 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_TileKey.json +35 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +133 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +122 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_Announce.json +488 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +67 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +343 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +181 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +35 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +307 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +267 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +264 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_Depart.json +40 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_KV.json +19 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +33 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +240 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +41 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_Transform.json +153 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +42 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +235 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +349 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +425 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +132 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +173 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +248 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +97 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +471 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +65 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_owm_Entity.json +250 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +124 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +74 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +77 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +113 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +493 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +533 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +215 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +328 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +32 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +23 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +85 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +111 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +36 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +25 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +91 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +136 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +200 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +112 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +34 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +141 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +237 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +235 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +147 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +308 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +202 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +222 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +357 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +149 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +327 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +214 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +285 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +236 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +212 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +103 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +23 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +46 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +102 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +229 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +302 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +143 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +31 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +35 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +108 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +84 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +84 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +23 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +80 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +231 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +113 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +179 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +115 -0
- spatialdds-0.3.0/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +133 -0
- {spatialdds-0.1.0 → spatialdds-0.3.0}/spatialdds/canonical.py +15 -11
- spatialdds-0.3.0/spatialdds/canonjson.py +74 -0
- spatialdds-0.1.0/spatialdds/__init__.py → spatialdds-0.3.0/spatialdds/client.py +230 -34
- spatialdds-0.3.0/spatialdds/demo/assets/duck.gltf +66 -0
- spatialdds-0.3.0/spatialdds/demo/assets/fountain-label.txt +9 -0
- spatialdds-0.3.0/spatialdds/demo/assets/fountain-marker.gltf +64 -0
- spatialdds-0.3.0/spatialdds/demo/fountain-anchors.mcap +0 -0
- spatialdds-0.3.0/spatialdds/demo/manifests/content-fountain-label.json +59 -0
- spatialdds-0.3.0/spatialdds/demo/manifests/content-fountain-marker.json +59 -0
- spatialdds-0.3.0/spatialdds/records.py +801 -0
- spatialdds-0.3.0/spatialdds/serve.py +311 -0
- spatialdds-0.3.0/spatialdds/server/__init__.py +21 -0
- spatialdds-0.3.0/spatialdds/server/gateway.py +695 -0
- spatialdds-0.3.0/spatialdds/server/gw_http.py +741 -0
- spatialdds-0.3.0/spatialdds/server/idlmodel.py +439 -0
- spatialdds-0.3.0/spatialdds/server/pin.py +146 -0
- spatialdds-0.3.0/spatialdds/server/publish.py +374 -0
- spatialdds-0.3.0/spatialdds/server/webjson.py +302 -0
- spatialdds-0.3.0/spatialdds/server/xcdr2.py +442 -0
- spatialdds-0.3.0/spatialdds.egg-info/PKG-INFO +349 -0
- spatialdds-0.3.0/spatialdds.egg-info/SOURCES.txt +145 -0
- spatialdds-0.3.0/spatialdds.egg-info/entry_points.txt +2 -0
- spatialdds-0.3.0/spatialdds.egg-info/requires.txt +7 -0
- spatialdds-0.1.0/PKG-INFO +0 -193
- spatialdds-0.1.0/README.md +0 -173
- spatialdds-0.1.0/pyproject.toml +0 -36
- spatialdds-0.1.0/spatialdds.egg-info/PKG-INFO +0 -193
- spatialdds-0.1.0/spatialdds.egg-info/SOURCES.txt +0 -11
- spatialdds-0.1.0/spatialdds.egg-info/requires.txt +0 -2
- {spatialdds-0.1.0 → spatialdds-0.3.0}/setup.cfg +0 -0
- {spatialdds-0.1.0/spatialdds → spatialdds-0.3.0/spatialdds/artifacts}/serializer_tables.json +0 -0
- {spatialdds-0.1.0 → spatialdds-0.3.0}/spatialdds/errors.py +0 -0
- {spatialdds-0.1.0 → spatialdds-0.3.0}/spatialdds.egg-info/dependency_links.txt +0 -0
- {spatialdds-0.1.0 → spatialdds-0.3.0}/spatialdds.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatialdds
|
|
3
|
+
Version: 0.3.0
|
|
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
|
+
Author: Open AR Cloud
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/OpenArCloud/spatialdds-web
|
|
8
|
+
Project-URL: Specification, https://github.com/OpenArCloud/SpatialDDS-spec
|
|
9
|
+
Keywords: spatialdds,spatial,geopose,anchors,registry,openarcloud
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: requests>=2.25
|
|
19
|
+
Requires-Dist: websockets>=12
|
|
20
|
+
Provides-Extra: server
|
|
21
|
+
Requires-Dist: aiohttp>=3.8; extra == "server"
|
|
22
|
+
Requires-Dist: mcap>=1.1; extra == "server"
|
|
23
|
+
Requires-Dist: jsonschema>=4.0; extra == "server"
|
|
24
|
+
|
|
25
|
+
# spatialdds
|
|
26
|
+
|
|
27
|
+
SpatialDDS over plain HTTPS and a WebSocket: a client for programs, and the
|
|
28
|
+
reference registry behind one command.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install spatialdds # the client: two dependencies
|
|
32
|
+
pip install "spatialdds[server]" # and the registry
|
|
33
|
+
pip install "./lib/spatialdds[server]" # from a checkout of this repo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`fetch` and a WebSocket really are the whole client -- the endpoint's own
|
|
37
|
+
README shows that in three lines of `curl`, and nothing here replaces it. This
|
|
38
|
+
package is the step after that, when a program wants native values instead of
|
|
39
|
+
canonical JSON, bytes it can trust, refusals it can act on, and records it did
|
|
40
|
+
not have to assemble member by member.
|
|
41
|
+
|
|
42
|
+
**Implements Appendix N as of SpatialDDS-spec `9c3e1f2`.** Package versions and
|
|
43
|
+
specification versions move independently; `spatialdds.SPEC_COMMIT` is the one
|
|
44
|
+
that says what the wire looks like.
|
|
45
|
+
|
|
46
|
+
## Your own registry, in one command
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
python -m spatialdds.serve --authority registry.mytown.example --publish
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That is a conformant SpatialDDS endpoint: empty, typed, and ready to be
|
|
53
|
+
published into. Its types come from the IDL pinned inside this package at
|
|
54
|
+
`SPEC_COMMIT`, so there is no base recording to find first -- the specification
|
|
55
|
+
is the type source, and `spatialdds.server.pin` is where it lives. State goes
|
|
56
|
+
to a journal (`./spatialdds-registry.mcap` by default, named in the banner)
|
|
57
|
+
which is an ordinary SpatialDDS recording: restart replays it, and any tool
|
|
58
|
+
that reads a recording can read your registry's state.
|
|
59
|
+
|
|
60
|
+
`python -m spatialdds.serve --demo` serves the packaged fountain set instead --
|
|
61
|
+
five surveyed anchors, two content manifests, the assets they name -- with
|
|
62
|
+
publishing on and a token printed. Same boot path, seed data on top.
|
|
63
|
+
|
|
64
|
+
The server half is an extra because being an endpoint is the part you opt
|
|
65
|
+
into: `aiohttp` serves it, `mcap` is the journal, `jsonschema` validates every
|
|
66
|
+
published body before it reaches the wire. Reading somebody else's registry
|
|
67
|
+
needs none of them.
|
|
68
|
+
|
|
69
|
+
## Publishing without writing out eighteen members
|
|
70
|
+
|
|
71
|
+
Appendix N's schemas make every member of a record required, so an `Announce`
|
|
72
|
+
is eighteen members, a `ContentAnnounce` sixteen, a `GeoAnchor` nine. The
|
|
73
|
+
builders take the arguments you actually have:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import spatialdds as sd
|
|
77
|
+
|
|
78
|
+
ep = sd.connect("http://127.0.0.1:8787", token="tok", scene="mytown")
|
|
79
|
+
ep.publish_service(service_id="mytown-vps", kind="VPS",
|
|
80
|
+
coverage=sd.circle(30.2849, -97.7393, 500))
|
|
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")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`publish_service` and `publish_content` are two PUTs each -- the manifest
|
|
89
|
+
first, then the record that points at it -- and return what `publish` returns,
|
|
90
|
+
with the manifest's own result on `.manifest`.
|
|
91
|
+
|
|
92
|
+
A full service contract is the same one call, with the claims stated:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
area = sd.circle(30.2849, -97.7393, 500)
|
|
96
|
+
ep.publish_service(
|
|
97
|
+
service_id="mytown-vps", kind="VPS", coverage=area,
|
|
98
|
+
caps=sd.capabilities(
|
|
99
|
+
supported=[sd.profile("spatial.core", minor=8)],
|
|
100
|
+
features=["web-binding/json"]),
|
|
101
|
+
manifest=sd.service_manifest(
|
|
102
|
+
"mytown-vps", "VPS", authority=ep.authority, scene="mytown",
|
|
103
|
+
profiles=["Core", "AR+Geo"],
|
|
104
|
+
request={{"features_topic": "feat.keyframe"}},
|
|
105
|
+
response={{"minimal": "geo.fix"}},
|
|
106
|
+
limits={{"max_fps": 10}},
|
|
107
|
+
auth={{"scheme": "bearer", "issuer": "https://registry.mytown.example"}},
|
|
108
|
+
coverage=area, cells=sd.cells(area)))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Every one of those is something a service operator knows and a library cannot
|
|
112
|
+
guess. [docs/MYTOWN.md](../../docs/MYTOWN.md) publishes that and prints the
|
|
113
|
+
whole served document; `gates/builders.py` rebuilds this repository's
|
|
114
|
+
hand-authored `austin-vps` and duck records through these calls and compares
|
|
115
|
+
them member by member, which is what makes "every member is reachable from an
|
|
116
|
+
argument" a check rather than a claim.
|
|
117
|
+
|
|
118
|
+
**Two forms for an asset, because there are two situations.**
|
|
119
|
+
`sd.asset(path_or_bytes, media_type=...)` reads the data and computes the hash
|
|
120
|
+
and the byte count itself: a hash nobody typed is a hash a reader can trust.
|
|
121
|
+
`sd.asset_ref(url, hash=..., media_type=...)` points at something somebody
|
|
122
|
+
else hosts, and the hash comes from whoever has the bytes. Pass no hash and
|
|
123
|
+
the reference carries no integrity claim, which costs a reader one thing:
|
|
124
|
+
`fetch_asset()` raises rather than reporting a check it could not perform
|
|
125
|
+
(finding 31). `publish_content` takes either, or a path, or bytes, or a URL.
|
|
126
|
+
|
|
127
|
+
The builders are also usable on their own: `sd.service_announce`,
|
|
128
|
+
`sd.service_manifest`, `sd.content_announce`, `sd.content_manifest`,
|
|
129
|
+
`sd.geo_anchor`, with `sd.circle`, `sd.bbox`, `sd.geopose`, `sd.cov_pos3`,
|
|
130
|
+
`sd.cov_none`, `sd.profile`, `sd.capabilities`, `sd.asset_ref`,
|
|
131
|
+
`sd.frame`,
|
|
132
|
+
`sd.earth_fixed`, `sd.stamp`, `sd.topic`, `sd.asset`, `sd.cells`,
|
|
133
|
+
`sd.service_uri` and `sd.content_uri`. Each returns a plain dict in canonical
|
|
134
|
+
form, publishable as it stands and dumpable as the authoring JSON this
|
|
135
|
+
repository's tools read. `spatialdds.records` is the module they live in.
|
|
136
|
+
|
|
137
|
+
**Conventions fill plumbing. They never fill claims.** A stamp of now, an
|
|
138
|
+
earth-fixed frame, a checksum computed from the record itself: plumbing, with
|
|
139
|
+
one sensible value each, and filling them saves you writing out what you had
|
|
140
|
+
no choice about. A profile list, a rate limit, a request topic, an auth
|
|
141
|
+
scheme, a title: claims, and no default for any of them is honest. State
|
|
142
|
+
nothing and you publish a minimal record, which is *correct*; state a
|
|
143
|
+
contract and you publish it in one call. There is nothing in between for a
|
|
144
|
+
library to invent.
|
|
145
|
+
|
|
146
|
+
**What they fill in, and what they refuse to.** The conventions above are
|
|
147
|
+
documented where they are applied: a stamp of now unless you pass `when=`, an
|
|
148
|
+
earth-fixed frame unless you pass one, and URIs assembled under a stated
|
|
149
|
+
authority -- never a guessed one. `ep.publish_*` gets the authority from the
|
|
150
|
+
endpoint itself (`ep.authority`, which asks the resolver metadata, then the
|
|
151
|
+
endpoint's own 404 document at `/`, then the address you connected to). What
|
|
152
|
+
they never fill is **identity**: `provider_id` and `org` are yours to state,
|
|
153
|
+
because a library that invented a provider id would be inventing provenance.
|
|
154
|
+
|
|
155
|
+
`GeoAnchor.checksum` is required by 1.8 and specified by nobody
|
|
156
|
+
(spatialdds-web finding 31). The builder fills it with the SHA-256 of the
|
|
157
|
+
record's own canonical JSON with `checksum` emptied -- a convention, chosen
|
|
158
|
+
because a reader can recompute it from what was served. If the specification
|
|
159
|
+
settles the semantics, this changes.
|
|
160
|
+
|
|
161
|
+
The full map of builder argument to IDL member, including every member filled
|
|
162
|
+
by convention, is printed by `python3 gates/builders.py --census` in the
|
|
163
|
+
repository.
|
|
164
|
+
|
|
165
|
+
## What it is for: the lift
|
|
166
|
+
|
|
167
|
+
An endpoint serves Appendix N.1 canonical JSON, and three rules separate that
|
|
168
|
+
from the values a program wants. All three are easy to get subtly wrong:
|
|
169
|
+
|
|
170
|
+
* `int64` and `uint64` are **unconditional decimal strings**. A client that
|
|
171
|
+
treats `stamp.sec` as a string gets surprising comparisons; one that calls
|
|
172
|
+
`int()` on every string corrupts identifiers.
|
|
173
|
+
* An octet run is **base64**.
|
|
174
|
+
* A union is tagged on `"type"`, and carries `_discriminator` once lifted.
|
|
175
|
+
|
|
176
|
+
These are not this library's rules -- they are the reference gateway's, and
|
|
177
|
+
the repository gates that the two agree by lifting every record in both
|
|
178
|
+
committed recordings with each implementation and comparing. Same rules, same
|
|
179
|
+
results, different source of the type information: the server reads the IDL,
|
|
180
|
+
and a client reading a remote endpoint has none, so this reads the generated
|
|
181
|
+
serializer tables that travel with the package. That gate earned its keep on
|
|
182
|
+
2026-10-15, when the disagreement it found turned out to be in the server.
|
|
183
|
+
|
|
184
|
+
A lifted union keeps `_discriminator` rather than `"type"`, because that is
|
|
185
|
+
what the lift produces and inventing a fourth shape would be a fourth rule.
|
|
186
|
+
Pass `lifted=False` to `connect()` to get the canonical JSON untouched.
|
|
187
|
+
|
|
188
|
+
## The three bars
|
|
189
|
+
|
|
190
|
+
Each one runs on its own, pasted into a fresh shell with nothing else run
|
|
191
|
+
first, which is why the serve line all three need is printed here rather than
|
|
192
|
+
assumed. `gates/library.py` executes each bar against its own fresh endpoint
|
|
193
|
+
and compares its output to what is printed below it.
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
python3 tools/run_gateway.py --mcap samples/fountain-anchors.mcap \
|
|
197
|
+
--manifests samples/manifests --assets samples/assets \
|
|
198
|
+
--publish --token not-a-secret --public-url https://demo.spatialdds.org
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Hello spatial, six lines.** Discover, subscribe, and read typed samples until
|
|
202
|
+
the latched set is complete.
|
|
203
|
+
|
|
204
|
+
<!-- bar: read -->
|
|
205
|
+
```python
|
|
206
|
+
import spatialdds as sd
|
|
207
|
+
ep = sd.connect("http://127.0.0.1:8787")
|
|
208
|
+
svc = next(h for h in ep.search(geohash="9v6krc") if h.serves("geo_anchor"))
|
|
209
|
+
for ev in svc.topic("geo_anchor").subscribe(timeout=10):
|
|
210
|
+
if isinstance(ev, sd.EndOfReplay): print("latched set complete:", ev.replayed); break
|
|
211
|
+
print(" ", ev.key, ev.data["method"], ev.data["geopose"]["stamp"]["sec"])
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```console
|
|
215
|
+
anchor/fountain-north-rim Surveyed 1791559800
|
|
216
|
+
anchor/fountain-south-rim VisualFix 1791559800
|
|
217
|
+
anchor/fountain-east-rim VisualFix 1791559800
|
|
218
|
+
anchor/fountain-west-rim VisualFix 1791559800
|
|
219
|
+
anchor/fountain-plaque GNSS 1791559800
|
|
220
|
+
latched set complete: 5
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`geopose.stamp.sec` arrives as an `int` there, not as `"1791559800"`. That is
|
|
224
|
+
the whole lift, in the one place a reader will notice it.
|
|
225
|
+
|
|
226
|
+
**Publish the duck, four lines.** The content manifest, and the announce that
|
|
227
|
+
places it.
|
|
228
|
+
|
|
229
|
+
<!-- bar: publish -->
|
|
230
|
+
```python
|
|
231
|
+
import json, spatialdds as sd
|
|
232
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret")
|
|
233
|
+
d = json.load(open("samples/duck.input.json"))
|
|
234
|
+
print("manifest", ep.publish_manifest(d["manifest"]).created, "| announce", ep.publish(topic=d["announce"]["path"], key=d["announce"]["key"], record=d["announce"]["record"]).created)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```console
|
|
238
|
+
manifest True | announce True
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Resolve and verify, two lines.** The URI resolved per the endpoint's own
|
|
242
|
+
§7.5.2 metadata, and the asset's bytes checked against the hash the manifest
|
|
243
|
+
declared.
|
|
244
|
+
|
|
245
|
+
<!-- bar: resolve -->
|
|
246
|
+
```python
|
|
247
|
+
import json, spatialdds as sd
|
|
248
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret"); ep.publish_manifest(json.load(open("samples/duck.input.json"))["manifest"]); print(len(ep.resolve("spatialdds://demo.spatialdds.org/south-mall/content/duck").fetch_asset()), "bytes verified")
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```console
|
|
252
|
+
1432 bytes verified
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The duck's manifest is published on the first line because these two lines
|
|
256
|
+
have to stand alone. Against an endpoint that already holds the manifest, that
|
|
257
|
+
call goes away and the bar is one line.
|
|
258
|
+
|
|
259
|
+
## The surface
|
|
260
|
+
|
|
261
|
+
| | |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `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
|
+
| `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
|
|
265
|
+
| `ep.near(lat, lon, radius_m)` | a helper over `search`, never a second protocol |
|
|
266
|
+
| `ep.resolve(uri)` | §7.5.2 resolution → `Resolved` |
|
|
267
|
+
| `ep.authority` | the DNS name the endpoint says it answers as |
|
|
268
|
+
| `ep.publish(topic=, key=, record=, if_match=None)` | PUT one instance → `Written` |
|
|
269
|
+
| `ep.publish_manifest(record)` | PUT a manifest, keyed by its own `id` |
|
|
270
|
+
| `ep.publish_service(service_id=, kind=, coverage=, manifest=None, ...)` | build and PUT an announce, and its manifest if given |
|
|
271
|
+
| `ep.publish_content(content_id=, position=, asset=, ...)` | build and PUT a content manifest and its announce |
|
|
272
|
+
| `ep.publish_anchor(anchor_id, lat=, lon=, method=, content=None, ...)` | build and PUT an anchor, optionally naming content |
|
|
273
|
+
| `ep.withdraw(topic=, key=)` | DELETE: leaves the latched set and disposes |
|
|
274
|
+
| `hit.service_id`, `.kind`, `.coverage`, `.topics` | the typed service manifest |
|
|
275
|
+
| `hit.serves(type_name)` | does this service offer a topic of that type? for picking one out of a search result |
|
|
276
|
+
| `hit.topic(type_name)` | a `Topic`, URL taken from the manifest, never built |
|
|
277
|
+
| `hit.reaches(lat, lon, radius_m)` | does the declared coverage reach a point |
|
|
278
|
+
| `topic.type`, `.url`, `.name`, `.durability`, `.access`, `.fqn` | what the manifest said |
|
|
279
|
+
| `topic.latched()` | every live instance, lifted |
|
|
280
|
+
| `topic.instance(key)` | one instance, key percent-encoded per N.3 |
|
|
281
|
+
| `topic.subscribe(timeout=None)` | a generator of `Sample` / `Dispose` / `EndOfReplay` / `Liveliness` |
|
|
282
|
+
| `resolved.doc`, `.uri`, `.etag`, `.assets` | the resolved manifest |
|
|
283
|
+
| `resolved.asset_url(i)`, `.fetch_asset(i)` | the URL it would fetch, and the verified bytes |
|
|
284
|
+
| `written.doc`, `.etag`, `.created`, `.manifest` | what a publish returned |
|
|
285
|
+
| `sd.service_manifest(service_id, kind, authority=, scene=, profiles=, request=, response=, limits=, auth=, coverage=, cells=)` | the contract behind an announce; every member a claim, so every member an argument |
|
|
286
|
+
| `sd.capabilities(supported=, preferred=, features=)`, `sd.profile(name, minor=)` | what a service supports, for `service_announce(caps=)` |
|
|
287
|
+
| `sd.asset(path_or_bytes, media_type=)` / `sd.asset_ref(url, hash=, media_type=)` | an asset hashed from its bytes, or a reference to one hosted elsewhere |
|
|
288
|
+
| `sd.records` | the builders, listed above |
|
|
289
|
+
| `sd.lift(fqn, doc)` | the lift on its own, for JSON you already have |
|
|
290
|
+
| `sd.SPEC_COMMIT`, `sd.APPENDIX_N`, `sd.__version__` | what this speaks |
|
|
291
|
+
|
|
292
|
+
`ep.near()` picks a geohash precision whose cell covers the radius, encodes the
|
|
293
|
+
centre and eight points around the query circle so the covering cells are found
|
|
294
|
+
without neighbour arithmetic, searches each distinct cell, and filters the hits
|
|
295
|
+
by their own declared coverage. Everything it sends is an ordinary §3.3.0
|
|
296
|
+
geohash query.
|
|
297
|
+
|
|
298
|
+
`topic.url` and `assets[].uri` are followed by different rules, deliberately. A
|
|
299
|
+
topic URL comes out of a service manifest this endpoint served, so its path is
|
|
300
|
+
this endpoint's and the library follows it by path -- which is what makes a
|
|
301
|
+
registry advertising a DNS name usable from a laptop. An asset URI is an
|
|
302
|
+
ordinary web URL that usually points somewhere else entirely, so it is fetched
|
|
303
|
+
exactly as written; the one exception is an asset on the endpoint's own
|
|
304
|
+
advertised authority, which is rebased onto the endpoint you are talking to.
|
|
305
|
+
|
|
306
|
+
## Errors
|
|
307
|
+
|
|
308
|
+
One family, built from the endpoint's own N.8 problem documents — `status`,
|
|
309
|
+
`type`, `detail` carried, not re-invented. Everything below derives from
|
|
310
|
+
`SpatialDDSError`, so one `except` catches the lot.
|
|
311
|
+
|
|
312
|
+
| | |
|
|
313
|
+
| --- | --- |
|
|
314
|
+
| `Unreachable` | the endpoint did not answer; names the URL and the serve line |
|
|
315
|
+
| `Unauthorized` | 401; says to pass `token=` and where the token is printed |
|
|
316
|
+
| `SurfaceOff` | 405 from a write verb; says to start with `--publish` |
|
|
317
|
+
| `Problem` | any other refusal, with `.status`, `.type`, `.slug`, `.detail` |
|
|
318
|
+
| `IntegrityError` | fetched bytes do not match the declared hash |
|
|
319
|
+
| `UnverifiableIntegrity` | the declared value is not a hash this library can check |
|
|
320
|
+
|
|
321
|
+
That last one is the unusual one and it is deliberate. SpatialDDS counts "a
|
|
322
|
+
checksum field in the body" as a §7.5.2 integrity signal, but nothing specifies
|
|
323
|
+
what such a field digests or in what form (spatialdds-web finding 31). Where a
|
|
324
|
+
declared value does not parse as a SHA-256 of the asset's bytes, this library
|
|
325
|
+
raises rather than reporting a check it did not perform.
|
|
326
|
+
|
|
327
|
+
## Integrity by default
|
|
328
|
+
|
|
329
|
+
Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
330
|
+
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
331
|
+
site, nowhere else.
|
|
332
|
+
|
|
333
|
+
## Not in 0.3.0
|
|
334
|
+
|
|
335
|
+
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
336
|
+
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
337
|
+
writes, and no DDS dependency of any kind. The wire is HTTP and a WebSocket.
|
|
338
|
+
|
|
339
|
+
`sd.asset` changed meaning in 0.3.0, which is why the version moved: it now
|
|
340
|
+
reads bytes and computes a hash, where in 0.2.0 it took a URL and a hash you
|
|
341
|
+
supplied. That second form is `sd.asset_ref`. A published release has to stay
|
|
342
|
+
reproducible from a public commit, so the API could not change under the same
|
|
343
|
+
number.
|
|
344
|
+
|
|
345
|
+
On the registry, four things are deferred rather than missing, and the same
|
|
346
|
+
four are on the 1.9 candidates list: **leases** (nothing expires; `ttl_sec` is
|
|
347
|
+
advice to caches), **per-provider scopes** (one token, one operator),
|
|
348
|
+
**server-set identity** (`provider_id` is whatever the publisher says), and
|
|
349
|
+
**upstream subscriptions** (a registry does not follow another registry).
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# spatialdds
|
|
2
|
+
|
|
3
|
+
SpatialDDS over plain HTTPS and a WebSocket: a client for programs, and the
|
|
4
|
+
reference registry behind one command.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pip install spatialdds # the client: two dependencies
|
|
8
|
+
pip install "spatialdds[server]" # and the registry
|
|
9
|
+
pip install "./lib/spatialdds[server]" # from a checkout of this repo
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`fetch` and a WebSocket really are the whole client -- the endpoint's own
|
|
13
|
+
README shows that in three lines of `curl`, and nothing here replaces it. This
|
|
14
|
+
package is the step after that, when a program wants native values instead of
|
|
15
|
+
canonical JSON, bytes it can trust, refusals it can act on, and records it did
|
|
16
|
+
not have to assemble member by member.
|
|
17
|
+
|
|
18
|
+
**Implements Appendix N as of SpatialDDS-spec `9c3e1f2`.** Package versions and
|
|
19
|
+
specification versions move independently; `spatialdds.SPEC_COMMIT` is the one
|
|
20
|
+
that says what the wire looks like.
|
|
21
|
+
|
|
22
|
+
## Your own registry, in one command
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
python -m spatialdds.serve --authority registry.mytown.example --publish
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is a conformant SpatialDDS endpoint: empty, typed, and ready to be
|
|
29
|
+
published into. Its types come from the IDL pinned inside this package at
|
|
30
|
+
`SPEC_COMMIT`, so there is no base recording to find first -- the specification
|
|
31
|
+
is the type source, and `spatialdds.server.pin` is where it lives. State goes
|
|
32
|
+
to a journal (`./spatialdds-registry.mcap` by default, named in the banner)
|
|
33
|
+
which is an ordinary SpatialDDS recording: restart replays it, and any tool
|
|
34
|
+
that reads a recording can read your registry's state.
|
|
35
|
+
|
|
36
|
+
`python -m spatialdds.serve --demo` serves the packaged fountain set instead --
|
|
37
|
+
five surveyed anchors, two content manifests, the assets they name -- with
|
|
38
|
+
publishing on and a token printed. Same boot path, seed data on top.
|
|
39
|
+
|
|
40
|
+
The server half is an extra because being an endpoint is the part you opt
|
|
41
|
+
into: `aiohttp` serves it, `mcap` is the journal, `jsonschema` validates every
|
|
42
|
+
published body before it reaches the wire. Reading somebody else's registry
|
|
43
|
+
needs none of them.
|
|
44
|
+
|
|
45
|
+
## Publishing without writing out eighteen members
|
|
46
|
+
|
|
47
|
+
Appendix N's schemas make every member of a record required, so an `Announce`
|
|
48
|
+
is eighteen members, a `ContentAnnounce` sixteen, a `GeoAnchor` nine. The
|
|
49
|
+
builders take the arguments you actually have:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import spatialdds as sd
|
|
53
|
+
|
|
54
|
+
ep = sd.connect("http://127.0.0.1:8787", token="tok", scene="mytown")
|
|
55
|
+
ep.publish_service(service_id="mytown-vps", kind="VPS",
|
|
56
|
+
coverage=sd.circle(30.2849, -97.7393, 500))
|
|
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")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`publish_service` and `publish_content` are two PUTs each -- the manifest
|
|
65
|
+
first, then the record that points at it -- and return what `publish` returns,
|
|
66
|
+
with the manifest's own result on `.manifest`.
|
|
67
|
+
|
|
68
|
+
A full service contract is the same one call, with the claims stated:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
area = sd.circle(30.2849, -97.7393, 500)
|
|
72
|
+
ep.publish_service(
|
|
73
|
+
service_id="mytown-vps", kind="VPS", coverage=area,
|
|
74
|
+
caps=sd.capabilities(
|
|
75
|
+
supported=[sd.profile("spatial.core", minor=8)],
|
|
76
|
+
features=["web-binding/json"]),
|
|
77
|
+
manifest=sd.service_manifest(
|
|
78
|
+
"mytown-vps", "VPS", authority=ep.authority, scene="mytown",
|
|
79
|
+
profiles=["Core", "AR+Geo"],
|
|
80
|
+
request={{"features_topic": "feat.keyframe"}},
|
|
81
|
+
response={{"minimal": "geo.fix"}},
|
|
82
|
+
limits={{"max_fps": 10}},
|
|
83
|
+
auth={{"scheme": "bearer", "issuer": "https://registry.mytown.example"}},
|
|
84
|
+
coverage=area, cells=sd.cells(area)))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every one of those is something a service operator knows and a library cannot
|
|
88
|
+
guess. [docs/MYTOWN.md](../../docs/MYTOWN.md) publishes that and prints the
|
|
89
|
+
whole served document; `gates/builders.py` rebuilds this repository's
|
|
90
|
+
hand-authored `austin-vps` and duck records through these calls and compares
|
|
91
|
+
them member by member, which is what makes "every member is reachable from an
|
|
92
|
+
argument" a check rather than a claim.
|
|
93
|
+
|
|
94
|
+
**Two forms for an asset, because there are two situations.**
|
|
95
|
+
`sd.asset(path_or_bytes, media_type=...)` reads the data and computes the hash
|
|
96
|
+
and the byte count itself: a hash nobody typed is a hash a reader can trust.
|
|
97
|
+
`sd.asset_ref(url, hash=..., media_type=...)` points at something somebody
|
|
98
|
+
else hosts, and the hash comes from whoever has the bytes. Pass no hash and
|
|
99
|
+
the reference carries no integrity claim, which costs a reader one thing:
|
|
100
|
+
`fetch_asset()` raises rather than reporting a check it could not perform
|
|
101
|
+
(finding 31). `publish_content` takes either, or a path, or bytes, or a URL.
|
|
102
|
+
|
|
103
|
+
The builders are also usable on their own: `sd.service_announce`,
|
|
104
|
+
`sd.service_manifest`, `sd.content_announce`, `sd.content_manifest`,
|
|
105
|
+
`sd.geo_anchor`, with `sd.circle`, `sd.bbox`, `sd.geopose`, `sd.cov_pos3`,
|
|
106
|
+
`sd.cov_none`, `sd.profile`, `sd.capabilities`, `sd.asset_ref`,
|
|
107
|
+
`sd.frame`,
|
|
108
|
+
`sd.earth_fixed`, `sd.stamp`, `sd.topic`, `sd.asset`, `sd.cells`,
|
|
109
|
+
`sd.service_uri` and `sd.content_uri`. Each returns a plain dict in canonical
|
|
110
|
+
form, publishable as it stands and dumpable as the authoring JSON this
|
|
111
|
+
repository's tools read. `spatialdds.records` is the module they live in.
|
|
112
|
+
|
|
113
|
+
**Conventions fill plumbing. They never fill claims.** A stamp of now, an
|
|
114
|
+
earth-fixed frame, a checksum computed from the record itself: plumbing, with
|
|
115
|
+
one sensible value each, and filling them saves you writing out what you had
|
|
116
|
+
no choice about. A profile list, a rate limit, a request topic, an auth
|
|
117
|
+
scheme, a title: claims, and no default for any of them is honest. State
|
|
118
|
+
nothing and you publish a minimal record, which is *correct*; state a
|
|
119
|
+
contract and you publish it in one call. There is nothing in between for a
|
|
120
|
+
library to invent.
|
|
121
|
+
|
|
122
|
+
**What they fill in, and what they refuse to.** The conventions above are
|
|
123
|
+
documented where they are applied: a stamp of now unless you pass `when=`, an
|
|
124
|
+
earth-fixed frame unless you pass one, and URIs assembled under a stated
|
|
125
|
+
authority -- never a guessed one. `ep.publish_*` gets the authority from the
|
|
126
|
+
endpoint itself (`ep.authority`, which asks the resolver metadata, then the
|
|
127
|
+
endpoint's own 404 document at `/`, then the address you connected to). What
|
|
128
|
+
they never fill is **identity**: `provider_id` and `org` are yours to state,
|
|
129
|
+
because a library that invented a provider id would be inventing provenance.
|
|
130
|
+
|
|
131
|
+
`GeoAnchor.checksum` is required by 1.8 and specified by nobody
|
|
132
|
+
(spatialdds-web finding 31). The builder fills it with the SHA-256 of the
|
|
133
|
+
record's own canonical JSON with `checksum` emptied -- a convention, chosen
|
|
134
|
+
because a reader can recompute it from what was served. If the specification
|
|
135
|
+
settles the semantics, this changes.
|
|
136
|
+
|
|
137
|
+
The full map of builder argument to IDL member, including every member filled
|
|
138
|
+
by convention, is printed by `python3 gates/builders.py --census` in the
|
|
139
|
+
repository.
|
|
140
|
+
|
|
141
|
+
## What it is for: the lift
|
|
142
|
+
|
|
143
|
+
An endpoint serves Appendix N.1 canonical JSON, and three rules separate that
|
|
144
|
+
from the values a program wants. All three are easy to get subtly wrong:
|
|
145
|
+
|
|
146
|
+
* `int64` and `uint64` are **unconditional decimal strings**. A client that
|
|
147
|
+
treats `stamp.sec` as a string gets surprising comparisons; one that calls
|
|
148
|
+
`int()` on every string corrupts identifiers.
|
|
149
|
+
* An octet run is **base64**.
|
|
150
|
+
* A union is tagged on `"type"`, and carries `_discriminator` once lifted.
|
|
151
|
+
|
|
152
|
+
These are not this library's rules -- they are the reference gateway's, and
|
|
153
|
+
the repository gates that the two agree by lifting every record in both
|
|
154
|
+
committed recordings with each implementation and comparing. Same rules, same
|
|
155
|
+
results, different source of the type information: the server reads the IDL,
|
|
156
|
+
and a client reading a remote endpoint has none, so this reads the generated
|
|
157
|
+
serializer tables that travel with the package. That gate earned its keep on
|
|
158
|
+
2026-10-15, when the disagreement it found turned out to be in the server.
|
|
159
|
+
|
|
160
|
+
A lifted union keeps `_discriminator` rather than `"type"`, because that is
|
|
161
|
+
what the lift produces and inventing a fourth shape would be a fourth rule.
|
|
162
|
+
Pass `lifted=False` to `connect()` to get the canonical JSON untouched.
|
|
163
|
+
|
|
164
|
+
## The three bars
|
|
165
|
+
|
|
166
|
+
Each one runs on its own, pasted into a fresh shell with nothing else run
|
|
167
|
+
first, which is why the serve line all three need is printed here rather than
|
|
168
|
+
assumed. `gates/library.py` executes each bar against its own fresh endpoint
|
|
169
|
+
and compares its output to what is printed below it.
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
python3 tools/run_gateway.py --mcap samples/fountain-anchors.mcap \
|
|
173
|
+
--manifests samples/manifests --assets samples/assets \
|
|
174
|
+
--publish --token not-a-secret --public-url https://demo.spatialdds.org
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Hello spatial, six lines.** Discover, subscribe, and read typed samples until
|
|
178
|
+
the latched set is complete.
|
|
179
|
+
|
|
180
|
+
<!-- bar: read -->
|
|
181
|
+
```python
|
|
182
|
+
import spatialdds as sd
|
|
183
|
+
ep = sd.connect("http://127.0.0.1:8787")
|
|
184
|
+
svc = next(h for h in ep.search(geohash="9v6krc") if h.serves("geo_anchor"))
|
|
185
|
+
for ev in svc.topic("geo_anchor").subscribe(timeout=10):
|
|
186
|
+
if isinstance(ev, sd.EndOfReplay): print("latched set complete:", ev.replayed); break
|
|
187
|
+
print(" ", ev.key, ev.data["method"], ev.data["geopose"]["stamp"]["sec"])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```console
|
|
191
|
+
anchor/fountain-north-rim Surveyed 1791559800
|
|
192
|
+
anchor/fountain-south-rim VisualFix 1791559800
|
|
193
|
+
anchor/fountain-east-rim VisualFix 1791559800
|
|
194
|
+
anchor/fountain-west-rim VisualFix 1791559800
|
|
195
|
+
anchor/fountain-plaque GNSS 1791559800
|
|
196
|
+
latched set complete: 5
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`geopose.stamp.sec` arrives as an `int` there, not as `"1791559800"`. That is
|
|
200
|
+
the whole lift, in the one place a reader will notice it.
|
|
201
|
+
|
|
202
|
+
**Publish the duck, four lines.** The content manifest, and the announce that
|
|
203
|
+
places it.
|
|
204
|
+
|
|
205
|
+
<!-- bar: publish -->
|
|
206
|
+
```python
|
|
207
|
+
import json, spatialdds as sd
|
|
208
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret")
|
|
209
|
+
d = json.load(open("samples/duck.input.json"))
|
|
210
|
+
print("manifest", ep.publish_manifest(d["manifest"]).created, "| announce", ep.publish(topic=d["announce"]["path"], key=d["announce"]["key"], record=d["announce"]["record"]).created)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```console
|
|
214
|
+
manifest True | announce True
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Resolve and verify, two lines.** The URI resolved per the endpoint's own
|
|
218
|
+
§7.5.2 metadata, and the asset's bytes checked against the hash the manifest
|
|
219
|
+
declared.
|
|
220
|
+
|
|
221
|
+
<!-- bar: resolve -->
|
|
222
|
+
```python
|
|
223
|
+
import json, spatialdds as sd
|
|
224
|
+
ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret"); ep.publish_manifest(json.load(open("samples/duck.input.json"))["manifest"]); print(len(ep.resolve("spatialdds://demo.spatialdds.org/south-mall/content/duck").fetch_asset()), "bytes verified")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```console
|
|
228
|
+
1432 bytes verified
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The duck's manifest is published on the first line because these two lines
|
|
232
|
+
have to stand alone. Against an endpoint that already holds the manifest, that
|
|
233
|
+
call goes away and the bar is one line.
|
|
234
|
+
|
|
235
|
+
## The surface
|
|
236
|
+
|
|
237
|
+
| | |
|
|
238
|
+
| --- | --- |
|
|
239
|
+
| `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
|
+
| `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
|
|
241
|
+
| `ep.near(lat, lon, radius_m)` | a helper over `search`, never a second protocol |
|
|
242
|
+
| `ep.resolve(uri)` | §7.5.2 resolution → `Resolved` |
|
|
243
|
+
| `ep.authority` | the DNS name the endpoint says it answers as |
|
|
244
|
+
| `ep.publish(topic=, key=, record=, if_match=None)` | PUT one instance → `Written` |
|
|
245
|
+
| `ep.publish_manifest(record)` | PUT a manifest, keyed by its own `id` |
|
|
246
|
+
| `ep.publish_service(service_id=, kind=, coverage=, manifest=None, ...)` | build and PUT an announce, and its manifest if given |
|
|
247
|
+
| `ep.publish_content(content_id=, position=, asset=, ...)` | build and PUT a content manifest and its announce |
|
|
248
|
+
| `ep.publish_anchor(anchor_id, lat=, lon=, method=, content=None, ...)` | build and PUT an anchor, optionally naming content |
|
|
249
|
+
| `ep.withdraw(topic=, key=)` | DELETE: leaves the latched set and disposes |
|
|
250
|
+
| `hit.service_id`, `.kind`, `.coverage`, `.topics` | the typed service manifest |
|
|
251
|
+
| `hit.serves(type_name)` | does this service offer a topic of that type? for picking one out of a search result |
|
|
252
|
+
| `hit.topic(type_name)` | a `Topic`, URL taken from the manifest, never built |
|
|
253
|
+
| `hit.reaches(lat, lon, radius_m)` | does the declared coverage reach a point |
|
|
254
|
+
| `topic.type`, `.url`, `.name`, `.durability`, `.access`, `.fqn` | what the manifest said |
|
|
255
|
+
| `topic.latched()` | every live instance, lifted |
|
|
256
|
+
| `topic.instance(key)` | one instance, key percent-encoded per N.3 |
|
|
257
|
+
| `topic.subscribe(timeout=None)` | a generator of `Sample` / `Dispose` / `EndOfReplay` / `Liveliness` |
|
|
258
|
+
| `resolved.doc`, `.uri`, `.etag`, `.assets` | the resolved manifest |
|
|
259
|
+
| `resolved.asset_url(i)`, `.fetch_asset(i)` | the URL it would fetch, and the verified bytes |
|
|
260
|
+
| `written.doc`, `.etag`, `.created`, `.manifest` | what a publish returned |
|
|
261
|
+
| `sd.service_manifest(service_id, kind, authority=, scene=, profiles=, request=, response=, limits=, auth=, coverage=, cells=)` | the contract behind an announce; every member a claim, so every member an argument |
|
|
262
|
+
| `sd.capabilities(supported=, preferred=, features=)`, `sd.profile(name, minor=)` | what a service supports, for `service_announce(caps=)` |
|
|
263
|
+
| `sd.asset(path_or_bytes, media_type=)` / `sd.asset_ref(url, hash=, media_type=)` | an asset hashed from its bytes, or a reference to one hosted elsewhere |
|
|
264
|
+
| `sd.records` | the builders, listed above |
|
|
265
|
+
| `sd.lift(fqn, doc)` | the lift on its own, for JSON you already have |
|
|
266
|
+
| `sd.SPEC_COMMIT`, `sd.APPENDIX_N`, `sd.__version__` | what this speaks |
|
|
267
|
+
|
|
268
|
+
`ep.near()` picks a geohash precision whose cell covers the radius, encodes the
|
|
269
|
+
centre and eight points around the query circle so the covering cells are found
|
|
270
|
+
without neighbour arithmetic, searches each distinct cell, and filters the hits
|
|
271
|
+
by their own declared coverage. Everything it sends is an ordinary §3.3.0
|
|
272
|
+
geohash query.
|
|
273
|
+
|
|
274
|
+
`topic.url` and `assets[].uri` are followed by different rules, deliberately. A
|
|
275
|
+
topic URL comes out of a service manifest this endpoint served, so its path is
|
|
276
|
+
this endpoint's and the library follows it by path -- which is what makes a
|
|
277
|
+
registry advertising a DNS name usable from a laptop. An asset URI is an
|
|
278
|
+
ordinary web URL that usually points somewhere else entirely, so it is fetched
|
|
279
|
+
exactly as written; the one exception is an asset on the endpoint's own
|
|
280
|
+
advertised authority, which is rebased onto the endpoint you are talking to.
|
|
281
|
+
|
|
282
|
+
## Errors
|
|
283
|
+
|
|
284
|
+
One family, built from the endpoint's own N.8 problem documents — `status`,
|
|
285
|
+
`type`, `detail` carried, not re-invented. Everything below derives from
|
|
286
|
+
`SpatialDDSError`, so one `except` catches the lot.
|
|
287
|
+
|
|
288
|
+
| | |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| `Unreachable` | the endpoint did not answer; names the URL and the serve line |
|
|
291
|
+
| `Unauthorized` | 401; says to pass `token=` and where the token is printed |
|
|
292
|
+
| `SurfaceOff` | 405 from a write verb; says to start with `--publish` |
|
|
293
|
+
| `Problem` | any other refusal, with `.status`, `.type`, `.slug`, `.detail` |
|
|
294
|
+
| `IntegrityError` | fetched bytes do not match the declared hash |
|
|
295
|
+
| `UnverifiableIntegrity` | the declared value is not a hash this library can check |
|
|
296
|
+
|
|
297
|
+
That last one is the unusual one and it is deliberate. SpatialDDS counts "a
|
|
298
|
+
checksum field in the body" as a §7.5.2 integrity signal, but nothing specifies
|
|
299
|
+
what such a field digests or in what form (spatialdds-web finding 31). Where a
|
|
300
|
+
declared value does not parse as a SHA-256 of the asset's bytes, this library
|
|
301
|
+
raises rather than reporting a check it did not perform.
|
|
302
|
+
|
|
303
|
+
## Integrity by default
|
|
304
|
+
|
|
305
|
+
Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
|
|
306
|
+
the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
|
|
307
|
+
site, nowhere else.
|
|
308
|
+
|
|
309
|
+
## Not in 0.3.0
|
|
310
|
+
|
|
311
|
+
Stated so nobody waits for them: no async API, no caching beyond ETag
|
|
312
|
+
revalidation, no retry policy, no MCAP tooling beyond the journal the server
|
|
313
|
+
writes, and no DDS dependency of any kind. The wire is HTTP and a WebSocket.
|
|
314
|
+
|
|
315
|
+
`sd.asset` changed meaning in 0.3.0, which is why the version moved: it now
|
|
316
|
+
reads bytes and computes a hash, where in 0.2.0 it took a URL and a hash you
|
|
317
|
+
supplied. That second form is `sd.asset_ref`. A published release has to stay
|
|
318
|
+
reproducible from a public commit, so the API could not change under the same
|
|
319
|
+
number.
|
|
320
|
+
|
|
321
|
+
On the registry, four things are deferred rather than missing, and the same
|
|
322
|
+
four are on the 1.9 candidates list: **leases** (nothing expires; `ttl_sec` is
|
|
323
|
+
advice to caches), **per-provider scopes** (one token, one operator),
|
|
324
|
+
**server-set identity** (`provider_id` is whatever the publisher says), and
|
|
325
|
+
**upstream subscriptions** (a registry does not follow another registry).
|