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,344 @@
1
+ """Transport-agnostic internal API buses with an in-process async queue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import copy
7
+ import inspect
8
+ import logging
9
+ from abc import ABC
10
+ from abc import abstractmethod
11
+ from dataclasses import dataclass
12
+ from typing import Awaitable
13
+ from typing import Callable
14
+ from typing import Generic
15
+ from typing import Optional
16
+ from typing import TypeVar
17
+ from typing import Union
18
+
19
+ from reticulum_telemetry_hub.internal_api.v1.schemas import CommandEnvelope
20
+ from reticulum_telemetry_hub.internal_api.v1.schemas import CommandResult
21
+ from reticulum_telemetry_hub.internal_api.v1.schemas import EventEnvelope
22
+ from reticulum_telemetry_hub.internal_api.v1.schemas import QueryEnvelope
23
+ from reticulum_telemetry_hub.internal_api.v1.schemas import QueryResult
24
+
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+ TEnvelope = TypeVar("TEnvelope")
29
+ TResult = TypeVar("TResult")
30
+
31
+ CommandHandler = Callable[
32
+ [CommandEnvelope],
33
+ Union[CommandResult, Awaitable[CommandResult]],
34
+ ]
35
+ QueryHandler = Callable[
36
+ [QueryEnvelope],
37
+ Union[QueryResult, Awaitable[QueryResult]],
38
+ ]
39
+ EventHandler = Callable[
40
+ [EventEnvelope],
41
+ Union[None, Awaitable[None]],
42
+ ]
43
+
44
+
45
+ @dataclass
46
+ class _WorkItem(Generic[TEnvelope, TResult]):
47
+ """Envelope plus awaiting future for queue dispatch."""
48
+
49
+ envelope: TEnvelope
50
+ future: asyncio.Future[TResult]
51
+
52
+
53
+ async def _maybe_await(result: Union[TResult, Awaitable[TResult]]) -> TResult:
54
+ """Await coroutine results while leaving sync values untouched."""
55
+
56
+ if inspect.isawaitable(result):
57
+ return await result
58
+ return result
59
+
60
+
61
+ def _copy_envelope(envelope: TEnvelope) -> TEnvelope:
62
+ """Return a defensive copy of the envelope to avoid shared state."""
63
+
64
+ copy_method = getattr(envelope, "model_copy", None)
65
+ if callable(copy_method):
66
+ return copy_method(deep=True)
67
+ return copy.deepcopy(envelope)
68
+
69
+
70
+ class CommandBus(ABC):
71
+ """Abstract command bus for synchronous command execution."""
72
+
73
+ @abstractmethod
74
+ def register_handler(self, handler: CommandHandler) -> None:
75
+ """Register a command handler."""
76
+
77
+ @abstractmethod
78
+ async def start(self) -> None:
79
+ """Start background processing."""
80
+
81
+ @abstractmethod
82
+ async def stop(self) -> None:
83
+ """Stop background processing."""
84
+
85
+ @abstractmethod
86
+ async def send(self, command: CommandEnvelope) -> CommandResult:
87
+ """Dispatch a command and await its result."""
88
+
89
+
90
+ class QueryBus(ABC):
91
+ """Abstract query bus for synchronous query execution."""
92
+
93
+ @abstractmethod
94
+ def register_handler(self, handler: QueryHandler) -> None:
95
+ """Register a query handler."""
96
+
97
+ @abstractmethod
98
+ async def start(self) -> None:
99
+ """Start background processing."""
100
+
101
+ @abstractmethod
102
+ async def stop(self) -> None:
103
+ """Stop background processing."""
104
+
105
+ @abstractmethod
106
+ async def execute(self, query: QueryEnvelope) -> QueryResult:
107
+ """Dispatch a query and await its result."""
108
+
109
+
110
+ class EventBus(ABC):
111
+ """Abstract event bus for asynchronous event publication."""
112
+
113
+ @abstractmethod
114
+ def subscribe(self, handler: EventHandler) -> Callable[[], None]:
115
+ """Subscribe to events and return an unsubscribe callback."""
116
+
117
+ @abstractmethod
118
+ async def start(self) -> None:
119
+ """Start background processing."""
120
+
121
+ @abstractmethod
122
+ async def stop(self) -> None:
123
+ """Stop background processing."""
124
+
125
+ @abstractmethod
126
+ async def publish(self, event: EventEnvelope) -> None:
127
+ """Publish an event for asynchronous delivery."""
128
+
129
+
130
+ class _InProcessHandlerBus(Generic[TEnvelope, TResult]):
131
+ """Shared in-process queue handling for command/query buses."""
132
+
133
+ def __init__(self, max_queue_size: int) -> None:
134
+ """Initialize the in-process bus."""
135
+
136
+ self._max_queue_size = max(max_queue_size, 1)
137
+ self._queue: Optional[asyncio.Queue[Union[_WorkItem[TEnvelope, TResult], object]]] = (
138
+ None
139
+ )
140
+ self._handler: Optional[
141
+ Callable[[TEnvelope], Union[TResult, Awaitable[TResult]]]
142
+ ] = None
143
+ self._worker: Optional[asyncio.Task[None]] = None
144
+ self._running = False
145
+ self._stop_sentinel = object()
146
+
147
+ def register_handler(
148
+ self, handler: Callable[[TEnvelope], Union[TResult, Awaitable[TResult]]]
149
+ ) -> None:
150
+ """Register the handler for incoming messages."""
151
+
152
+ self._handler = handler
153
+
154
+ async def start(self) -> None:
155
+ """Start the queue worker."""
156
+
157
+ if self._running:
158
+ return
159
+ self._queue = asyncio.Queue(maxsize=self._max_queue_size)
160
+ self._running = True
161
+ self._worker = asyncio.create_task(self._worker_loop())
162
+
163
+ async def stop(self) -> None:
164
+ """Stop the queue worker after draining items."""
165
+
166
+ if not self._running:
167
+ return
168
+ self._running = False
169
+ if self._queue is not None:
170
+ await self._queue.put(self._stop_sentinel)
171
+ if self._worker is not None:
172
+ await self._worker
173
+ self._worker = None
174
+
175
+ async def dispatch(self, envelope: TEnvelope) -> TResult:
176
+ """Dispatch an envelope and await the handler result."""
177
+
178
+ if not self._running or self._queue is None:
179
+ raise RuntimeError("Bus is not running")
180
+ if self._handler is None:
181
+ raise RuntimeError("Bus handler is not registered")
182
+
183
+ loop = asyncio.get_running_loop()
184
+ future: asyncio.Future[TResult] = loop.create_future()
185
+ await self._queue.put(_WorkItem(envelope=_copy_envelope(envelope), future=future))
186
+ return await future
187
+
188
+ async def _worker_loop(self) -> None:
189
+ """Process queued work items sequentially."""
190
+
191
+ if self._queue is None:
192
+ return
193
+ while True:
194
+ item = await self._queue.get()
195
+ if item is self._stop_sentinel:
196
+ self._queue.task_done()
197
+ break
198
+
199
+ try:
200
+ if self._handler is None:
201
+ raise RuntimeError("Bus handler is not registered")
202
+ result = await _maybe_await(self._handler(item.envelope))
203
+ if not item.future.cancelled():
204
+ item.future.set_result(result)
205
+ except Exception as exc:
206
+ if not item.future.cancelled():
207
+ item.future.set_exception(exc)
208
+ finally:
209
+ self._queue.task_done()
210
+
211
+
212
+ class InProcessCommandBus(CommandBus):
213
+ """In-process command bus using an asyncio queue."""
214
+
215
+ def __init__(self, *, max_queue_size: int = 64) -> None:
216
+ """Initialize the command bus."""
217
+
218
+ self._bus = _InProcessHandlerBus[CommandEnvelope, CommandResult](max_queue_size)
219
+
220
+ def register_handler(self, handler: CommandHandler) -> None:
221
+ """Register a command handler."""
222
+
223
+ self._bus.register_handler(handler)
224
+
225
+ async def start(self) -> None:
226
+ """Start background processing."""
227
+
228
+ await self._bus.start()
229
+
230
+ async def stop(self) -> None:
231
+ """Stop background processing."""
232
+
233
+ await self._bus.stop()
234
+
235
+ async def send(self, command: CommandEnvelope) -> CommandResult:
236
+ """Dispatch a command and await its result."""
237
+
238
+ return await self._bus.dispatch(command)
239
+
240
+
241
+ class InProcessQueryBus(QueryBus):
242
+ """In-process query bus using an asyncio queue."""
243
+
244
+ def __init__(self, *, max_queue_size: int = 64) -> None:
245
+ """Initialize the query bus."""
246
+
247
+ self._bus = _InProcessHandlerBus[QueryEnvelope, QueryResult](max_queue_size)
248
+
249
+ def register_handler(self, handler: QueryHandler) -> None:
250
+ """Register a query handler."""
251
+
252
+ self._bus.register_handler(handler)
253
+
254
+ async def start(self) -> None:
255
+ """Start background processing."""
256
+
257
+ await self._bus.start()
258
+
259
+ async def stop(self) -> None:
260
+ """Stop background processing."""
261
+
262
+ await self._bus.stop()
263
+
264
+ async def execute(self, query: QueryEnvelope) -> QueryResult:
265
+ """Dispatch a query and await its result."""
266
+
267
+ return await self._bus.dispatch(query)
268
+
269
+
270
+ class InProcessEventBus(EventBus):
271
+ """In-process event bus using an asyncio queue."""
272
+
273
+ def __init__(self, *, max_queue_size: int = 64) -> None:
274
+ """Initialize the event bus."""
275
+
276
+ self._max_queue_size = max(max_queue_size, 1)
277
+ self._queue: Optional[asyncio.Queue[Union[EventEnvelope, object]]] = None
278
+ self._worker: Optional[asyncio.Task[None]] = None
279
+ self._running = False
280
+ self._stop_sentinel = object()
281
+ self._subscribers: list[EventHandler] = []
282
+
283
+ def subscribe(self, handler: EventHandler) -> Callable[[], None]:
284
+ """Subscribe to events and return an unsubscribe callback."""
285
+
286
+ self._subscribers.append(handler)
287
+
288
+ def _unsubscribe() -> None:
289
+ if handler in self._subscribers:
290
+ self._subscribers.remove(handler)
291
+
292
+ return _unsubscribe
293
+
294
+ async def start(self) -> None:
295
+ """Start background processing."""
296
+
297
+ if self._running:
298
+ return
299
+ self._queue = asyncio.Queue(maxsize=self._max_queue_size)
300
+ self._running = True
301
+ self._worker = asyncio.create_task(self._worker_loop())
302
+
303
+ async def stop(self) -> None:
304
+ """Stop background processing."""
305
+
306
+ if not self._running:
307
+ return
308
+ self._running = False
309
+ if self._queue is not None:
310
+ await self._queue.put(self._stop_sentinel)
311
+ if self._worker is not None:
312
+ await self._worker
313
+ self._worker = None
314
+
315
+ async def publish(self, event: EventEnvelope) -> None:
316
+ """Publish an event for asynchronous delivery."""
317
+
318
+ if not self._running or self._queue is None:
319
+ raise RuntimeError("Event bus is not running")
320
+ await self._queue.put(_copy_envelope(event))
321
+
322
+ async def _worker_loop(self) -> None:
323
+ """Dispatch events to subscribers sequentially."""
324
+
325
+ if self._queue is None:
326
+ return
327
+ while True:
328
+ item = await self._queue.get()
329
+ if item is self._stop_sentinel:
330
+ self._queue.task_done()
331
+ break
332
+
333
+ for handler in list(self._subscribers):
334
+ try:
335
+ await _maybe_await(handler(_copy_envelope(item)))
336
+ except Exception:
337
+ _LOGGER.exception(
338
+ "Event handler failed",
339
+ extra={
340
+ "event_id": str(item.event_id),
341
+ "event_type": item.event_type.value,
342
+ },
343
+ )
344
+ self._queue.task_done()