parallelazy 0.0.1__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.
- parallelazy/__init__.py +43 -0
- parallelazy/environment_manager.py +55 -0
- parallelazy/examples/__init__.py +0 -0
- parallelazy/examples/example_configuration.py +194 -0
- parallelazy/examples/example_count.py +84 -0
- parallelazy/examples/example_data_pipeline.py +226 -0
- parallelazy/examples/example_env_variables.py +158 -0
- parallelazy/examples/example_error_handling.py +266 -0
- parallelazy/examples/example_graceful_shutdown.py +139 -0
- parallelazy/examples/example_ml_training.py +280 -0
- parallelazy/examples/example_pubsub.py +137 -0
- parallelazy/examples/example_rpc_calls.py +182 -0
- parallelazy/examples/example_worker_pool.py +188 -0
- parallelazy/plugin.py +223 -0
- parallelazy/plugin_data.py +466 -0
- parallelazy/plugin_manager.py +617 -0
- parallelazy/plugin_rpc.py +511 -0
- parallelazy/plugin_transport.py +347 -0
- parallelazy/plugin_worker.py +700 -0
- parallelazy/tests/__init__.py +0 -0
- parallelazy/utils.py +71 -0
- parallelazy-0.0.1.dist-info/METADATA +17 -0
- parallelazy-0.0.1.dist-info/RECORD +26 -0
- parallelazy-0.0.1.dist-info/WHEEL +5 -0
- parallelazy-0.0.1.dist-info/licenses/LICENSE +339 -0
- parallelazy-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin System Message Protocol (Refactored)
|
|
3
|
+
|
|
4
|
+
Unified, lean messaging system for inter-process communication:
|
|
5
|
+
|
|
6
|
+
Core Concepts:
|
|
7
|
+
1. Request/Response: Unified for ALL operations (lifecycle, services, status)
|
|
8
|
+
2. DataExchange: Unified subscription model for pub/sub + env vars
|
|
9
|
+
3. Channel: Proper async request/response routing with background thread
|
|
10
|
+
|
|
11
|
+
Message Flows:
|
|
12
|
+
- Control: Manager sends Request("start"), Worker replies with Response
|
|
13
|
+
- Services: Manager sends Request("call_service", ...), Worker replies
|
|
14
|
+
- Pub/Sub: Manager.publish(topic, data) → direct to subscriber queues
|
|
15
|
+
- Env Vars: Manager.set_env(key, value) → broadcasts EnvUpdate to subscribers
|
|
16
|
+
|
|
17
|
+
No Commands/Responses wrappers, no double indirection. Just Request and Response.
|
|
18
|
+
"""
|
|
19
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
20
|
+
from enum import Enum
|
|
21
|
+
import logging
|
|
22
|
+
from concurrent.futures import Future
|
|
23
|
+
from torch import multiprocessing as mp
|
|
24
|
+
from dataclasses import dataclass, field, fields
|
|
25
|
+
from multiprocessing.connection import Connection
|
|
26
|
+
|
|
27
|
+
import threading
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from parallelazy.plugin.plugin_worker import PluginRPC
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
RequestCallback = Callable[["PluginRPC","Request"], Any]
|
|
40
|
+
ResponseCallback = Callable[["PluginRPC","Response"], Any]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class QueueDescriptor:
|
|
45
|
+
"""Descriptor for holding 2 connections, read/write repesenting a queue made with pipes."""
|
|
46
|
+
def __init__(self, send: Connection, recieve: Connection):
|
|
47
|
+
self.send = send
|
|
48
|
+
self.receive = recieve
|
|
49
|
+
|
|
50
|
+
def __str__(self): return f"QueueDescriptor(send_closed={self.send.closed()}, receive_closed={self.receive.closed()})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ============================================================================
|
|
55
|
+
# CHANNEL: Bidirectional duplex IPC with proper async response routing
|
|
56
|
+
# ============================================================================
|
|
57
|
+
class Queue:
|
|
58
|
+
|
|
59
|
+
def __init__(self, descriptor: QueueDescriptor = None):
|
|
60
|
+
if descriptor is None:
|
|
61
|
+
receive, send = mp.Pipe(duplex=False)
|
|
62
|
+
else:
|
|
63
|
+
receive, send = descriptor.receive, descriptor.send
|
|
64
|
+
|
|
65
|
+
self._send = QueueSend(send)
|
|
66
|
+
self._receive = QueueRecive(receive)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def sender(self):
|
|
70
|
+
return self._send
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def receiver(self):
|
|
74
|
+
return self._receive
|
|
75
|
+
|
|
76
|
+
def closed(self):
|
|
77
|
+
return self._send._conn.closed or self._receive._conn.closed
|
|
78
|
+
|
|
79
|
+
def put(self, item):
|
|
80
|
+
self._send.put(item)
|
|
81
|
+
|
|
82
|
+
def put_nowait(self, item):
|
|
83
|
+
self._send.put(item)
|
|
84
|
+
|
|
85
|
+
def get(self):
|
|
86
|
+
return self._receive.get()
|
|
87
|
+
|
|
88
|
+
def get_nowait(self):
|
|
89
|
+
return self._receive.get()
|
|
90
|
+
|
|
91
|
+
def close(self):
|
|
92
|
+
self._send.put(None) # Send a sentinel to indicate closure
|
|
93
|
+
self._send.close()
|
|
94
|
+
self._receive.close()
|
|
95
|
+
|
|
96
|
+
def descriptor(self):
|
|
97
|
+
return QueueDescriptor(self._send._conn, self._receive._conn)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class QueueSend:
|
|
101
|
+
def __init__(self, conn: Connection):
|
|
102
|
+
self._conn = conn
|
|
103
|
+
self._lock = threading.Lock()
|
|
104
|
+
|
|
105
|
+
def put(self, item):
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._conn.send(item)
|
|
108
|
+
|
|
109
|
+
def close(self):
|
|
110
|
+
self._conn.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class QueueRecive:
|
|
114
|
+
def __init__(self, conn: Connection):
|
|
115
|
+
self._conn = conn
|
|
116
|
+
|
|
117
|
+
def get(self):
|
|
118
|
+
return self._conn.recv()
|
|
119
|
+
|
|
120
|
+
def close(self):
|
|
121
|
+
self._conn.close()
|
|
122
|
+
|
|
123
|
+
class Channel:
|
|
124
|
+
"""
|
|
125
|
+
Duplex communication channel with request/response routing.
|
|
126
|
+
Pass channel.reverse() to the other process — it's picklable.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, descriptor: QueueDescriptor = None ):
|
|
130
|
+
if descriptor is None:
|
|
131
|
+
r1, w1 = mp.Pipe(duplex=False)
|
|
132
|
+
r2, w2 = mp.Pipe(duplex=False)
|
|
133
|
+
self._send = QueueSend(w1)
|
|
134
|
+
self._receive = QueueRecive(r2)
|
|
135
|
+
self._remote_send = QueueSend(w2)
|
|
136
|
+
self._remote_receive = QueueRecive(r1)
|
|
137
|
+
else:
|
|
138
|
+
self._send = QueueSend(descriptor.send)
|
|
139
|
+
self._receive = QueueRecive(descriptor.receive)
|
|
140
|
+
self._remote_send = None
|
|
141
|
+
self._remote_receive = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def descriptor(self):
|
|
145
|
+
return QueueDescriptor(self._send._conn, self._receive._conn)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def remote_descriptor(self):
|
|
149
|
+
return QueueDescriptor(self._remote_send._conn, self._remote_receive._conn)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def send(self, message) -> None:
|
|
153
|
+
self._send.put(message)
|
|
154
|
+
|
|
155
|
+
def receive(self) -> Optional[object]:
|
|
156
|
+
"""Receive a message, blocking until one arrives or the pipe closes."""
|
|
157
|
+
return self._receive.get()
|
|
158
|
+
|
|
159
|
+
def shutdown(self):
|
|
160
|
+
if self._send:
|
|
161
|
+
self._send.close()
|
|
162
|
+
if self._receive:
|
|
163
|
+
self._receive.close()
|
|
164
|
+
if self._remote_send:
|
|
165
|
+
self._remote_send.close()
|
|
166
|
+
if self._remote_receive:
|
|
167
|
+
self._remote_receive.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ============================================================================
|
|
171
|
+
# CORE MESSAGE TYPES
|
|
172
|
+
# ============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CommandType:
|
|
176
|
+
"""Standard commands for plugin control."""
|
|
177
|
+
PLUGIN_START = "plugin_start"
|
|
178
|
+
PLUGIN_STOP = "plugin_stop"
|
|
179
|
+
PLUGIN_STATUS = "plugin_status"
|
|
180
|
+
PLUGIN_STATUS_ALL = "plugin_status_all"
|
|
181
|
+
PLUGIN_ERRORS = "plugin_errors"
|
|
182
|
+
PLUGIN_CALL = "plugin_call"
|
|
183
|
+
|
|
184
|
+
ENV_SET = "env_set"
|
|
185
|
+
ENV_GET = "env_get"
|
|
186
|
+
ENV_GET_ALL = "env_get_all"
|
|
187
|
+
ENV_SUBSCRIBE = "env_subscribe"
|
|
188
|
+
ENV_UNSUBSCRIBE = "env_unsubscribe"
|
|
189
|
+
ENV_UPDATE = "env_update"
|
|
190
|
+
|
|
191
|
+
TOPIC_SUBSCRIBE = "topic_subscribe"
|
|
192
|
+
TOPIC_UNSUBSCRIBE = "topic_unsubscribe"
|
|
193
|
+
TOPIC_ANNOUNCE = "topic_announce"
|
|
194
|
+
TOPIC_LIST = "topic_list"
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def commands(cls):
|
|
198
|
+
return iter([
|
|
199
|
+
cls.PLUGIN_START,
|
|
200
|
+
cls.PLUGIN_STOP,
|
|
201
|
+
cls.PLUGIN_STATUS,
|
|
202
|
+
cls.PLUGIN_STATUS_ALL,
|
|
203
|
+
cls.PLUGIN_ERRORS,
|
|
204
|
+
cls.PLUGIN_CALL,
|
|
205
|
+
cls.ENV_SET,
|
|
206
|
+
cls.ENV_GET,
|
|
207
|
+
cls.ENV_GET_ALL,
|
|
208
|
+
cls.ENV_SUBSCRIBE,
|
|
209
|
+
cls.ENV_UNSUBSCRIBE,
|
|
210
|
+
cls.ENV_UPDATE,
|
|
211
|
+
cls.TOPIC_SUBSCRIBE,
|
|
212
|
+
cls.TOPIC_UNSUBSCRIBE,
|
|
213
|
+
cls.TOPIC_ANNOUNCE,
|
|
214
|
+
cls.TOPIC_LIST
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class Message:
|
|
220
|
+
"""Base message class for all communication."""
|
|
221
|
+
sender: str = None
|
|
222
|
+
message_id: str = None
|
|
223
|
+
|
|
224
|
+
def copy(self):
|
|
225
|
+
kwargs = {f.name: getattr(self, f.name) for f in fields(self)}
|
|
226
|
+
kwargs['message_id'] = None
|
|
227
|
+
return type(self)(**kwargs)
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class Request(Message):
|
|
231
|
+
"""
|
|
232
|
+
Unified request for all operations.ada
|
|
233
|
+
Worker replies with Response(message_id, result=...) or Response(message_id, error=...)
|
|
234
|
+
"""
|
|
235
|
+
command: CommandType = None
|
|
236
|
+
|
|
237
|
+
def copy(self):
|
|
238
|
+
"""Create a copy of the message (for forwarding/modification)."""
|
|
239
|
+
clone = type(self)(**self.__dict__)
|
|
240
|
+
clone.message_id = None # message_id are generated at request time.
|
|
241
|
+
return clone
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class Response(Message):
|
|
245
|
+
"""
|
|
246
|
+
Unified response for all request results.
|
|
247
|
+
- result: The return value of the request (if successful)
|
|
248
|
+
- error: An error message (if an error occurred)
|
|
249
|
+
"""
|
|
250
|
+
result: Any = None
|
|
251
|
+
error: Optional[str] = None
|
|
252
|
+
|
|
253
|
+
class PendingRequest():
|
|
254
|
+
def __init__(self, message_id: str, callback: Callable=None):
|
|
255
|
+
super().__init__()
|
|
256
|
+
self.future = Future()
|
|
257
|
+
self.message_id = message_id
|
|
258
|
+
if callback:
|
|
259
|
+
self.future.add_done_callback(callback)
|
|
260
|
+
|
|
261
|
+
def on_ready(self, callback: Callable):
|
|
262
|
+
self.future.add_done_callback(callback)
|
|
263
|
+
|
|
264
|
+
def set_result(self, result: Response|Any):
|
|
265
|
+
if not isinstance(result, Response):
|
|
266
|
+
result = Response(message_id=self.message_id, result=result)
|
|
267
|
+
self.future.set_result(result)
|
|
268
|
+
|
|
269
|
+
def set_error(self, error: str|Exception|Any):
|
|
270
|
+
self.future.set_result(Response(message_id=self.message_id, error=str(error)))
|
|
271
|
+
|
|
272
|
+
def wait(self, timeout=None) -> None | Response:
|
|
273
|
+
try:
|
|
274
|
+
result: Response = self.future.result(timeout=timeout)
|
|
275
|
+
return result
|
|
276
|
+
except TimeoutError:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
def __hash__(self):
|
|
280
|
+
return hash(self.message_id)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class Subscription:
|
|
284
|
+
"""
|
|
285
|
+
Represents a subscription to a topic or environment variable.
|
|
286
|
+
"""
|
|
287
|
+
def __init__(self, topic: str, callback: Callable, queue: Queue=None, thread: Optional[threading.Thread] = None):
|
|
288
|
+
self.topic = topic
|
|
289
|
+
self.callback = callback
|
|
290
|
+
self.queue = queue or Queue() # For topic subscriptions, queue to receive data
|
|
291
|
+
self.thread: Optional[threading.Thread] = thread # Thread handling this subscription (if needed)
|
|
292
|
+
|
|
293
|
+
class Subscriber:
|
|
294
|
+
"""Represents a subscriber to a topic or environment variable."""
|
|
295
|
+
def __init__(self, plugin_id: str, topic: str, queue_descriptor: QueueDescriptor, queue: Optional[Queue] = None):
|
|
296
|
+
self.plugin_id: str = plugin_id
|
|
297
|
+
self.topic: str = topic
|
|
298
|
+
self.queue_descriptor: QueueDescriptor = queue_descriptor
|
|
299
|
+
self._queue: Optional[Queue] = queue # Will be initialized when subscription is active
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def queue(self) -> Queue:
|
|
303
|
+
if self._queue is None:
|
|
304
|
+
self._queue = Queue(self.queue_descriptor)
|
|
305
|
+
return self._queue
|
|
306
|
+
|
|
307
|
+
@queue.setter
|
|
308
|
+
def queue(self, value: Queue):
|
|
309
|
+
self._queue = value
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def __str__(self):
|
|
313
|
+
return f"Subscriber(plugin_id={self.plugin_id}, topic={self.topic}, queue_descriptor={self.queue_descriptor})"
|
|
314
|
+
|
|
315
|
+
# ============================================================================
|
|
316
|
+
# STATUS DATA STRUCTURES
|
|
317
|
+
# ============================================================================
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class PluginStatus:
|
|
321
|
+
"""Status snapshot from plugin worker."""
|
|
322
|
+
plugin_id: str
|
|
323
|
+
state: str # "initialized", "loaded", "running", "stopped", "error"
|
|
324
|
+
is_running: bool
|
|
325
|
+
subscriptions: int # Number of data exchange subscriptions
|
|
326
|
+
subscribers: int # Number of subscribers to this plugin's topics
|
|
327
|
+
published: int # Number of topics published
|
|
328
|
+
errors: int
|
|
329
|
+
|
|
330
|
+
def to_dict(self):
|
|
331
|
+
return {
|
|
332
|
+
"plugin_id": self.plugin_id,
|
|
333
|
+
"state": self.state,
|
|
334
|
+
"is_running": self.is_running,
|
|
335
|
+
"subscriptions": self.subscriptions,
|
|
336
|
+
"subscribers": self.subscribers,
|
|
337
|
+
"published": self.published,
|
|
338
|
+
"errors": self.errors
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class ProcessStatus:
|
|
344
|
+
"""Status of a plugin from manager perspective."""
|
|
345
|
+
plugin_id: str
|
|
346
|
+
process_alive: bool
|
|
347
|
+
process_pid: Optional[int]
|
|
348
|
+
worker_status: Optional[PluginStatus] = None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
Classes for each request command:
|
|
354
|
+
PLUGIN_START = "plugin_start"
|
|
355
|
+
PLUGIN_STOP = "plugin_stop"
|
|
356
|
+
PLUGIN_STATUS = "plugin_status"
|
|
357
|
+
PLUGIN_STATUS_ALL = "plugin_status_all"
|
|
358
|
+
PLUGIN_ERRORS = "plugin_errors"
|
|
359
|
+
PLUGIN_CALL = "plugin_call"
|
|
360
|
+
|
|
361
|
+
ENV_SET = "env_set"
|
|
362
|
+
ENV_GET = "env_get"
|
|
363
|
+
ENV_GET_ALL = "env_get_all"
|
|
364
|
+
ENV_SUBSCRIBE = "env_subscribe"
|
|
365
|
+
ENV_UNSUBSCRIBE = "env_unsubscribe"
|
|
366
|
+
ENV_UPDATE = "env_update"
|
|
367
|
+
|
|
368
|
+
TOPIC_SUBSCRIBE = "topic_subscribe"
|
|
369
|
+
TOPIC_UNSUBSCRIBE = "topic_unsubscribe"
|
|
370
|
+
TOPIC_PUBLISH = "topic_publish"
|
|
371
|
+
TOPIC_LIST = "topic_list"
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
### Plugin Lifecycle Requests
|
|
378
|
+
|
|
379
|
+
@dataclass
|
|
380
|
+
class RequestPluginStart(Request):
|
|
381
|
+
command: CommandType = field(default=CommandType.PLUGIN_START, init=False)
|
|
382
|
+
plugin_id: Optional[str] = None
|
|
383
|
+
|
|
384
|
+
@dataclass
|
|
385
|
+
class RequestPluginStop(Request):
|
|
386
|
+
command: CommandType = field(default=CommandType.PLUGIN_STOP, init=False)
|
|
387
|
+
plugin_id: Optional[str] = None
|
|
388
|
+
|
|
389
|
+
@dataclass
|
|
390
|
+
class RequestPluginStatus(Request):
|
|
391
|
+
command: CommandType = field(default=CommandType.PLUGIN_STATUS, init=False)
|
|
392
|
+
plugin_id: Optional[str] = None
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class RequestPluginStatusAll(Request):
|
|
396
|
+
command: CommandType = field(default=CommandType.PLUGIN_STATUS_ALL, init=False)
|
|
397
|
+
|
|
398
|
+
@dataclass
|
|
399
|
+
class RequestPluginErrors(Request):
|
|
400
|
+
command: CommandType = field(default=CommandType.PLUGIN_ERRORS, init=False)
|
|
401
|
+
plugin_id: Optional[str] = None
|
|
402
|
+
|
|
403
|
+
@dataclass
|
|
404
|
+
class RequestPluginCall(Request):
|
|
405
|
+
command: CommandType = field(default=CommandType.PLUGIN_CALL, init=False)
|
|
406
|
+
plugin_id: str = None
|
|
407
|
+
method_name: str = None
|
|
408
|
+
args: tuple = None
|
|
409
|
+
kwargs: dict = None
|
|
410
|
+
|
|
411
|
+
@dataclass
|
|
412
|
+
class RequestEnvSet(Request):
|
|
413
|
+
command: CommandType = field(default=CommandType.ENV_SET, init=False)
|
|
414
|
+
name: str = None
|
|
415
|
+
value: Any = None
|
|
416
|
+
|
|
417
|
+
@dataclass
|
|
418
|
+
class RequestEnvGet(Request):
|
|
419
|
+
command: CommandType = field(default=CommandType.ENV_GET, init=False)
|
|
420
|
+
name: str = None
|
|
421
|
+
|
|
422
|
+
@dataclass
|
|
423
|
+
class RequestEnvGetAll(Request):
|
|
424
|
+
command: CommandType = field(default=CommandType.ENV_GET_ALL, init=False)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@dataclass
|
|
428
|
+
class RequestEnvSubscribe(Request):
|
|
429
|
+
command: CommandType = field(default=CommandType.ENV_SUBSCRIBE, init=False)
|
|
430
|
+
name: str = None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@dataclass
|
|
434
|
+
class RequestEnvUnsubscribe(Request):
|
|
435
|
+
command: CommandType = field(default=CommandType.ENV_UNSUBSCRIBE, init=False)
|
|
436
|
+
name: str = None
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
class RequestEnvUpdate(Request):
|
|
440
|
+
command: CommandType = field(default=CommandType.ENV_UPDATE, init=False)
|
|
441
|
+
name: str = None
|
|
442
|
+
|
|
443
|
+
### Pub/Sub Requests
|
|
444
|
+
|
|
445
|
+
@dataclass
|
|
446
|
+
class RequestTopicSubscribe(Request):
|
|
447
|
+
command: CommandType = field(default=CommandType.TOPIC_SUBSCRIBE, init=False)
|
|
448
|
+
subscriber: Subscriber = None
|
|
449
|
+
|
|
450
|
+
@dataclass
|
|
451
|
+
class RequestTopicUnsubscribe(Request):
|
|
452
|
+
command: CommandType = field(default=CommandType.TOPIC_UNSUBSCRIBE, init=False)
|
|
453
|
+
subscriber: Subscriber = None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@dataclass
|
|
457
|
+
class RequestTopicAnnounce(Request):
|
|
458
|
+
command: CommandType = field(default=CommandType.TOPIC_ANNOUNCE, init=False)
|
|
459
|
+
topic: str = None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@dataclass
|
|
463
|
+
class RequestTopicList(Request):
|
|
464
|
+
command: CommandType = field(default=CommandType.TOPIC_LIST, init=False)
|
|
465
|
+
plugin_id: Optional[str] = None
|
|
466
|
+
|