FlowerPower 0.9.12.4__py3-none-any.whl → 1.0.0b1__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.
- flowerpower/__init__.py +17 -2
- flowerpower/cfg/__init__.py +201 -149
- flowerpower/cfg/base.py +122 -24
- flowerpower/cfg/pipeline/__init__.py +254 -0
- flowerpower/cfg/pipeline/adapter.py +66 -0
- flowerpower/cfg/pipeline/run.py +40 -11
- flowerpower/cfg/pipeline/schedule.py +69 -79
- flowerpower/cfg/project/__init__.py +149 -0
- flowerpower/cfg/project/adapter.py +57 -0
- flowerpower/cfg/project/job_queue.py +165 -0
- flowerpower/cli/__init__.py +92 -35
- flowerpower/cli/job_queue.py +878 -0
- flowerpower/cli/mqtt.py +49 -4
- flowerpower/cli/pipeline.py +576 -381
- flowerpower/cli/utils.py +55 -0
- flowerpower/flowerpower.py +12 -7
- flowerpower/fs/__init__.py +20 -2
- flowerpower/fs/base.py +350 -26
- flowerpower/fs/ext.py +797 -216
- flowerpower/fs/storage_options.py +1097 -55
- flowerpower/io/base.py +13 -18
- flowerpower/io/loader/__init__.py +28 -0
- flowerpower/io/loader/deltatable.py +7 -10
- flowerpower/io/metadata.py +1 -0
- flowerpower/io/saver/__init__.py +28 -0
- flowerpower/io/saver/deltatable.py +4 -3
- flowerpower/job_queue/__init__.py +252 -0
- flowerpower/job_queue/apscheduler/__init__.py +11 -0
- flowerpower/job_queue/apscheduler/_setup/datastore.py +110 -0
- flowerpower/job_queue/apscheduler/_setup/eventbroker.py +93 -0
- flowerpower/job_queue/apscheduler/manager.py +1063 -0
- flowerpower/job_queue/apscheduler/setup.py +524 -0
- flowerpower/job_queue/apscheduler/trigger.py +169 -0
- flowerpower/job_queue/apscheduler/utils.py +309 -0
- flowerpower/job_queue/base.py +382 -0
- flowerpower/job_queue/rq/__init__.py +10 -0
- flowerpower/job_queue/rq/_trigger.py +37 -0
- flowerpower/job_queue/rq/concurrent_workers/gevent_worker.py +226 -0
- flowerpower/job_queue/rq/concurrent_workers/thread_worker.py +231 -0
- flowerpower/job_queue/rq/manager.py +1449 -0
- flowerpower/job_queue/rq/setup.py +150 -0
- flowerpower/job_queue/rq/utils.py +69 -0
- flowerpower/pipeline/__init__.py +5 -0
- flowerpower/pipeline/base.py +118 -0
- flowerpower/pipeline/io.py +407 -0
- flowerpower/pipeline/job_queue.py +505 -0
- flowerpower/pipeline/manager.py +1586 -0
- flowerpower/pipeline/registry.py +560 -0
- flowerpower/pipeline/runner.py +560 -0
- flowerpower/pipeline/visualizer.py +142 -0
- flowerpower/plugins/mqtt/__init__.py +12 -0
- flowerpower/plugins/mqtt/cfg.py +16 -0
- flowerpower/plugins/mqtt/manager.py +789 -0
- flowerpower/settings.py +110 -0
- flowerpower/utils/logging.py +21 -0
- flowerpower/utils/misc.py +57 -9
- flowerpower/utils/sql.py +122 -24
- flowerpower/utils/templates.py +18 -142
- flowerpower/web/app.py +0 -0
- flowerpower-1.0.0b1.dist-info/METADATA +324 -0
- flowerpower-1.0.0b1.dist-info/RECORD +94 -0
- {flowerpower-0.9.12.4.dist-info → flowerpower-1.0.0b1.dist-info}/WHEEL +1 -1
- flowerpower/cfg/pipeline/tracker.py +0 -14
- flowerpower/cfg/project/open_telemetry.py +0 -8
- flowerpower/cfg/project/tracker.py +0 -11
- flowerpower/cfg/project/worker.py +0 -19
- flowerpower/cli/scheduler.py +0 -309
- flowerpower/event_handler.py +0 -23
- flowerpower/mqtt.py +0 -525
- flowerpower/pipeline.py +0 -2419
- flowerpower/scheduler.py +0 -680
- flowerpower/tui.py +0 -79
- flowerpower/utils/datastore.py +0 -186
- flowerpower/utils/eventbroker.py +0 -127
- flowerpower/utils/executor.py +0 -58
- flowerpower/utils/trigger.py +0 -140
- flowerpower-0.9.12.4.dist-info/METADATA +0 -575
- flowerpower-0.9.12.4.dist-info/RECORD +0 -70
- /flowerpower/{cfg/pipeline/params.py → cli/worker.py} +0 -0
- {flowerpower-0.9.12.4.dist-info → flowerpower-1.0.0b1.dist-info}/entry_points.txt +0 -0
- {flowerpower-0.9.12.4.dist-info → flowerpower-1.0.0b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,789 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
import random
|
3
|
+
import socket
|
4
|
+
import time
|
5
|
+
from pathlib import Path
|
6
|
+
from types import TracebackType
|
7
|
+
from typing import Callable, Any
|
8
|
+
|
9
|
+
import mmh3
|
10
|
+
from loguru import logger
|
11
|
+
from munch import Munch
|
12
|
+
from paho.mqtt.client import CallbackAPIVersion, Client
|
13
|
+
|
14
|
+
from .cfg import MqttConfig
|
15
|
+
from ...cfg import ProjectConfig
|
16
|
+
from ...cfg.pipeline.run import ExecutorConfig, WithAdapterConfig
|
17
|
+
from ...cfg.project.adapter import AdapterConfig
|
18
|
+
from ...pipeline.manager import PipelineManager
|
19
|
+
from ...utils.logging import setup_logging
|
20
|
+
from ...fs import get_filesystem, AbstractFileSystem, BaseStorageOptions
|
21
|
+
|
22
|
+
setup_logging()
|
23
|
+
|
24
|
+
|
25
|
+
class MqttManager:
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
username: str | None = None,
|
29
|
+
password: str | None = None,
|
30
|
+
host: str | None = "localhost",
|
31
|
+
port: int | None = 1883,
|
32
|
+
topic: str | None = None,
|
33
|
+
first_reconnect_delay: int = 1,
|
34
|
+
max_reconnect_count: int = 5,
|
35
|
+
reconnect_rate: int = 2,
|
36
|
+
max_reconnect_delay: int = 60,
|
37
|
+
transport: str = "tcp",
|
38
|
+
clean_session: bool = True,
|
39
|
+
client_id: str | None = None,
|
40
|
+
client_id_suffix: str | None = None,
|
41
|
+
**kwargs,
|
42
|
+
):
|
43
|
+
if "user" in kwargs:
|
44
|
+
username = kwargs["user"]
|
45
|
+
if "pw" in kwargs:
|
46
|
+
password = kwargs["pw"]
|
47
|
+
|
48
|
+
self.topic = topic
|
49
|
+
|
50
|
+
self._username = username
|
51
|
+
self._password = password
|
52
|
+
self._host = host
|
53
|
+
self._port = port
|
54
|
+
self._first_reconnect_delay = first_reconnect_delay
|
55
|
+
self._max_reconnect_count = max_reconnect_count
|
56
|
+
self._reconnect_rate = reconnect_rate
|
57
|
+
self._max_reconnect_delay = max_reconnect_delay
|
58
|
+
self._transport = transport
|
59
|
+
|
60
|
+
self._clean_session = clean_session
|
61
|
+
self._client_id = client_id
|
62
|
+
self._client_id_suffix = client_id_suffix
|
63
|
+
|
64
|
+
self._client = None
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def from_event_broker(cls, base_dir: str | None = None):
|
68
|
+
base_dir = base_dir or str(Path.cwd())
|
69
|
+
|
70
|
+
jq_backend = ProjectConfig.load(base_dir=base_dir).job_queue.backend
|
71
|
+
if jq_backend is None:
|
72
|
+
raise ValueError(
|
73
|
+
"No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
|
74
|
+
"If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
|
75
|
+
"or use the provide a configuration dict to the MQTTManager.from_dict() method."
|
76
|
+
)
|
77
|
+
if hasattr(jq_backend, "event_broker") is False:
|
78
|
+
raise ValueError(
|
79
|
+
"No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
|
80
|
+
"If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
|
81
|
+
"or use the provide a configuration dict to the MQTTManager.from_dict() method."
|
82
|
+
)
|
83
|
+
if jq_backend.event_broker.type != "mqtt":
|
84
|
+
raise ValueError(
|
85
|
+
"No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
|
86
|
+
"If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
|
87
|
+
"or use the provide a configuration dict to the MQTTManager.from_dict() method."
|
88
|
+
)
|
89
|
+
else:
|
90
|
+
event_broker_cfg = jq_backend.event_broker
|
91
|
+
return cls(
|
92
|
+
**event_broker_cfg.dict(),
|
93
|
+
)
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def from_config(cls, cfg: MqttConfig | None=None, path: str | None = None, fs: AbstractFileSystem | None = None, storage_options: dict | BaseStorageOptions = {}):
|
97
|
+
if cfg is None:
|
98
|
+
if path is None:
|
99
|
+
raise ValueError(
|
100
|
+
"No configuration provided. Please provide `config` or `path` to the configuration file."
|
101
|
+
)
|
102
|
+
|
103
|
+
if cfg is None:
|
104
|
+
import os
|
105
|
+
if fs is None:
|
106
|
+
fs = get_filesystem(path=os.path.dirname(path), storage_options=storage_options)
|
107
|
+
|
108
|
+
cfg = MqttConfig.from_yaml(path=os.path.basename(path), fs=fs)
|
109
|
+
|
110
|
+
return cls(
|
111
|
+
**cfg.dict(),
|
112
|
+
)
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def from_dict(cls, cfg: dict):
|
116
|
+
return cls(
|
117
|
+
**cfg,
|
118
|
+
)
|
119
|
+
|
120
|
+
def __enter__(self) -> "MqttManager":
|
121
|
+
self.connect()
|
122
|
+
return self
|
123
|
+
|
124
|
+
def __exit__(
|
125
|
+
self,
|
126
|
+
exc_type: type[BaseException] | None,
|
127
|
+
exc_val: BaseException | None,
|
128
|
+
exc_tb: TracebackType | None,
|
129
|
+
) -> None:
|
130
|
+
# Add any cleanup code here if needed
|
131
|
+
self.disconnect()
|
132
|
+
|
133
|
+
@staticmethod
|
134
|
+
def _on_connect(client, userdata, flags, rc, properties):
|
135
|
+
if rc == 0:
|
136
|
+
logger.info(f"Connected to MQTT Broker {userdata.host}!")
|
137
|
+
logger.info(
|
138
|
+
f"Connected as {userdata.client_id} with clean session {userdata.clean_session}"
|
139
|
+
)
|
140
|
+
else:
|
141
|
+
logger.error(f"Failed to connect, return code {rc}")
|
142
|
+
|
143
|
+
@staticmethod
|
144
|
+
def _on_disconnect(client, userdata, disconnect_flags, rc, properties=None):
|
145
|
+
reconnect_count, reconnect_delay = 0, userdata.first_reconnect_delay
|
146
|
+
|
147
|
+
if userdata.max_reconnect_count == 0:
|
148
|
+
logger.info("Disconnected successfully!")
|
149
|
+
return
|
150
|
+
|
151
|
+
while reconnect_count < userdata.max_reconnect_count:
|
152
|
+
logger.info(f"Reconnecting in {reconnect_delay} seconds...")
|
153
|
+
time.sleep(reconnect_delay)
|
154
|
+
|
155
|
+
try:
|
156
|
+
client.reconnect()
|
157
|
+
logger.info("Reconnected successfully!")
|
158
|
+
return
|
159
|
+
except Exception as err:
|
160
|
+
logger.error(f"{err}. Reconnect failed. Retrying...")
|
161
|
+
|
162
|
+
reconnect_delay *= userdata.reconnect_rate
|
163
|
+
reconnect_delay = min(reconnect_delay, userdata.max_reconnect_delay)
|
164
|
+
reconnect_count += 1
|
165
|
+
logger.info(f"Reconnect failed after {reconnect_count} attempts. Exiting...")
|
166
|
+
|
167
|
+
@staticmethod
|
168
|
+
def _on_publish(client, userdata, mid, rc, properties):
|
169
|
+
logger.info(f"Published message id: {mid}")
|
170
|
+
|
171
|
+
@staticmethod
|
172
|
+
def _on_subscribe(client, userdata, mid, qos, properties):
|
173
|
+
if isinstance(qos, list):
|
174
|
+
qos_msg = str(qos[0])
|
175
|
+
else:
|
176
|
+
qos_msg = f"and granted QoS {qos[0]}"
|
177
|
+
logger.info(f"Subscribed {qos_msg}")
|
178
|
+
|
179
|
+
def connect(self) -> Client:
|
180
|
+
if self._client_id is None and self._clean_session:
|
181
|
+
# Random Client ID when clean session is True
|
182
|
+
self._client_id = f"flowerpower-client-{random.randint(0, 10000)}"
|
183
|
+
elif self._client_id is None and not self._clean_session:
|
184
|
+
# Deterministic Client ID when clean session is False
|
185
|
+
self._client_id = f"flowerpower-client-{
|
186
|
+
mmh3.hash_bytes(
|
187
|
+
str(self._host)
|
188
|
+
+ str(self._port)
|
189
|
+
+ str(self.topic)
|
190
|
+
+ str(socket.gethostname())
|
191
|
+
).hex()
|
192
|
+
}"
|
193
|
+
|
194
|
+
if self._client_id_suffix:
|
195
|
+
self._client_id = f"{self._client_id}-{self._client_id_suffix}"
|
196
|
+
|
197
|
+
logger.debug(
|
198
|
+
f"Client ID: {self._client_id} - Clean session: {self._clean_session}"
|
199
|
+
)
|
200
|
+
client = Client(
|
201
|
+
CallbackAPIVersion.VERSION2,
|
202
|
+
client_id=self._client_id,
|
203
|
+
transport=self._transport,
|
204
|
+
clean_session=self._clean_session,
|
205
|
+
userdata=Munch(
|
206
|
+
user=self._username,
|
207
|
+
pw=self._password,
|
208
|
+
host=self._host,
|
209
|
+
port=self._port,
|
210
|
+
topic=self.topic,
|
211
|
+
first_reconnect_delay=self._first_reconnect_delay,
|
212
|
+
max_reconnect_count=self._max_reconnect_count,
|
213
|
+
reconnect_rate=self._reconnect_rate,
|
214
|
+
max_reconnect_delay=self._max_reconnect_delay,
|
215
|
+
transport=self._transport,
|
216
|
+
client_id=self._client_id,
|
217
|
+
clean_session=self._clean_session,
|
218
|
+
),
|
219
|
+
)
|
220
|
+
if self._password != "" and self._username != "":
|
221
|
+
client.username_pw_set(self._username, self._password)
|
222
|
+
|
223
|
+
client.on_connect = self._on_connect # self._on_connect
|
224
|
+
client.on_disconnect = self._on_disconnect # self._on_disconnect
|
225
|
+
client.on_publish = self._on_publish
|
226
|
+
client.on_subscribe = self._on_subscribe
|
227
|
+
|
228
|
+
client.connect(self._host, self._port)
|
229
|
+
self._client = client
|
230
|
+
# topic = topic or topic
|
231
|
+
if self.topic:
|
232
|
+
self.subscribe()
|
233
|
+
|
234
|
+
def disconnect(self):
|
235
|
+
self._max_reconnect_count = 0
|
236
|
+
self._client._userdata.max_reconnect_count = 0
|
237
|
+
self._client.disconnect()
|
238
|
+
|
239
|
+
def reconnect(self):
|
240
|
+
self._client.reconnect()
|
241
|
+
|
242
|
+
def publish(self, topic, payload):
|
243
|
+
if self._client is None:
|
244
|
+
self.connect()
|
245
|
+
# elif self._client.is_connected() is False:
|
246
|
+
# self.reconnect()
|
247
|
+
self._client.publish(topic, payload)
|
248
|
+
|
249
|
+
def subscribe(self, topic: str | None = None, qos: int = 2):
|
250
|
+
if topic is not None:
|
251
|
+
self.topic = topic
|
252
|
+
self._client.subscribe(self.topic, qos=qos)
|
253
|
+
|
254
|
+
def unsubscribe(self, topic: str | None = None):
|
255
|
+
if topic is not None:
|
256
|
+
self.topic = topic
|
257
|
+
self._client.unsubscribe(self.topic)
|
258
|
+
|
259
|
+
def register_on_message(self, on_message: Callable):
|
260
|
+
self._client.on_message = on_message
|
261
|
+
|
262
|
+
def run_in_background(
|
263
|
+
self,
|
264
|
+
on_message: Callable,
|
265
|
+
topic: str | None = None,
|
266
|
+
qos: int = 2,
|
267
|
+
) -> None:
|
268
|
+
"""
|
269
|
+
Run the MQTT client in the background.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
on_message: Callback function to run when a message is received
|
273
|
+
topic: MQTT topic to listen to
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
None
|
277
|
+
"""
|
278
|
+
if self._client is None or not self._client.is_connected():
|
279
|
+
self.connect()
|
280
|
+
|
281
|
+
if topic:
|
282
|
+
self.subscribe(topic, qos=qos)
|
283
|
+
|
284
|
+
self._client.on_message = on_message
|
285
|
+
self._client.loop_start()
|
286
|
+
|
287
|
+
def run_until_break(
|
288
|
+
self,
|
289
|
+
on_message: Callable,
|
290
|
+
topic: str | None = None,
|
291
|
+
qos: int = 2,
|
292
|
+
):
|
293
|
+
"""
|
294
|
+
Run the MQTT client until a break signal is received.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
on_message: Callback function to run when a message is received
|
298
|
+
topic: MQTT topic to listen to
|
299
|
+
|
300
|
+
Returns:
|
301
|
+
None
|
302
|
+
"""
|
303
|
+
if self._client is None or not self._client.is_connected():
|
304
|
+
self.connect()
|
305
|
+
|
306
|
+
if topic:
|
307
|
+
self.subscribe(topic, qos=qos)
|
308
|
+
|
309
|
+
self._client.on_message = on_message
|
310
|
+
self._client.loop_forever()
|
311
|
+
|
312
|
+
def start_listener(
|
313
|
+
self,
|
314
|
+
on_message: Callable,
|
315
|
+
topic: str | None = None,
|
316
|
+
background: bool = False,
|
317
|
+
qos: int = 2,
|
318
|
+
) -> None:
|
319
|
+
"""
|
320
|
+
Start the MQTT listener.
|
321
|
+
|
322
|
+
Args:
|
323
|
+
on_message: Callback function to run when a message is received
|
324
|
+
topic: MQTT topic to listen to
|
325
|
+
background: Run the listener in the background
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
None
|
329
|
+
"""
|
330
|
+
if background:
|
331
|
+
self.run_in_background(on_message, topic, qos)
|
332
|
+
else:
|
333
|
+
self.run_until_break(on_message, topic, qos)
|
334
|
+
|
335
|
+
def stop_listener(
|
336
|
+
self,
|
337
|
+
) -> None:
|
338
|
+
"""
|
339
|
+
Stop the MQTT listener.
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
None
|
343
|
+
"""
|
344
|
+
self._client.loop_stop()
|
345
|
+
logger.info("Client stopped.")
|
346
|
+
|
347
|
+
|
348
|
+
|
349
|
+
def run_pipeline_on_message(
|
350
|
+
self,
|
351
|
+
name: str,
|
352
|
+
topic: str | None = None,
|
353
|
+
inputs: dict | None = None,
|
354
|
+
final_vars: list | None = None,
|
355
|
+
config: dict | None = None,
|
356
|
+
cache: bool | dict = False,
|
357
|
+
executor_cfg: str | dict | ExecutorConfig | None = None,
|
358
|
+
with_adapter_cfg: dict | WithAdapterConfig | None = None,
|
359
|
+
pipeline_adapter_cfg: dict | AdapterConfig | None = None,
|
360
|
+
project_adapter_cfg: dict | AdapterConfig | None = None,
|
361
|
+
adapter: dict[str, Any] | None = None,
|
362
|
+
reload: bool = False,
|
363
|
+
log_level: str | None = None,
|
364
|
+
result_ttl: float | dt.timedelta = 0,
|
365
|
+
run_in: int | str | dt.timedelta | None = None,
|
366
|
+
max_retries: int | None = None,
|
367
|
+
retry_delay: float | None = None,
|
368
|
+
jitter_factor: float | None = None,
|
369
|
+
retry_exceptions: tuple | list | None = None,
|
370
|
+
as_job: bool = False,
|
371
|
+
base_dir: str | None = None,
|
372
|
+
storage_options: dict = {},
|
373
|
+
fs: AbstractFileSystem | None = None,
|
374
|
+
background: bool = False,
|
375
|
+
qos: int = 2,
|
376
|
+
config_hook: Callable[[bytes, int], dict] | None = None,
|
377
|
+
**kwargs,
|
378
|
+
):
|
379
|
+
"""
|
380
|
+
Start a pipeline listener that listens to a topic and processes the message using a pipeline.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
name (str): Name of the pipeline
|
384
|
+
topic (str | None): MQTT topic to listen to
|
385
|
+
inputs (dict | None): Inputs for the pipeline
|
386
|
+
final_vars (list | None): Final variables for the pipeline
|
387
|
+
config (dict | None): Configuration for the pipeline driver
|
388
|
+
cache (bool | dict): Cache for the pipeline
|
389
|
+
executor_cfg (str | dict | ExecutorConfig | None): Executor configuration
|
390
|
+
with_adapter_cfg (dict | WithAdapterConfig | None): With adapter configuration
|
391
|
+
pipeline_adapter_cfg (dict | AdapterConfig | None): Pipeline adapter configuration
|
392
|
+
project_adapter_cfg (dict | AdapterConfig | None): Project adapter configuration
|
393
|
+
adapter (dict[str, Any] | None): Adapter configuration
|
394
|
+
reload (bool): Reload the pipeline
|
395
|
+
log_level (str | None): Log level for the pipeline
|
396
|
+
result_ttl (float | dt.timedelta): Result expiration time for the pipeline
|
397
|
+
run_in (int | str | dt.timedelta | None): Run in time for the pipeline
|
398
|
+
max_retries (int | None): Maximum number of retries for the pipeline
|
399
|
+
retry_delay (float | None): Delay between retries for the pipeline
|
400
|
+
jitter_factor (float | None): Jitter factor for the pipeline
|
401
|
+
retry_exceptions (tuple | list | None): Exceptions to retry for the pipeline
|
402
|
+
as_job (bool): Run the pipeline as a job
|
403
|
+
base_dir (str | None): Base directory for the pipeline
|
404
|
+
storage_options (dict): Storage options for the pipeline
|
405
|
+
fs (AbstractFileSystem | None): File system for the pipeline
|
406
|
+
background (bool): Run the listener in the background
|
407
|
+
qos (int): Quality of Service for the MQTT client
|
408
|
+
config_hook (Callable[[bytes, int], dict] | None): Hook function to modify the configuration of the pipeline
|
409
|
+
**kwargs: Additional keyword arguments
|
410
|
+
|
411
|
+
Returns:
|
412
|
+
None
|
413
|
+
|
414
|
+
Raises:
|
415
|
+
ValueError: If the config_hook is not callable
|
416
|
+
|
417
|
+
Example:
|
418
|
+
```python
|
419
|
+
from flowerpower.plugins.mqtt import MqttManager
|
420
|
+
mqtt = MqttManager()
|
421
|
+
mqtt.run_pipeline_on_message(
|
422
|
+
name="my_pipeline",
|
423
|
+
topic="my_topic",
|
424
|
+
inputs={"key": "value"},
|
425
|
+
config={"param": "value"},
|
426
|
+
as_job=True,
|
427
|
+
)
|
428
|
+
```
|
429
|
+
"""
|
430
|
+
|
431
|
+
if inputs is None:
|
432
|
+
inputs = {}
|
433
|
+
|
434
|
+
if config is None:
|
435
|
+
config = {}
|
436
|
+
|
437
|
+
if config_hook is not None and not callable(config_hook):
|
438
|
+
raise ValueError("config_hook must be a callable function")
|
439
|
+
|
440
|
+
def on_message(client, userdata, msg):
|
441
|
+
logger.info(f"Received message on topic {topic}")
|
442
|
+
|
443
|
+
inputs["payload"] = msg.payload
|
444
|
+
inputs["topic"] = msg.topic
|
445
|
+
|
446
|
+
if config_hook is not None:
|
447
|
+
config_ = config_hook(inputs["payload"], inputs["topic"])
|
448
|
+
logger.debug(f"Config from hook: {config_}")
|
449
|
+
if any([k in config_ for k in config.keys()]):
|
450
|
+
logger.warning(
|
451
|
+
"Config from hook overwrites config from pipeline"
|
452
|
+
)
|
453
|
+
config.update(config_)
|
454
|
+
logger.debug(f"Config after update: {config}")
|
455
|
+
|
456
|
+
|
457
|
+
with PipelineManager(
|
458
|
+
storage_options=storage_options, fs=fs, base_dir=base_dir
|
459
|
+
) as pipeline:
|
460
|
+
try:
|
461
|
+
if as_job:
|
462
|
+
pipeline.add_job(
|
463
|
+
name=name,
|
464
|
+
inputs=inputs,
|
465
|
+
final_vars=final_vars,
|
466
|
+
config=config,
|
467
|
+
cache=cache,
|
468
|
+
executor_cfg=executor_cfg,
|
469
|
+
with_adapter_cfg=with_adapter_cfg,
|
470
|
+
pipeline_adapter_cfg=pipeline_adapter_cfg,
|
471
|
+
project_adapter_cfg=project_adapter_cfg,
|
472
|
+
adapter=adapter,
|
473
|
+
run_in=run_in,
|
474
|
+
reload=reload,
|
475
|
+
log_level=log_level,
|
476
|
+
result_ttl=result_ttl,
|
477
|
+
max_retries=max_retries,
|
478
|
+
retry_delay=retry_delay,
|
479
|
+
jitter_factor=jitter_factor,
|
480
|
+
retry_exceptions=retry_exceptions,
|
481
|
+
**kwargs,
|
482
|
+
)
|
483
|
+
else:
|
484
|
+
pipeline.run(
|
485
|
+
name=name,
|
486
|
+
inputs=inputs,
|
487
|
+
final_vars=final_vars,
|
488
|
+
config=config,
|
489
|
+
cache=cache,
|
490
|
+
executor_cfg=executor_cfg,
|
491
|
+
with_adapter_cfg=with_adapter_cfg,
|
492
|
+
pipeline_adapter_cfg=pipeline_adapter_cfg,
|
493
|
+
project_adapter_cfg=project_adapter_cfg,
|
494
|
+
adapter=adapter,
|
495
|
+
reload=reload,
|
496
|
+
log_level=log_level,
|
497
|
+
result_ttl=result_ttl,
|
498
|
+
run_in=run_in,
|
499
|
+
max_retries=max_retries,
|
500
|
+
retry_delay=retry_delay,
|
501
|
+
jitter_factor=jitter_factor,
|
502
|
+
retry_exceptions=retry_exceptions,
|
503
|
+
**kwargs,
|
504
|
+
)
|
505
|
+
logger.success("Message processed successfully")
|
506
|
+
return
|
507
|
+
except Exception as e:
|
508
|
+
_ = e
|
509
|
+
logger.exception(e)
|
510
|
+
|
511
|
+
logger.warning("Message processing failed")
|
512
|
+
|
513
|
+
self.start_listener(
|
514
|
+
on_message=on_message, topic=topic, background=background, qos=qos
|
515
|
+
)
|
516
|
+
|
517
|
+
|
518
|
+
def start_listener(
|
519
|
+
on_message: Callable,
|
520
|
+
topic: str | None = None,
|
521
|
+
background: bool = False,
|
522
|
+
mqtt_cfg: dict | MqttConfig = {},
|
523
|
+
base_dir: str | None = None,
|
524
|
+
username: str | None = None,
|
525
|
+
password: str | None = None,
|
526
|
+
host: str | None = None,
|
527
|
+
port: int | None = None,
|
528
|
+
clean_session: bool = True,
|
529
|
+
qos: int = 2,
|
530
|
+
client_id: str | None = None,
|
531
|
+
client_id_suffix: str | None = None,
|
532
|
+
config_hook: Callable[[bytes, int], dict] | None = None,
|
533
|
+
**kwargs,
|
534
|
+
) -> None:
|
535
|
+
"""
|
536
|
+
Start the MQTT listener.
|
537
|
+
|
538
|
+
The connection to the MQTT broker is established using the provided configuration of a
|
539
|
+
MQTT event broker defined in the project configuration file `conf/project.toml`.
|
540
|
+
If no configuration is found, you have to provide either the argument `mqtt_cfg`, dict with the
|
541
|
+
connection parameters or the arguments `username`, `password`, `host`, and `port`.
|
542
|
+
|
543
|
+
Args:
|
544
|
+
on_message (Callable): Callback function to run when a message is received
|
545
|
+
topic (str | None): MQTT topic to listen to
|
546
|
+
background (bool): Run the listener in the background
|
547
|
+
mqtt_cfg (dict | MqttConfig): MQTT client configuration. Use either this or arguments
|
548
|
+
username, password, host, and port.
|
549
|
+
base_dir (str | None): Base directory for the module
|
550
|
+
username (str | None): Username for the MQTT client
|
551
|
+
password (str | None): Password for the MQTT client
|
552
|
+
host (str | None): Host for the MQTT client
|
553
|
+
port (int | None): Port for the MQTT client
|
554
|
+
clean_session (bool): Clean session flag for the MQTT client
|
555
|
+
qos (int): Quality of Service for the MQTT client
|
556
|
+
client_id (str | None): Client ID for the MQTT client
|
557
|
+
client_id_suffix (str | None): Client ID suffix for the MQTT client
|
558
|
+
config_hook (Callable[[bytes, int], dict] | None): Hook function to modify the configuration of the pipeline
|
559
|
+
**kwargs: Additional keyword arguments
|
560
|
+
|
561
|
+
Returns:
|
562
|
+
None
|
563
|
+
|
564
|
+
Raises:
|
565
|
+
ValueError: If the config_hook is not callable
|
566
|
+
ValueError: If no client configuration is found
|
567
|
+
|
568
|
+
Example:
|
569
|
+
```python
|
570
|
+
from flowerpower.plugins.mqtt import start_listener
|
571
|
+
|
572
|
+
start_listener(
|
573
|
+
on_message=my_on_message_function,
|
574
|
+
topic="my_topic",
|
575
|
+
background=True,
|
576
|
+
mqtt_cfg={"host": "localhost", "port": 1883},
|
577
|
+
)
|
578
|
+
```
|
579
|
+
"""
|
580
|
+
try:
|
581
|
+
client = MqttManager.from_event_broker(base_dir)
|
582
|
+
except ValueError:
|
583
|
+
if mqtt_cfg:
|
584
|
+
if isinstance(mqtt_cfg, MqttConfig):
|
585
|
+
client = MqttManager.from_config(mqtt_cfg)
|
586
|
+
elif isinstance(mqtt_cfg, dict):
|
587
|
+
client = MqttManager.from_dict(mqtt_cfg)
|
588
|
+
elif host and port:
|
589
|
+
client = MqttManager(
|
590
|
+
username=username,
|
591
|
+
password=password,
|
592
|
+
host=host,
|
593
|
+
port=port,
|
594
|
+
clean_session=clean_session,
|
595
|
+
client_id=client_id,
|
596
|
+
client_id_suffix=client_id_suffix,
|
597
|
+
config_hook=config_hook,
|
598
|
+
**kwargs,
|
599
|
+
|
600
|
+
)
|
601
|
+
else:
|
602
|
+
raise ValueError(
|
603
|
+
"No client configuration found. Please provide a client configuration "
|
604
|
+
"or a FlowerPower project base directory, in which a event broker is "
|
605
|
+
"configured in the `config/project.yml` file."
|
606
|
+
)
|
607
|
+
|
608
|
+
client.start_listener(on_message=on_message, topic=topic, background=background, qos=qos)
|
609
|
+
|
610
|
+
|
611
|
+
def run_pipeline_on_message(
|
612
|
+
name: str,
|
613
|
+
topic: str | None = None,
|
614
|
+
inputs: dict | None = None,
|
615
|
+
final_vars: list | None = None,
|
616
|
+
config: dict | None = None,
|
617
|
+
cache: bool | dict = False,
|
618
|
+
executor_cfg: str | dict | ExecutorConfig | None = None,
|
619
|
+
with_adapter_cfg: dict | WithAdapterConfig | None = None,
|
620
|
+
pipeline_adapter_cfg: dict | AdapterConfig | None = None,
|
621
|
+
project_adapter_cfg: dict | AdapterConfig | None = None,
|
622
|
+
adapter: dict[str, Any] | None = None,
|
623
|
+
reload: bool = False,
|
624
|
+
log_level: str | None = None,
|
625
|
+
result_ttl: float | dt.timedelta = 0,
|
626
|
+
run_in: int | str | dt.timedelta | None = None,
|
627
|
+
max_retries: int | None = None,
|
628
|
+
retry_delay: float | None = None,
|
629
|
+
jitter_factor: float | None = None,
|
630
|
+
retry_exceptions: tuple | list | None = None,
|
631
|
+
as_job: bool = False,
|
632
|
+
base_dir: str | None = None,
|
633
|
+
storage_options: dict = {},
|
634
|
+
fs: AbstractFileSystem | None = None,
|
635
|
+
background: bool = False,
|
636
|
+
mqtt_cfg: dict | MqttConfig = {},
|
637
|
+
host: str | None = None,
|
638
|
+
port: int | None = None,
|
639
|
+
username: str | None = None,
|
640
|
+
password: str | None = None,
|
641
|
+
clean_session: bool = True,
|
642
|
+
qos: int = 2,
|
643
|
+
client_id: str | None = None,
|
644
|
+
client_id_suffix: str | None = None,
|
645
|
+
config_hook: Callable[[bytes, int], dict] | None = None,
|
646
|
+
**kwargs,
|
647
|
+
):
|
648
|
+
"""
|
649
|
+
Start a pipeline listener that listens to a topic and processes the message using a pipeline.
|
650
|
+
|
651
|
+
Args:
|
652
|
+
name (str): Name of the pipeline
|
653
|
+
topic (str | None): MQTT topic to listen to
|
654
|
+
inputs (dict | None): Inputs for the pipeline
|
655
|
+
final_vars (list | None): Final variables for the pipeline
|
656
|
+
config (dict | None): Configuration for the pipeline driver
|
657
|
+
cache (bool | dict): Cache for the pipeline
|
658
|
+
executor_cfg (str | dict | ExecutorConfig | None): Executor configuration
|
659
|
+
with_adapter_cfg (dict | WithAdapterConfig | None): With adapter configuration
|
660
|
+
pipeline_adapter_cfg (dict | AdapterConfig | None): Pipeline adapter configuration
|
661
|
+
project_adapter_cfg (dict | AdapterConfig | None): Project adapter configuration
|
662
|
+
adapter (dict[str, Any] | None): Adapter configuration
|
663
|
+
reload (bool): Reload the pipeline
|
664
|
+
log_level (str | None): Log level for the pipeline
|
665
|
+
result_ttl (float | dt.timedelta): Result expiration time for the pipeline
|
666
|
+
run_in (int | str | dt.timedelta | None): Run in time for the pipeline
|
667
|
+
max_retries (int | None): Maximum number of retries for the pipeline
|
668
|
+
retry_delay (float | None): Delay between retries for the pipeline
|
669
|
+
jitter_factor (float | None): Jitter factor for the pipeline
|
670
|
+
retry_exceptions (tuple | list | None): Exceptions to retry for the pipeline
|
671
|
+
as_job (bool): Run the pipeline as a job
|
672
|
+
base_dir (str | None): Base directory for the pipeline
|
673
|
+
storage_options (dict): Storage options for the pipeline
|
674
|
+
fs (AbstractFileSystem | None): File system for the pipeline
|
675
|
+
background (bool): Run the listener in the background
|
676
|
+
mqtt_cfg (dict | MqttConfig): MQTT client configuration. Use either this or arguments
|
677
|
+
username, password, host, and port.
|
678
|
+
host (str | None): Host for the MQTT client
|
679
|
+
port (int | None): Port for the MQTT client
|
680
|
+
username (str | None): Username for the MQTT client
|
681
|
+
password (str | None): Password for the MQTT client
|
682
|
+
clean_session (bool): Clean session flag for the MQTT client
|
683
|
+
qos (int): Quality of Service for the MQTT client
|
684
|
+
client_id (str | None): Client ID for the MQTT client
|
685
|
+
client_id_suffix (str | None): Client ID suffix for the MQTT client
|
686
|
+
config_hook (Callable[[bytes, int], dict] | None): Hook function to modify the configuration of the pipeline
|
687
|
+
**kwargs: Additional keyword arguments
|
688
|
+
|
689
|
+
Returns:
|
690
|
+
None
|
691
|
+
|
692
|
+
Raises:
|
693
|
+
ValueError: If the config_hook is not callable
|
694
|
+
ValueError: If no client configuration is found
|
695
|
+
|
696
|
+
Example:
|
697
|
+
```python
|
698
|
+
from flowerpower.plugins.mqtt import run_pipeline_on_message
|
699
|
+
|
700
|
+
run_pipeline_on_message(
|
701
|
+
name="my_pipeline",
|
702
|
+
topic="my_topic",
|
703
|
+
inputs={"key": "value"},
|
704
|
+
config={"param": "value"},
|
705
|
+
as_job=True,
|
706
|
+
)
|
707
|
+
```
|
708
|
+
"""
|
709
|
+
try:
|
710
|
+
client = MqttManager.from_event_broker(base_dir)
|
711
|
+
except ValueError:
|
712
|
+
if mqtt_cfg:
|
713
|
+
if isinstance(mqtt_cfg, MqttConfig):
|
714
|
+
client = MqttManager.from_config(mqtt_cfg)
|
715
|
+
elif isinstance(mqtt_cfg, dict):
|
716
|
+
client = MqttManager.from_dict(mqtt_cfg)
|
717
|
+
elif host and port:
|
718
|
+
client = MqttManager(
|
719
|
+
username=username,
|
720
|
+
password=password,
|
721
|
+
host=host,
|
722
|
+
port=port,
|
723
|
+
clean_session=clean_session,
|
724
|
+
client_id=client_id,
|
725
|
+
client_id_suffix=client_id_suffix,
|
726
|
+
config_hook=config_hook,
|
727
|
+
**kwargs,
|
728
|
+
|
729
|
+
)
|
730
|
+
else:
|
731
|
+
raise ValueError(
|
732
|
+
"No client configuration found. Please provide a client configuration "
|
733
|
+
"or a FlowerPower project base directory, in which a event broker is "
|
734
|
+
"configured in the `config/project.yml` file."
|
735
|
+
)
|
736
|
+
|
737
|
+
if client._client_id is None and client_id is not None:
|
738
|
+
client._client_id = client_id
|
739
|
+
|
740
|
+
if client._client_id_suffix is None and client_id_suffix is not None:
|
741
|
+
client._client_id_suffix = client_id_suffix
|
742
|
+
|
743
|
+
"""
|
744
|
+
cli_clean_session | config_clean_session | result
|
745
|
+
TRUE TRUE TRUE
|
746
|
+
FALSE FALSE FALSE
|
747
|
+
FALSE TRUE FALSE
|
748
|
+
TRUE FALSE FALSE
|
749
|
+
|
750
|
+
Clean session should only use default value if neither cli nor config source says otherwise
|
751
|
+
"""
|
752
|
+
client._clean_session = client._clean_session and clean_session
|
753
|
+
|
754
|
+
if client.topic is None and topic is not None:
|
755
|
+
client.topic = topic
|
756
|
+
|
757
|
+
client.run_pipeline_on_message(
|
758
|
+
name=name,
|
759
|
+
topic=topic,
|
760
|
+
inputs=inputs,
|
761
|
+
final_vars=final_vars,
|
762
|
+
config=config,
|
763
|
+
cache=cache,
|
764
|
+
executor_cfg=executor_cfg,
|
765
|
+
with_adapter_cfg=with_adapter_cfg,
|
766
|
+
pipeline_adapter_cfg=pipeline_adapter_cfg,
|
767
|
+
project_adapter_cfg=project_adapter_cfg,
|
768
|
+
adapter=adapter,
|
769
|
+
reload=reload,
|
770
|
+
log_level=log_level,
|
771
|
+
result_ttl=result_ttl,
|
772
|
+
run_in=run_in,
|
773
|
+
max_retries=max_retries,
|
774
|
+
retry_delay=retry_delay,
|
775
|
+
jitter_factor=jitter_factor,
|
776
|
+
retry_exceptions=retry_exceptions,
|
777
|
+
as_job=as_job,
|
778
|
+
base_dir=base_dir,
|
779
|
+
storage_options=storage_options,
|
780
|
+
fs=fs,
|
781
|
+
background=background,
|
782
|
+
qos=qos,
|
783
|
+
client_id=client_id,
|
784
|
+
client_id_suffix=client_id_suffix,
|
785
|
+
config_hook=config_hook,
|
786
|
+
**kwargs,
|
787
|
+
)
|
788
|
+
|
789
|
+
|