spatialdds 0.1.0__tar.gz → 0.2.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.
Files changed (151) hide show
  1. {spatialdds-0.1.0 → spatialdds-0.2.0}/PKG-INFO +123 -22
  2. spatialdds-0.2.0/README.md +270 -0
  3. spatialdds-0.2.0/pyproject.toml +68 -0
  4. spatialdds-0.2.0/spatialdds/__init__.py +88 -0
  5. spatialdds-0.2.0/spatialdds/artifacts/idl/core.idl +340 -0
  6. spatialdds-0.2.0/spatialdds/artifacts/idl/discovery.idl +238 -0
  7. spatialdds-0.2.0/spatialdds/artifacts/idl/types.idl +156 -0
  8. spatialdds-0.2.0/spatialdds/artifacts/schemas/builtin_Time.json +22 -0
  9. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +235 -0
  10. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +197 -0
  11. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +314 -0
  12. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +20 -0
  13. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +335 -0
  14. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +306 -0
  15. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +19 -0
  16. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +298 -0
  17. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +355 -0
  18. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +73 -0
  19. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +56 -0
  20. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_common_KV.json +19 -0
  21. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +45 -0
  22. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_common_Vec2.json +19 -0
  23. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +29 -0
  24. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +38 -0
  25. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +23 -0
  26. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +19 -0
  27. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_Edge.json +113 -0
  28. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +283 -0
  29. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +231 -0
  30. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +223 -0
  31. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +254 -0
  32. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +157 -0
  33. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +113 -0
  34. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_Node.json +245 -0
  35. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +214 -0
  36. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +95 -0
  37. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +29 -0
  38. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +54 -0
  39. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +66 -0
  40. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_TileKey.json +35 -0
  41. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +133 -0
  42. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +122 -0
  43. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_Announce.json +488 -0
  44. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +67 -0
  45. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +343 -0
  46. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +181 -0
  47. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +35 -0
  48. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +307 -0
  49. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +267 -0
  50. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +264 -0
  51. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_Depart.json +40 -0
  52. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_KV.json +19 -0
  53. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +33 -0
  54. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +240 -0
  55. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +41 -0
  56. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_Transform.json +153 -0
  57. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +42 -0
  58. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +235 -0
  59. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +349 -0
  60. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +425 -0
  61. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +132 -0
  62. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +173 -0
  63. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +248 -0
  64. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +97 -0
  65. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +471 -0
  66. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +65 -0
  67. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_owm_Entity.json +250 -0
  68. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +124 -0
  69. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +74 -0
  70. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +77 -0
  71. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +113 -0
  72. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +493 -0
  73. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +533 -0
  74. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +215 -0
  75. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +328 -0
  76. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +32 -0
  77. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +23 -0
  78. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +85 -0
  79. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +111 -0
  80. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +36 -0
  81. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +25 -0
  82. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +91 -0
  83. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +136 -0
  84. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +200 -0
  85. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +112 -0
  86. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +34 -0
  87. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +141 -0
  88. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +237 -0
  89. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +235 -0
  90. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +147 -0
  91. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +308 -0
  92. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +202 -0
  93. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +222 -0
  94. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +357 -0
  95. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +149 -0
  96. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +327 -0
  97. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +214 -0
  98. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +285 -0
  99. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +236 -0
  100. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +212 -0
  101. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +103 -0
  102. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +23 -0
  103. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +46 -0
  104. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +102 -0
  105. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +229 -0
  106. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +302 -0
  107. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +143 -0
  108. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +31 -0
  109. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +35 -0
  110. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +108 -0
  111. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +84 -0
  112. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +84 -0
  113. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +23 -0
  114. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +80 -0
  115. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +231 -0
  116. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +113 -0
  117. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +179 -0
  118. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +115 -0
  119. spatialdds-0.2.0/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +133 -0
  120. {spatialdds-0.1.0 → spatialdds-0.2.0}/spatialdds/canonical.py +15 -11
  121. spatialdds-0.2.0/spatialdds/canonjson.py +74 -0
  122. spatialdds-0.1.0/spatialdds/__init__.py → spatialdds-0.2.0/spatialdds/client.py +192 -34
  123. spatialdds-0.2.0/spatialdds/demo/assets/duck.gltf +66 -0
  124. spatialdds-0.2.0/spatialdds/demo/assets/fountain-label.txt +9 -0
  125. spatialdds-0.2.0/spatialdds/demo/assets/fountain-marker.gltf +64 -0
  126. spatialdds-0.2.0/spatialdds/demo/fountain-anchors.mcap +0 -0
  127. spatialdds-0.2.0/spatialdds/demo/manifests/content-fountain-label.json +59 -0
  128. spatialdds-0.2.0/spatialdds/demo/manifests/content-fountain-marker.json +59 -0
  129. spatialdds-0.2.0/spatialdds/records.py +608 -0
  130. spatialdds-0.2.0/spatialdds/serve.py +311 -0
  131. spatialdds-0.2.0/spatialdds/server/__init__.py +21 -0
  132. spatialdds-0.2.0/spatialdds/server/gateway.py +695 -0
  133. spatialdds-0.2.0/spatialdds/server/gw_http.py +741 -0
  134. spatialdds-0.2.0/spatialdds/server/idlmodel.py +439 -0
  135. spatialdds-0.2.0/spatialdds/server/pin.py +146 -0
  136. spatialdds-0.2.0/spatialdds/server/publish.py +374 -0
  137. spatialdds-0.2.0/spatialdds/server/webjson.py +302 -0
  138. spatialdds-0.2.0/spatialdds/server/xcdr2.py +442 -0
  139. {spatialdds-0.1.0 → spatialdds-0.2.0}/spatialdds.egg-info/PKG-INFO +123 -22
  140. spatialdds-0.2.0/spatialdds.egg-info/SOURCES.txt +145 -0
  141. spatialdds-0.2.0/spatialdds.egg-info/entry_points.txt +2 -0
  142. spatialdds-0.2.0/spatialdds.egg-info/requires.txt +7 -0
  143. spatialdds-0.1.0/README.md +0 -173
  144. spatialdds-0.1.0/pyproject.toml +0 -36
  145. spatialdds-0.1.0/spatialdds.egg-info/SOURCES.txt +0 -11
  146. spatialdds-0.1.0/spatialdds.egg-info/requires.txt +0 -2
  147. {spatialdds-0.1.0 → spatialdds-0.2.0}/setup.cfg +0 -0
  148. {spatialdds-0.1.0/spatialdds → spatialdds-0.2.0/spatialdds/artifacts}/serializer_tables.json +0 -0
  149. {spatialdds-0.1.0 → spatialdds-0.2.0}/spatialdds/errors.py +0 -0
  150. {spatialdds-0.1.0 → spatialdds-0.2.0}/spatialdds.egg-info/dependency_links.txt +0 -0
  151. {spatialdds-0.1.0 → spatialdds-0.2.0}/spatialdds.egg-info/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialdds
