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.
- reticulum_telemetry_hub/api/__init__.py +23 -0
- reticulum_telemetry_hub/api/models.py +323 -0
- reticulum_telemetry_hub/api/service.py +836 -0
- reticulum_telemetry_hub/api/storage.py +528 -0
- reticulum_telemetry_hub/api/storage_base.py +156 -0
- reticulum_telemetry_hub/api/storage_models.py +118 -0
- reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
- reticulum_telemetry_hub/atak_cot/base.py +277 -0
- reticulum_telemetry_hub/atak_cot/chat.py +506 -0
- reticulum_telemetry_hub/atak_cot/detail.py +235 -0
- reticulum_telemetry_hub/atak_cot/event.py +181 -0
- reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
- reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
- reticulum_telemetry_hub/config/__init__.py +25 -0
- reticulum_telemetry_hub/config/constants.py +7 -0
- reticulum_telemetry_hub/config/manager.py +515 -0
- reticulum_telemetry_hub/config/models.py +215 -0
- reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
- reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
- reticulum_telemetry_hub/internal_api/__init__.py +21 -0
- reticulum_telemetry_hub/internal_api/bus.py +344 -0
- reticulum_telemetry_hub/internal_api/core.py +690 -0
- reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
- reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
- reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
- reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
- reticulum_telemetry_hub/internal_api/versioning.py +63 -0
- reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
- reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
- reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
- reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
- reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
- reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
- reticulum_telemetry_hub/northbound/__init__.py +5 -0
- reticulum_telemetry_hub/northbound/app.py +195 -0
- reticulum_telemetry_hub/northbound/auth.py +119 -0
- reticulum_telemetry_hub/northbound/gateway.py +310 -0
- reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
- reticulum_telemetry_hub/northbound/models.py +213 -0
- reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
- reticulum_telemetry_hub/northbound/routes_files.py +119 -0
- reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
- reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
- reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
- reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
- reticulum_telemetry_hub/northbound/serializers.py +72 -0
- reticulum_telemetry_hub/northbound/services.py +373 -0
- reticulum_telemetry_hub/northbound/websocket.py +855 -0
- reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
- reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
- reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
- reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
- reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
- reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
- reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
- reticulum_telemetry_hub/reticulum_server/services.py +422 -0
- reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
- reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
- {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
- reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
- lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
- lxmf_telemetry/model/persistance/__init__.py +0 -3
- lxmf_telemetry/model/persistance/sensors/location.py +0 -69
- lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
- lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
- lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
- lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
- lxmf_telemetry/telemetry_controller.py +0 -124
- reticulum_server/main.py +0 -182
- reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
- reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
- {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
- {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
- {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()
|