ReticulumTelemetryHub 0.1.0__py3-none-any.whl → 0.143.0__py3-none-any.whl

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 (108) hide show
  1. reticulum_telemetry_hub/api/__init__.py +23 -0
  2. reticulum_telemetry_hub/api/models.py +323 -0
  3. reticulum_telemetry_hub/api/service.py +836 -0
  4. reticulum_telemetry_hub/api/storage.py +528 -0
  5. reticulum_telemetry_hub/api/storage_base.py +156 -0
  6. reticulum_telemetry_hub/api/storage_models.py +118 -0
  7. reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
  8. reticulum_telemetry_hub/atak_cot/base.py +277 -0
  9. reticulum_telemetry_hub/atak_cot/chat.py +506 -0
  10. reticulum_telemetry_hub/atak_cot/detail.py +235 -0
  11. reticulum_telemetry_hub/atak_cot/event.py +181 -0
  12. reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
  13. reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
  14. reticulum_telemetry_hub/config/__init__.py +25 -0
  15. reticulum_telemetry_hub/config/constants.py +7 -0
  16. reticulum_telemetry_hub/config/manager.py +515 -0
  17. reticulum_telemetry_hub/config/models.py +215 -0
  18. reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
  19. reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
  20. reticulum_telemetry_hub/internal_api/__init__.py +21 -0
  21. reticulum_telemetry_hub/internal_api/bus.py +344 -0
  22. reticulum_telemetry_hub/internal_api/core.py +690 -0
  23. reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
  24. reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
  25. reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
  26. reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
  27. reticulum_telemetry_hub/internal_api/versioning.py +63 -0
  28. reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
  29. reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
  30. reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
  31. reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
  32. reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
  33. reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
  34. reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
  35. reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
  36. reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
  37. reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
  38. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
  39. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
  40. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
  41. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
  42. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
  43. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
  44. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
  45. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
  46. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
  47. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
  48. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
  49. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
  50. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
  51. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
  52. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
  53. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
  54. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
  55. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
  56. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
  57. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
  58. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
  59. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
  60. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
  61. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
  62. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
  63. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
  64. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
  65. reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
  66. reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
  67. reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
  68. reticulum_telemetry_hub/northbound/__init__.py +5 -0
  69. reticulum_telemetry_hub/northbound/app.py +195 -0
  70. reticulum_telemetry_hub/northbound/auth.py +119 -0
  71. reticulum_telemetry_hub/northbound/gateway.py +310 -0
  72. reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
  73. reticulum_telemetry_hub/northbound/models.py +213 -0
  74. reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
  75. reticulum_telemetry_hub/northbound/routes_files.py +119 -0
  76. reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
  77. reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
  78. reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
  79. reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
  80. reticulum_telemetry_hub/northbound/serializers.py +72 -0
  81. reticulum_telemetry_hub/northbound/services.py +373 -0
  82. reticulum_telemetry_hub/northbound/websocket.py +855 -0
  83. reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
  84. reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
  85. reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
  86. reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
  87. reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
  88. reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
  89. reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
  90. reticulum_telemetry_hub/reticulum_server/services.py +422 -0
  91. reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
  92. reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
  93. {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
  94. reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
  95. lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
  96. lxmf_telemetry/model/persistance/__init__.py +0 -3
  97. lxmf_telemetry/model/persistance/sensors/location.py +0 -69
  98. lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
  99. lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
  100. lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
  101. lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
  102. lxmf_telemetry/telemetry_controller.py +0 -124
  103. reticulum_server/main.py +0 -182
  104. reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
  105. reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
  106. {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
  107. {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
  108. {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
@@ -0,0 +1,119 @@
1
+ """File and image routes for the northbound API."""
2
+ # pylint: disable=import-error
3
+
4
+ from __future__ import annotations
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi import HTTPException
8
+ from fastapi import status
9
+ from fastapi.responses import FileResponse
10
+
11
+ from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
12
+
13
+ from .services import NorthboundServices
14
+
15
+
16
+ def register_file_routes(
17
+ app: FastAPI,
18
+ *,
19
+ services: NorthboundServices,
20
+ api: ReticulumTelemetryHubAPI,
21
+ ) -> None:
22
+ """Register file and image routes on the FastAPI app.
23
+
24
+ Args:
25
+ app (FastAPI): FastAPI application instance.
26
+ services (NorthboundServices): Aggregated services.
27
+ api (ReticulumTelemetryHubAPI): API service instance.
28
+
29
+ Returns:
30
+ None: Routes are registered on the application.
31
+ """
32
+
33
+ @app.get("/File")
34
+ def list_files() -> list[dict]:
35
+ """List stored files.
36
+
37
+ Returns:
38
+ list[dict]: File attachment entries.
39
+ """
40
+
41
+ return [attachment.to_dict() for attachment in services.list_files()]
42
+
43
+ @app.get("/File/{file_id}")
44
+ def retrieve_file(file_id: int) -> dict:
45
+ """Retrieve file metadata by ID.
46
+
47
+ Args:
48
+ file_id (int): File record identifier.
49
+
50
+ Returns:
51
+ dict: File attachment payload.
52
+ """
53
+
54
+ try:
55
+ attachment = api.retrieve_file(file_id)
56
+ except KeyError as exc:
57
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
58
+ return attachment.to_dict()
59
+
60
+ @app.get("/File/{file_id}/raw")
61
+ def retrieve_file_raw(file_id: int) -> FileResponse:
62
+ """Return raw file bytes by ID.
63
+
64
+ Args:
65
+ file_id (int): File record identifier.
66
+
67
+ Returns:
68
+ FileResponse: File response payload.
69
+ """
70
+
71
+ try:
72
+ attachment = api.retrieve_file(file_id)
73
+ except KeyError as exc:
74
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
75
+ return FileResponse(path=attachment.path, media_type=attachment.media_type)
76
+
77
+ @app.get("/Image")
78
+ def list_images() -> list[dict]:
79
+ """List stored images.
80
+
81
+ Returns:
82
+ list[dict]: Image attachment entries.
83
+ """
84
+
85
+ return [attachment.to_dict() for attachment in services.list_images()]
86
+
87
+ @app.get("/Image/{file_id}")
88
+ def retrieve_image(file_id: int) -> dict:
89
+ """Retrieve image metadata by ID.
90
+
91
+ Args:
92
+ file_id (int): Image record identifier.
93
+
94
+ Returns:
95
+ dict: Image attachment payload.
96
+ """
97
+
98
+ try:
99
+ attachment = api.retrieve_image(file_id)
100
+ except KeyError as exc:
101
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
102
+ return attachment.to_dict()
103
+
104
+ @app.get("/Image/{file_id}/raw")
105
+ def retrieve_image_raw(file_id: int) -> FileResponse:
106
+ """Return raw image bytes by ID.
107
+
108
+ Args:
109
+ file_id (int): Image record identifier.
110
+
111
+ Returns:
112
+ FileResponse: Image response payload.
113
+ """
114
+
115
+ try:
116
+ attachment = api.retrieve_image(file_id)
117
+ except KeyError as exc:
118
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
119
+ return FileResponse(path=attachment.path, media_type=attachment.media_type)
@@ -0,0 +1,345 @@
1
+ """Core REST routes for the northbound API."""
2
+ # pylint: disable=import-error
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+ from typing import Callable
8
+ from typing import Optional
9
+
10
+ from fastapi import Body
11
+ from fastapi import Depends
12
+ from fastapi import FastAPI
13
+ from fastapi import HTTPException
14
+ from fastapi import Query
15
+ from fastapi import Response
16
+ from fastapi import status
17
+
18
+ from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
19
+ from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
20
+ TelemetryController,
21
+ )
22
+
23
+ from .models import ConfigRollbackPayload
24
+ from .models import MessagePayload
25
+ from .services import NorthboundServices
26
+
27
+
28
+ def register_core_routes(
29
+ app: FastAPI,
30
+ *,
31
+ services: NorthboundServices,
32
+ api: ReticulumTelemetryHubAPI,
33
+ telemetry_controller: TelemetryController,
34
+ require_protected: Callable[[], None],
35
+ resolve_openapi_spec: Callable[[], Optional[Path]],
36
+ ) -> None:
37
+ """Register core REST routes on the FastAPI app.
38
+
39
+ Args:
40
+ app (FastAPI): FastAPI application instance.
41
+ services (NorthboundServices): Aggregated services.
42
+ api (ReticulumTelemetryHubAPI): API service instance.
43
+ telemetry_controller (TelemetryController): Telemetry controller instance.
44
+ require_protected (Callable[[], None]): Dependency for protected routes.
45
+ resolve_openapi_spec (Callable[[], Optional[Path]]): OpenAPI spec resolver.
46
+
47
+ Returns:
48
+ None: Routes are registered on the application.
49
+ """
50
+
51
+ @app.get("/openapi.yaml", include_in_schema=False)
52
+ def openapi_yaml() -> Response:
53
+ """Return the OpenAPI YAML file if available.
54
+
55
+ Returns:
56
+ Response: YAML content response.
57
+ """
58
+
59
+ spec_path = resolve_openapi_spec()
60
+ if not spec_path:
61
+ raise HTTPException(
62
+ status_code=status.HTTP_404_NOT_FOUND,
63
+ detail="OpenAPI spec not found",
64
+ )
65
+ return Response(spec_path.read_text(encoding="utf-8"), media_type="application/yaml")
66
+
67
+ @app.get("/Help")
68
+ def get_help_text() -> Response:
69
+ """Return the list of supported commands.
70
+
71
+ Returns:
72
+ Response: Plain text command list.
73
+ """
74
+
75
+ return Response(services.help_text(), media_type="text/plain")
76
+
77
+ @app.get("/Examples")
78
+ def get_examples_text() -> Response:
79
+ """Return command payload examples.
80
+
81
+ Returns:
82
+ Response: Plain text examples.
83
+ """
84
+
85
+ return Response(services.examples_text(), media_type="text/plain")
86
+
87
+ @app.get("/Status", dependencies=[Depends(require_protected)])
88
+ def get_status() -> dict:
89
+ """Return dashboard status metrics.
90
+
91
+ Returns:
92
+ dict: Status payload.
93
+ """
94
+
95
+ return services.status_snapshot()
96
+
97
+ @app.get("/Events", dependencies=[Depends(require_protected)])
98
+ def get_events() -> list[dict]:
99
+ """Return recent events.
100
+
101
+ Returns:
102
+ list[dict]: Event entries.
103
+ """
104
+
105
+ return services.list_events()
106
+
107
+ @app.get("/Telemetry")
108
+ def get_telemetry(
109
+ since: int = Query(alias="since"),
110
+ topic_id: Optional[str] = Query(default=None, alias="topic_id"),
111
+ ) -> dict:
112
+ """Return telemetry entries since a timestamp.
113
+
114
+ Args:
115
+ since (int): Unix timestamp (seconds) for the earliest entries.
116
+ topic_id (Optional[str]): Optional topic filter.
117
+
118
+ Returns:
119
+ dict: Telemetry response payload.
120
+ """
121
+
122
+ try:
123
+ entries = services.telemetry_entries(since=since, topic_id=topic_id)
124
+ except KeyError as exc:
125
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
126
+ except ValueError as exc:
127
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
128
+ return {"entries": entries}
129
+
130
+ @app.get("/Config", dependencies=[Depends(require_protected)])
131
+ def get_config() -> Response:
132
+ """Return the config.ini payload.
133
+
134
+ Returns:
135
+ Response: Plain text configuration payload.
136
+ """
137
+
138
+ return Response(api.get_config_text(), media_type="text/plain")
139
+
140
+ @app.put("/Config", dependencies=[Depends(require_protected)])
141
+ def apply_config(config_text: str = Body(media_type="text/plain")) -> dict:
142
+ """Apply a new config.ini payload.
143
+
144
+ Args:
145
+ config_text (str): Raw config.ini payload.
146
+
147
+ Returns:
148
+ dict: Apply result payload.
149
+ """
150
+
151
+ try:
152
+ result = api.apply_config_text(config_text)
153
+ except ValueError as exc:
154
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
155
+ services.record_event("config_applied", "Configuration applied")
156
+ return result
157
+
158
+ @app.post("/Config/Validate", dependencies=[Depends(require_protected)])
159
+ def validate_config(config_text: str = Body(media_type="text/plain")) -> dict:
160
+ """Validate a config.ini payload.
161
+
162
+ Args:
163
+ config_text (str): Raw config.ini payload.
164
+
165
+ Returns:
166
+ dict: Validation result payload.
167
+ """
168
+
169
+ return api.validate_config_text(config_text)
170
+
171
+ @app.post("/Config/Rollback", dependencies=[Depends(require_protected)])
172
+ def rollback_config(payload: Optional[ConfigRollbackPayload] = Body(default=None)) -> dict:
173
+ """Rollback config.ini using a backup path.
174
+
175
+ Args:
176
+ payload (Optional[ConfigRollbackPayload]): Rollback payload.
177
+
178
+ Returns:
179
+ dict: Rollback result payload.
180
+ """
181
+
182
+ backup_path = payload.backup_path if payload else None
183
+ result = api.rollback_config_text(backup_path=backup_path)
184
+ services.record_event("config_rolled_back", "Configuration rolled back")
185
+ return result
186
+
187
+ @app.post("/Command/FlushTelemetry", dependencies=[Depends(require_protected)])
188
+ def flush_telemetry() -> dict:
189
+ """Flush stored telemetry entries.
190
+
191
+ Returns:
192
+ dict: Flush result payload.
193
+ """
194
+
195
+ deleted = telemetry_controller.clear_telemetry()
196
+ services.record_event("telemetry_flushed", f"Telemetry flushed ({deleted} rows)")
197
+ return {"deleted": deleted}
198
+
199
+ @app.post("/Command/ReloadConfig", dependencies=[Depends(require_protected)])
200
+ def reload_config() -> dict:
201
+ """Reload config.ini from disk.
202
+
203
+ Returns:
204
+ dict: Reloaded configuration payload.
205
+ """
206
+
207
+ info = services.reload_config()
208
+ services.record_event("config_reloaded", "Configuration reloaded")
209
+ return info.to_dict()
210
+
211
+ @app.post("/Message", dependencies=[Depends(require_protected)])
212
+ def send_message(payload: MessagePayload) -> dict:
213
+ """Send a message into the Reticulum Telemetry Hub."""
214
+
215
+ try:
216
+ services.send_message(
217
+ payload.content,
218
+ topic_id=payload.topic_id,
219
+ destination=payload.destination,
220
+ )
221
+ except RuntimeError as exc:
222
+ raise HTTPException(
223
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
224
+ detail=str(exc),
225
+ ) from exc
226
+ services.record_event(
227
+ "message_sent",
228
+ "Northbound message dispatched",
229
+ metadata={
230
+ "topic_id": payload.topic_id,
231
+ "destination": payload.destination,
232
+ },
233
+ )
234
+ return {"sent": True}
235
+
236
+ @app.get("/Command/DumpRouting", dependencies=[Depends(require_protected)])
237
+ def dump_routing() -> dict:
238
+ """Return connected destination hashes.
239
+
240
+ Returns:
241
+ dict: Routing summary payload.
242
+ """
243
+
244
+ return services.dump_routing()
245
+
246
+ @app.get("/Identities", dependencies=[Depends(require_protected)])
247
+ def list_identities() -> list[dict]:
248
+ """Return identity moderation status entries.
249
+
250
+ Returns:
251
+ list[dict]: Identity status entries.
252
+ """
253
+
254
+ return [status_entry.to_dict() for status_entry in services.list_identity_statuses()]
255
+
256
+ @app.post("/Client/{identity}/Ban", dependencies=[Depends(require_protected)])
257
+ def ban_identity(identity: str) -> dict:
258
+ """Ban an identity.
259
+
260
+ Args:
261
+ identity (str): Identity to ban.
262
+
263
+ Returns:
264
+ dict: Updated identity status.
265
+ """
266
+
267
+ status_entry = api.ban_identity(identity)
268
+ services.record_event("identity_banned", f"Identity banned: {identity}")
269
+ return status_entry.to_dict()
270
+
271
+ @app.post("/Client/{identity}/Unban", dependencies=[Depends(require_protected)])
272
+ def unban_identity(identity: str) -> dict:
273
+ """Unban an identity.
274
+
275
+ Args:
276
+ identity (str): Identity to unban.
277
+
278
+ Returns:
279
+ dict: Updated identity status.
280
+ """
281
+
282
+ status_entry = api.unban_identity(identity)
283
+ services.record_event("identity_unbanned", f"Identity unbanned: {identity}")
284
+ return status_entry.to_dict()
285
+
286
+ @app.post("/Client/{identity}/Blackhole", dependencies=[Depends(require_protected)])
287
+ def blackhole_identity(identity: str) -> dict:
288
+ """Blackhole an identity.
289
+
290
+ Args:
291
+ identity (str): Identity to blackhole.
292
+
293
+ Returns:
294
+ dict: Updated identity status.
295
+ """
296
+
297
+ status_entry = api.blackhole_identity(identity)
298
+ services.record_event("identity_blackholed", f"Identity blackholed: {identity}")
299
+ return status_entry.to_dict()
300
+
301
+ @app.post("/RTH")
302
+ def rth_join(identity: str = Query(alias="identity")) -> bool:
303
+ """Join the Reticulum Telemetry Hub.
304
+
305
+ Args:
306
+ identity (str): Identity to register.
307
+
308
+ Returns:
309
+ bool: ``True`` when the identity is recorded.
310
+ """
311
+
312
+ return api.join(identity)
313
+
314
+ @app.put("/RTH")
315
+ def rth_leave(identity: str = Query(alias="identity")) -> bool:
316
+ """Leave the Reticulum Telemetry Hub.
317
+
318
+ Args:
319
+ identity (str): Identity to remove.
320
+
321
+ Returns:
322
+ bool: ``True`` when the identity is removed.
323
+ """
324
+
325
+ return api.leave(identity)
326
+
327
+ @app.get("/Client", dependencies=[Depends(require_protected)])
328
+ def list_clients() -> list[dict]:
329
+ """List clients.
330
+
331
+ Returns:
332
+ list[dict]: Client entries.
333
+ """
334
+
335
+ return [client.to_dict() for client in services.list_clients()]
336
+
337
+ @app.get("/api/v1/app/info")
338
+ def app_info() -> dict:
339
+ """Return application metadata.
340
+
341
+ Returns:
342
+ dict: Application info payload.
343
+ """
344
+
345
+ return services.app_info().to_dict()
@@ -0,0 +1,150 @@
1
+ """Subscriber routes for the northbound API."""
2
+ # pylint: disable=import-error
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Callable
7
+
8
+ from fastapi import Depends
9
+ from fastapi import FastAPI
10
+ from fastapi import HTTPException
11
+ from fastapi import Query
12
+ from fastapi import status
13
+
14
+ from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
15
+
16
+ from .models import SubscriberPayload
17
+ from .serializers import build_subscriber
18
+ from .serializers import serialize_subscriber
19
+ from .services import NorthboundServices
20
+
21
+
22
+ def register_subscriber_routes(
23
+ app: FastAPI,
24
+ *,
25
+ services: NorthboundServices,
26
+ api: ReticulumTelemetryHubAPI,
27
+ require_protected: Callable[[], None],
28
+ ) -> None:
29
+ """Register subscriber routes on the FastAPI app.
30
+
31
+ Args:
32
+ app (FastAPI): FastAPI application instance.
33
+ services (NorthboundServices): Aggregated services.
34
+ api (ReticulumTelemetryHubAPI): API service instance.
35
+ require_protected (Callable[[], None]): Dependency for protected routes.
36
+
37
+ Returns:
38
+ None: Routes are registered on the application.
39
+ """
40
+
41
+ @app.get("/Subscriber/{subscriber_id}", dependencies=[Depends(require_protected)])
42
+ def retrieve_subscriber(subscriber_id: str) -> dict:
43
+ """Retrieve a subscriber by ID.
44
+
45
+ Args:
46
+ subscriber_id (str): Subscriber identifier.
47
+
48
+ Returns:
49
+ dict: Subscriber payload.
50
+ """
51
+
52
+ try:
53
+ subscriber = api.retrieve_subscriber(subscriber_id)
54
+ except KeyError as exc:
55
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
56
+ return serialize_subscriber(subscriber)
57
+
58
+ @app.post("/Subscriber/Add", dependencies=[Depends(require_protected)])
59
+ def add_subscriber(payload: SubscriberPayload) -> dict:
60
+ """Add a subscriber mapping.
61
+
62
+ Args:
63
+ payload (SubscriberPayload): Subscriber payload.
64
+
65
+ Returns:
66
+ dict: Subscriber payload.
67
+ """
68
+
69
+ subscriber = build_subscriber(payload)
70
+ try:
71
+ created = api.add_subscriber(subscriber)
72
+ except ValueError as exc:
73
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
74
+ services.record_event("subscriber_added", f"Subscriber added: {created.subscriber_id}")
75
+ return serialize_subscriber(created)
76
+
77
+ @app.post("/Subscriber", dependencies=[Depends(require_protected)])
78
+ def create_subscriber(payload: SubscriberPayload) -> dict:
79
+ """Create a subscriber.
80
+
81
+ Args:
82
+ payload (SubscriberPayload): Subscriber payload.
83
+
84
+ Returns:
85
+ dict: Subscriber payload.
86
+ """
87
+
88
+ subscriber = build_subscriber(payload)
89
+ try:
90
+ created = api.create_subscriber(subscriber)
91
+ except ValueError as exc:
92
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
93
+ services.record_event("subscriber_created", f"Subscriber created: {created.subscriber_id}")
94
+ return serialize_subscriber(created)
95
+
96
+ @app.delete("/Subscriber", dependencies=[Depends(require_protected)])
97
+ def delete_subscriber(subscriber_id: str = Query(alias="id")) -> dict:
98
+ """Delete a subscriber.
99
+
100
+ Args:
101
+ subscriber_id (str): Subscriber identifier.
102
+
103
+ Returns:
104
+ dict: Deleted subscriber payload.
105
+ """
106
+
107
+ try:
108
+ subscriber = api.delete_subscriber(subscriber_id)
109
+ except KeyError as exc:
110
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
111
+ services.record_event("subscriber_deleted", f"Subscriber deleted: {subscriber.subscriber_id}")
112
+ return serialize_subscriber(subscriber)
113
+
114
+ @app.get("/Subscriber", dependencies=[Depends(require_protected)])
115
+ def list_subscribers() -> list[dict]:
116
+ """List subscribers.
117
+
118
+ Returns:
119
+ list[dict]: Subscriber entries.
120
+ """
121
+
122
+ return [serialize_subscriber(subscriber) for subscriber in services.list_subscribers()]
123
+
124
+ @app.patch("/Subscriber", dependencies=[Depends(require_protected)])
125
+ def patch_subscriber(payload: SubscriberPayload) -> dict:
126
+ """Update a subscriber.
127
+
128
+ Args:
129
+ payload (SubscriberPayload): Subscriber update payload.
130
+
131
+ Returns:
132
+ dict: Updated subscriber payload.
133
+ """
134
+
135
+ if not payload.subscriber_id:
136
+ raise HTTPException(
137
+ status_code=status.HTTP_400_BAD_REQUEST,
138
+ detail="SubscriberID is required",
139
+ )
140
+ try:
141
+ subscriber = api.patch_subscriber(
142
+ payload.subscriber_id,
143
+ **payload.model_dump(by_alias=True, exclude_unset=True),
144
+ )
145
+ except ValueError as exc:
146
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
147
+ except KeyError as exc:
148
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
149
+ services.record_event("subscriber_updated", f"Subscriber updated: {subscriber.subscriber_id}")
150
+ return serialize_subscriber(subscriber)