3
- Version: 0.1.0
4
- Summary: SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N).
3
+ Version: 0.2.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
5
  Author: Open AR Cloud
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/OpenArCloud/spatialdds-web
8
8
  Project-URL: Specification, https://github.com/OpenArCloud/SpatialDDS-spec
9
- Keywords: spatialdds,spatial,geopose,anchors,openarcloud
9
+ Keywords: spatialdds,spatial,geopose,anchors,registry,openarcloud
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
@@ -17,24 +17,106 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: requests>=2.25
19
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"
20
24
 
21
25
  # spatialdds
22
26
 
23
- SpatialDDS over plain HTTPS and a WebSocket, for programs.
27
+ SpatialDDS over plain HTTPS and a WebSocket: a client for programs, and the
28
+ reference registry behind one command.
24
29
 
25
30
  ```sh
26
- pip install spatialdds
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
27
34
  ```
28
35
 
29
- `fetch` and a WebSocket really are the whole client — the endpoint's own README
30
- shows that in three lines of `curl`, and nothing here replaces it. This package
31
- is the step after that, when a program wants native values instead of canonical
32
- JSON, bytes it can trust, and refusals it can act on.
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.
33
41
 
34
42
  **Implements Appendix N as of SpatialDDS-spec `9c3e1f2`.** Package versions and
35
43
  specification versions move independently; `spatialdds.SPEC_COMMIT` is the one
36
44
  that says what the wire looks like.
37
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
+ The builders are also usable on their own: `sd.service_announce`,
93
+ `sd.content_announce`, `sd.content_manifest`, `sd.geo_anchor`, with
94
+ `sd.circle`, `sd.bbox`, `sd.geopose`, `sd.cov_pos3`, `sd.cov_none`,
95
+ `sd.frame`,
96
+ `sd.earth_fixed`, `sd.stamp`, `sd.topic`, `sd.asset`, `sd.cells`,
97
+ `sd.service_uri` and `sd.content_uri`. Each returns a plain dict in canonical
98
+ form, publishable as it stands and dumpable as the authoring JSON this
99
+ repository's tools read. `spatialdds.records` is the module they live in.
100
+
101
+ **What they fill in, and what they refuse to.** Three conventions, each
102
+ documented where it is applied: a stamp of now unless you pass `when=`, an
103
+ earth-fixed frame unless you pass one, and URIs assembled under a stated
104
+ authority -- never a guessed one. `ep.publish_*` gets the authority from the
105
+ endpoint itself (`ep.authority`, which asks the resolver metadata, then the
106
+ endpoint's own 404 document at `/`, then the address you connected to). What
107
+ they never fill is **identity**: `provider_id` and `org` are yours to state,
108
+ because a library that invented a provider id would be inventing provenance.
109
+
110
+ `GeoAnchor.checksum` is required by 1.8 and specified by nobody
111
+ (spatialdds-web finding 31). The builder fills it with the SHA-256 of the
112
+ record's own canonical JSON with `checksum` emptied -- a convention, chosen
113
+ because a reader can recompute it from what was served. If the specification
114
+ settles the semantics, this changes.
115
+
116
+ The full map of builder argument to IDL member, including every member filled
117
+ by convention, is printed by `python3 gates/builders.py --census` in the
118
+ repository.
119
+
38
120
  ## What it is for: the lift
39
121
 
40
122
  An endpoint serves Appendix N.1 canonical JSON, and three rules separate that
@@ -46,12 +128,13 @@ from the values a program wants. All three are easy to get subtly wrong:
46
128
  * An octet run is **base64**.
47
129
  * A union is tagged on `"type"`, and carries `_discriminator` once lifted.
48
130
 
49
- These are not this library's rules — they are the reference gateway's, and the
50
- repository gates that the two agree by lifting every record in both committed
51
- recordings with each and comparing. Same rules, same results, different source
52
- of the type information: the gateway reads the IDL a recording carries, and a
53
- client has no recording, so this reads the generated serializer tables that
54
- travel with the package.
131
+ These are not this library's rules -- they are the reference gateway's, and
132
+ the repository gates that the two agree by lifting every record in both
133
+ committed recordings with each implementation and comparing. Same rules, same
134
+ results, different source of the type information: the server reads the IDL,
135
+ and a client reading a remote endpoint has none, so this reads the generated
136
+ serializer tables that travel with the package. That gate earned its keep on
137
+ 2026-10-15, when the disagreement it found turned out to be in the server.
55
138
 
56
139
  A lifted union keeps `_discriminator` rather than `"type"`, because that is
57
140
  what the lift produces and inventing a fourth shape would be a fourth rule.
@@ -132,12 +215,16 @@ call goes away and the bar is one line.
132
215
 
133
216
  | | |
134
217
  | --- | --- |
135
- | `sd.connect(base, token=None, verify_integrity=True, timeout=15.0, lifted=True)` | an `Endpoint`; nothing is fetched until you ask |
218
+ | `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 |
136
219
  | `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
137
220
  | `ep.near(lat, lon, radius_m)` | a helper over `search`, never a second protocol |
138
221
  | `ep.resolve(uri)` | §7.5.2 resolution → `Resolved` |
222
+ | `ep.authority` | the DNS name the endpoint says it answers as |
139
223
  | `ep.publish(topic=, key=, record=, if_match=None)` | PUT one instance → `Written` |
140
224
  | `ep.publish_manifest(record)` | PUT a manifest, keyed by its own `id` |
225
+ | `ep.publish_service(service_id=, kind=, coverage=, manifest=None, ...)` | build and PUT an announce, and its manifest if given |
226
+ | `ep.publish_content(content_id=, position=, asset=, ...)` | build and PUT a content manifest and its announce |
227
+ | `ep.publish_anchor(anchor_id, lat=, lon=, method=, content=None, ...)` | build and PUT an anchor, optionally naming content |
141
228
  | `ep.withdraw(topic=, key=)` | DELETE: leaves the latched set and disposes |
142
229
  | `hit.service_id`, `.kind`, `.coverage`, `.topics` | the typed service manifest |
143
230
  | `hit.topic(type_name)` | a `Topic`, URL taken from the manifest, never built |
@@ -148,7 +235,8 @@ call goes away and the bar is one line.
148
235
  | `topic.subscribe(timeout=None)` | a generator of `Sample` / `Dispose` / `EndOfReplay` / `Liveliness` |
149
236
  | `resolved.doc`, `.uri`, `.etag`, `.assets` | the resolved manifest |
150
237
  | `resolved.asset_url(i)`, `.fetch_asset(i)` | the URL it would fetch, and the verified bytes |
151
- | `written.doc`, `.etag`, `.created` | what a publish returned |
238
+ | `written.doc`, `.etag`, `.created`, `.manifest` | what a publish returned |
239
+ | `sd.records` | the builders, listed above |
152
240
  | `sd.lift(fqn, doc)` | the lift on its own, for JSON you already have |
153
241
  | `sd.SPEC_COMMIT`, `sd.APPENDIX_N`, `sd.__version__` | what this speaks |
154
242
 
@@ -158,6 +246,14 @@ without neighbour arithmetic, searches each distinct cell, and filters the hits
158
246
  by their own declared coverage. Everything it sends is an ordinary §3.3.0
159
247
  geohash query.
160
248
 
249
+ `topic.url` and `assets[].uri` are followed by different rules, deliberately. A
250
+ topic URL comes out of a service manifest this endpoint served, so its path is
251
+ this endpoint's and the library follows it by path -- which is what makes a
252
+ registry advertising a DNS name usable from a laptop. An asset URI is an
253
+ ordinary web URL that usually points somewhere else entirely, so it is fetched
254
+ exactly as written; the one exception is an asset on the endpoint's own
255
+ advertised authority, which is rebased onto the endpoint you are talking to.
256
+
161
257
  ## Errors
162
258
 
163
259
  One family, built from the endpoint's own N.8 problem documents — `status`,
@@ -185,9 +281,14 @@ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
185
281
  the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
186
282
  site, nowhere else.
187
283
 
188
- ## Not in 0.1.0
284
+ ## Not in 0.2.0
285
+
286
+ Stated so nobody waits for them: no async API, no caching beyond ETag
287
+ revalidation, no retry policy, no MCAP tooling beyond the journal the server
288
+ writes, and no DDS dependency of any kind. The wire is HTTP and a WebSocket.
189
289
 
190
- Stated so nobody waits for them: no CLI, no async API, no caching beyond ETag
191
- revalidation, no retry policy, no MCAP tooling, and no DDS dependency of any
192
- kind. The wire is HTTP and a WebSocket; two dependencies, `requests` and
193
- `websockets`, and nothing else.
290
+ On the registry, four things are deferred rather than missing, and the same
291
+ four are on the 1.9 candidates list: **leases** (nothing expires; `ttl_sec` is
292
+ advice to caches), **per-provider scopes** (one token, one operator),
293
+ **server-set identity** (`provider_id` is whatever the publisher says), and
294
+ **upstream subscriptions** (a registry does not follow another registry).
@@ -0,0 +1,270 @@
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
+ The builders are also usable on their own: `sd.service_announce`,
69
+ `sd.content_announce`, `sd.content_manifest`, `sd.geo_anchor`, with
70
+ `sd.circle`, `sd.bbox`, `sd.geopose`, `sd.cov_pos3`, `sd.cov_none`,
71
+ `sd.frame`,
72
+ `sd.earth_fixed`, `sd.stamp`, `sd.topic`, `sd.asset`, `sd.cells`,
73
+ `sd.service_uri` and `sd.content_uri`. Each returns a plain dict in canonical
74
+ form, publishable as it stands and dumpable as the authoring JSON this
75
+ repository's tools read. `spatialdds.records` is the module they live in.
76
+
77
+ **What they fill in, and what they refuse to.** Three conventions, each
78
+ documented where it is applied: a stamp of now unless you pass `when=`, an
79
+ earth-fixed frame unless you pass one, and URIs assembled under a stated
80
+ authority -- never a guessed one. `ep.publish_*` gets the authority from the
81
+ endpoint itself (`ep.authority`, which asks the resolver metadata, then the
82
+ endpoint's own 404 document at `/`, then the address you connected to). What
83
+ they never fill is **identity**: `provider_id` and `org` are yours to state,
84
+ because a library that invented a provider id would be inventing provenance.
85
+
86
+ `GeoAnchor.checksum` is required by 1.8 and specified by nobody
87
+ (spatialdds-web finding 31). The builder fills it with the SHA-256 of the
88
+ record's own canonical JSON with `checksum` emptied -- a convention, chosen
89
+ because a reader can recompute it from what was served. If the specification
90
+ settles the semantics, this changes.
91
+
92
+ The full map of builder argument to IDL member, including every member filled
93
+ by convention, is printed by `python3 gates/builders.py --census` in the
94
+ repository.
95
+
96
+ ## What it is for: the lift
97
+
98
+ An endpoint serves Appendix N.1 canonical JSON, and three rules separate that
99
+ from the values a program wants. All three are easy to get subtly wrong:
100
+
101
+ * `int64` and `uint64` are **unconditional decimal strings**. A client that
102
+ treats `stamp.sec` as a string gets surprising comparisons; one that calls
103
+ `int()` on every string corrupts identifiers.
104
+ * An octet run is **base64**.
105
+ * A union is tagged on `"type"`, and carries `_discriminator` once lifted.
106
+
107
+ These are not this library's rules -- they are the reference gateway's, and
108
+ the repository gates that the two agree by lifting every record in both
109
+ committed recordings with each implementation and comparing. Same rules, same
110
+ results, different source of the type information: the server reads the IDL,
111
+ and a client reading a remote endpoint has none, so this reads the generated
112
+ serializer tables that travel with the package. That gate earned its keep on
113
+ 2026-10-15, when the disagreement it found turned out to be in the server.
114
+
115
+ A lifted union keeps `_discriminator` rather than `"type"`, because that is
116
+ what the lift produces and inventing a fourth shape would be a fourth rule.
117
+ Pass `lifted=False` to `connect()` to get the canonical JSON untouched.
118
+
119
+ ## The three bars
120
+
121
+ Each one runs on its own, pasted into a fresh shell with nothing else run
122
+ first, which is why the serve line all three need is printed here rather than
123
+ assumed. `gates/library.py` executes each bar against its own fresh endpoint
124
+ and compares its output to what is printed below it.
125
+
126
+ ```sh
127
+ python3 tools/run_gateway.py --mcap samples/fountain-anchors.mcap \
128
+ --manifests samples/manifests --assets samples/assets \
129
+ --publish --token not-a-secret --public-url https://demo.spatialdds.org
130
+ ```
131
+
132
+ **Hello spatial, six lines.** Discover, subscribe, and read typed samples until
133
+ the latched set is complete.
134
+
135
+ <!-- bar: read -->
136
+ ```python
137
+ import spatialdds as sd
138
+ ep = sd.connect("http://127.0.0.1:8787")
139
+ svc = ep.search(geohash="9v6krc")[0]
140
+ for ev in svc.topic("geo_anchor").subscribe(timeout=10):
141
+ if isinstance(ev, sd.EndOfReplay): print("latched set complete:", ev.replayed); break
142
+ print(" ", ev.key, ev.data["method"], ev.data["geopose"]["stamp"]["sec"])
143
+ ```
144
+
145
+ ```console
146
+ anchor/fountain-north-rim Surveyed 1791559800
147
+ anchor/fountain-south-rim VisualFix 1791559800
148
+ anchor/fountain-east-rim VisualFix 1791559800
149
+ anchor/fountain-west-rim VisualFix 1791559800
150
+ anchor/fountain-plaque GNSS 1791559800
151
+ latched set complete: 5
152
+ ```
153
+
154
+ `geopose.stamp.sec` arrives as an `int` there, not as `"1791559800"`. That is
155
+ the whole lift, in the one place a reader will notice it.
156
+
157
+ **Publish the duck, four lines.** The content manifest, and the announce that
158
+ places it.
159
+
160
+ <!-- bar: publish -->
161
+ ```python
162
+ import json, spatialdds as sd
163
+ ep = sd.connect("http://127.0.0.1:8787", token="not-a-secret")
164
+ d = json.load(open("samples/duck.input.json"))
165
+ print("manifest", ep.publish_manifest(d["manifest"]).created, "| announce", ep.publish(topic=d["announce"]["path"], key=d["announce"]["key"], record=d["announce"]["record"]).created)
166
+ ```
167
+
168
+ ```console
169
+ manifest True | announce True
170
+ ```
171
+
172
+ **Resolve and verify, two lines.** The URI resolved per the endpoint's own
173
+ §7.5.2 metadata, and the asset's bytes checked against the hash the manifest
174
+ declared.
175
+
176
+ <!-- bar: resolve -->
177
+ ```python
178
+ import json, spatialdds as sd
179
+ 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")
180
+ ```
181
+
182
+ ```console
183
+ 1432 bytes verified
184
+ ```
185
+
186
+ The duck's manifest is published on the first line because these two lines
187
+ have to stand alone. Against an endpoint that already holds the manifest, that
188
+ call goes away and the bar is one line.
189
+
190
+ ## The surface
191
+
192
+ | | |
193
+ | --- | --- |
194
+ | `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 |
195
+ | `ep.search(geohash=None, kinds=None)` | §3.3.0 search → a list of `Hit` |
196
+ | `ep.near(lat, lon, radius_m)` | a helper over `search`, never a second protocol |
197
+ | `ep.resolve(uri)` | §7.5.2 resolution → `Resolved` |
198
+ | `ep.authority` | the DNS name the endpoint says it answers as |
199
+ | `ep.publish(topic=, key=, record=, if_match=None)` | PUT one instance → `Written` |
200
+ | `ep.publish_manifest(record)` | PUT a manifest, keyed by its own `id` |
201
+ | `ep.publish_service(service_id=, kind=, coverage=, manifest=None, ...)` | build and PUT an announce, and its manifest if given |
202
+ | `ep.publish_content(content_id=, position=, asset=, ...)` | build and PUT a content manifest and its announce |
203
+ | `ep.publish_anchor(anchor_id, lat=, lon=, method=, content=None, ...)` | build and PUT an anchor, optionally naming content |
204
+ | `ep.withdraw(topic=, key=)` | DELETE: leaves the latched set and disposes |
205
+ | `hit.service_id`, `.kind`, `.coverage`, `.topics` | the typed service manifest |
206
+ | `hit.topic(type_name)` | a `Topic`, URL taken from the manifest, never built |
207
+ | `hit.reaches(lat, lon, radius_m)` | does the declared coverage reach a point |
208
+ | `topic.type`, `.url`, `.name`, `.durability`, `.access`, `.fqn` | what the manifest said |
209
+ | `topic.latched()` | every live instance, lifted |
210
+ | `topic.instance(key)` | one instance, key percent-encoded per N.3 |
211
+ | `topic.subscribe(timeout=None)` | a generator of `Sample` / `Dispose` / `EndOfReplay` / `Liveliness` |
212
+ | `resolved.doc`, `.uri`, `.etag`, `.assets` | the resolved manifest |
213
+ | `resolved.asset_url(i)`, `.fetch_asset(i)` | the URL it would fetch, and the verified bytes |
214
+ | `written.doc`, `.etag`, `.created`, `.manifest` | what a publish returned |
215
+ | `sd.records` | the builders, listed above |
216
+ | `sd.lift(fqn, doc)` | the lift on its own, for JSON you already have |
217
+ | `sd.SPEC_COMMIT`, `sd.APPENDIX_N`, `sd.__version__` | what this speaks |
218
+
219
+ `ep.near()` picks a geohash precision whose cell covers the radius, encodes the
220
+ centre and eight points around the query circle so the covering cells are found
221
+ without neighbour arithmetic, searches each distinct cell, and filters the hits
222
+ by their own declared coverage. Everything it sends is an ordinary §3.3.0
223
+ geohash query.
224
+
225
+ `topic.url` and `assets[].uri` are followed by different rules, deliberately. A
226
+ topic URL comes out of a service manifest this endpoint served, so its path is
227
+ this endpoint's and the library follows it by path -- which is what makes a
228
+ registry advertising a DNS name usable from a laptop. An asset URI is an
229
+ ordinary web URL that usually points somewhere else entirely, so it is fetched
230
+ exactly as written; the one exception is an asset on the endpoint's own
231
+ advertised authority, which is rebased onto the endpoint you are talking to.
232
+
233
+ ## Errors
234
+
235
+ One family, built from the endpoint's own N.8 problem documents — `status`,
236
+ `type`, `detail` carried, not re-invented. Everything below derives from
237
+ `SpatialDDSError`, so one `except` catches the lot.
238
+
239
+ | | |
240
+ | --- | --- |
241
+ | `Unreachable` | the endpoint did not answer; names the URL and the serve line |
242
+ | `Unauthorized` | 401; says to pass `token=` and where the token is printed |
243
+ | `SurfaceOff` | 405 from a write verb; says to start with `--publish` |
244
+ | `Problem` | any other refusal, with `.status`, `.type`, `.slug`, `.detail` |
245
+ | `IntegrityError` | fetched bytes do not match the declared hash |
246
+ | `UnverifiableIntegrity` | the declared value is not a hash this library can check |
247
+
248
+ That last one is the unusual one and it is deliberate. SpatialDDS counts "a
249
+ checksum field in the body" as a §7.5.2 integrity signal, but nothing specifies
250
+ what such a field digests or in what form (spatialdds-web finding 31). Where a
251
+ declared value does not parse as a SHA-256 of the asset's bytes, this library
252
+ raises rather than reporting a check it did not perform.
253
+
254
+ ## Integrity by default
255
+
256
+ Latched reads revalidate with the ETag the endpoint gave; `fetch_asset` checks
257
+ the bytes. Opting out exists and is loud: `verify_integrity=False`, at the call
258
+ site, nowhere else.
259
+
260
+ ## Not in 0.2.0
261
+
262
+ Stated so nobody waits for them: no async API, no caching beyond ETag
263
+ revalidation, no retry policy, no MCAP tooling beyond the journal the server
264
+ writes, and no DDS dependency of any kind. The wire is HTTP and a WebSocket.
265
+
266
+ On the registry, four things are deferred rather than missing, and the same
267
+ four are on the 1.9 candidates list: **leases** (nothing expires; `ttl_sec` is
268
+ advice to caches), **per-provider scopes** (one token, one operator),
269
+ **server-set identity** (`provider_id` is whatever the publisher says), and
270
+ **upstream subscriptions** (a registry does not follow another registry).
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spatialdds"
7
+ version = "0.2.0"
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
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Open AR Cloud" }]
13
+ keywords = ["spatialdds", "spatial", "geopose", "anchors", "registry", "openarcloud"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Scientific/Engineering :: GIS",
21
+ ]
22
+ # Two, and no more, for reading and publishing. The wire is HTTP and a
23
+ # WebSocket; anything else would be this library's opinion rather than the
24
+ # binding's requirement.
25
+ dependencies = ["requests>=2.25", "websockets>=12"]
26
+
27
+ [project.optional-dependencies]
28
+ # Being an endpoint is the part you opt into. aiohttp serves it, mcap is the
29
+ # format a registry's state lives in, and jsonschema validates every published
30
+ # body against the vendored schemas before it reaches the wire. That last one
31
+ # is not optional in any useful sense: without it the write surface refuses
32
+ # every publish with a 500 rather than accept something unvalidated, so
33
+ # leaving it out of this group would ship a registry nobody can publish into.
34
+ server = ["aiohttp>=3.8", "mcap>=1.1", "jsonschema>=4.0"]
35
+
36
+ [project.scripts]
37
+ spatialdds-serve = "spatialdds.serve:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/OpenArCloud/spatialdds-web"
41
+ Specification = "https://github.com/OpenArCloud/SpatialDDS-spec"
42
+
43
+ [tool.setuptools]
44
+ packages = ["spatialdds", "spatialdds.server"]
45
+
46
+ [tool.setuptools.package-data]
47
+ # Everything vendored travels with the package, because the point of 0.2.0 is
48
+ # that `pip install` is enough:
49
+ #
50
+ # * `artifacts/serializer_tables.json` -- what the lift reads, in both
51
+ # directions. A client has no recording to learn the types from.
52
+ # * `artifacts/schemas/*.json` -- the 112 generated JSON Schemas. The endpoint
53
+ # publishes them and validates against them, and §8.4's third-party reader
54
+ # has nothing else to go on.
55
+ # * `artifacts/idl/*.idl` -- the pinned IDL. A registry that starts empty
56
+ # boots its types from this; the tables cannot stand in because they carry
57
+ # no `@key`, so a keyed resource would have no instance names.
58
+ # * `demo/**` -- the fountain set `--demo` serves. Byte-identical to the
59
+ # repository's `samples/` copy, which `tools/pack_demo.py` maintains and
60
+ # `gates/serve_demo.py` checks.
61
+ spatialdds = [
62
+ "artifacts/serializer_tables.json",
63
+ "artifacts/schemas/*.json",
64
+ "artifacts/idl/*.idl",
65
+ "demo/*.mcap",
66
+ "demo/manifests/*.json",
67
+ "demo/assets/*",
68
+ ]
@@ -0,0 +1,88 @@
1
+ """SpatialDDS over plain HTTPS and a WebSocket, for programs.
2
+
3
+ `fetch` and a WebSocket really are the whole client -- the endpoint's README
4
+ shows that in three lines of `curl` -- and this package exists for the step
5
+ after that, when a program wants native values, verified bytes, and refusals it
6
+ can act on.
7
+
8
+ import spatialdds as sd
9
+
10
+ ep = sd.connect("https://demo.spatialdds.org")
11
+ svc = ep.search(geohash="9v6krc")[0]
12
+ for a in svc.topic("geo_anchor").latched():
13
+ print(a["anchor_id"], a["geopose"]["stamp"]["sec"]) # an int, not "17…"
14
+
15
+ What it implements: Appendix N as of SpatialDDS-spec `9c3e1f2`. Package
16
+ versions move independently of specification versions; `SPEC_COMMIT` is the
17
+ one to check.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import importlib
22
+ from typing import Any
23
+
24
+ __all__ = [
25
+ "connect", "Endpoint", "Hit", "Topic", "Resolved", "Written",
26
+ "Sample", "Dispose", "EndOfReplay", "Liveliness",
27
+ "SpatialDDSError", "Unreachable", "Problem", "Unauthorized", "SurfaceOff",
28
+ "IntegrityError", "UnverifiableIntegrity",
29
+ "lift", "SPEC_COMMIT", "APPENDIX_N", "__version__",
30
+ # The record builders, re-exported so that `sd.circle(...)` reads the way
31
+ # it does in the documentation. They live in `spatialdds.records`.
32
+ "records", "circle", "bbox", "earth_fixed", "frame", "stamp", "geopose",
33
+ "cov_pos3", "cov_none", "topic", "asset", "cells",
34
+ "service_announce", "content_announce", "content_manifest", "geo_anchor",
35
+ "service_uri", "content_uri",
36
+ ]
37
+
38
+ __version__ = "0.2.0"
39
+ #: The SpatialDDS-spec commit whose Appendix N and generated artifacts this
40
+ #: package implements. Package versions and specification versions move
41
+ #: independently; this is the one that says what the wire looks like.
42
+ SPEC_COMMIT = "9c3e1f2"
43
+ APPENDIX_N = f"Appendix N as of spec {SPEC_COMMIT}"
44
+
45
+ # Everything else lives in `client` and is fetched on first access (PEP 562).
46
+ # This module stays free of third-party imports on purpose: `import
47
+ # spatialdds.server` executes it, and an eager `import requests` here would
48
+ # hand the server the client's dependencies for nothing. The two halves share
49
+ # the vendored `artifacts/` as data and no code at all.
50
+ # `gates/import_graph.py` holds both directions, each shown failable.
51
+
52
+
53
+ #: The package's own modules. Named here because `from spatialdds import
54
+ #: server` (or `serve`, or `canonical`) asks this module for the attribute
55
+ #: before the import machinery tries the submodule -- and answering that
56
+ #: question by importing the client would hand the server `requests` through
57
+ #: the back door, which is exactly the coupling this file exists to avoid.
58
+ _SUBMODULES = frozenset({"client", "canonical", "errors", "records",
59
+ "serve", "server"})
60
+
61
+
62
+ def __getattr__(name: str) -> Any:
63
+ if name.startswith("__"):
64
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
65
+ if name in _SUBMODULES:
66
+ return importlib.import_module("." + name, __name__)
67
+ # `import_module` and not `from . import client`: the latter asks this
68
+ # module for the attribute first, which calls straight back into here.
69
+ client = importlib.import_module(".client", __name__)
70
+ if name == "client":
71
+ return client
72
+ try:
73
+ return getattr(client, name)
74
+ except AttributeError:
75
+ pass
76
+ # The builders, re-exported. Second rather than first because the API is
77
+ # what people reach for; `spatialdds.records` is always there under its
78
+ # own name for anyone who prefers it spelled out.
79
+ recs = importlib.import_module(".records", __name__)
80
+ try:
81
+ return getattr(recs, name)
82
+ except AttributeError:
83
+ raise AttributeError(
84
+ f"module {__name__!r} has no attribute {name!r}") from None
85
+
86
+
87
+ def __dir__() -> list:
88
+ return sorted(set(globals()) | set(__all__))