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.
Files changed (81) hide show
  1. flowerpower/__init__.py +17 -2
  2. flowerpower/cfg/__init__.py +201 -149
  3. flowerpower/cfg/base.py +122 -24
  4. flowerpower/cfg/pipeline/__init__.py +254 -0
  5. flowerpower/cfg/pipeline/adapter.py +66 -0
  6. flowerpower/cfg/pipeline/run.py +40 -11
  7. flowerpower/cfg/pipeline/schedule.py +69 -79
  8. flowerpower/cfg/project/__init__.py +149 -0
  9. flowerpower/cfg/project/adapter.py +57 -0
  10. flowerpower/cfg/project/job_queue.py +165 -0
  11. flowerpower/cli/__init__.py +92 -35
  12. flowerpower/cli/job_queue.py +878 -0
  13. flowerpower/cli/mqtt.py +49 -4
  14. flowerpower/cli/pipeline.py +576 -381
  15. flowerpower/cli/utils.py +55 -0
  16. flowerpower/flowerpower.py +12 -7
  17. flowerpower/fs/__init__.py +20 -2
  18. flowerpower/fs/base.py +350 -26
  19. flowerpower/fs/ext.py +797 -216
  20. flowerpower/fs/storage_options.py +1097 -55
  21. flowerpower/io/base.py +13 -18
  22. flowerpower/io/loader/__init__.py +28 -0
  23. flowerpower/io/loader/deltatable.py +7 -10
  24. flowerpower/io/metadata.py +1 -0
  25. flowerpower/io/saver/__init__.py +28 -0
  26. flowerpower/io/saver/deltatable.py +4 -3
  27. flowerpower/job_queue/__init__.py +252 -0
  28. flowerpower/job_queue/apscheduler/__init__.py +11 -0
  29. flowerpower/job_queue/apscheduler/_setup/datastore.py +110 -0
  30. flowerpower/job_queue/apscheduler/_setup/eventbroker.py +93 -0
  31. flowerpower/job_queue/apscheduler/manager.py +1063 -0
  32. flowerpower/job_queue/apscheduler/setup.py +524 -0
  33. flowerpower/job_queue/apscheduler/trigger.py +169 -0
  34. flowerpower/job_queue/apscheduler/utils.py +309 -0
  35. flowerpower/job_queue/base.py +382 -0
  36. flowerpower/job_queue/rq/__init__.py +10 -0
  37. flowerpower/job_queue/rq/_trigger.py +37 -0
  38. flowerpower/job_queue/rq/concurrent_workers/gevent_worker.py +226 -0
  39. flowerpower/job_queue/rq/concurrent_workers/thread_worker.py +231 -0
  40. flowerpower/job_queue/rq/manager.py +1449 -0
  41. flowerpower/job_queue/rq/setup.py +150 -0
  42. flowerpower/job_queue/rq/utils.py +69 -0
  43. flowerpower/pipeline/__init__.py +5 -0
  44. flowerpower/pipeline/base.py +118 -0
  45. flowerpower/pipeline/io.py +407 -0
  46. flowerpower/pipeline/job_queue.py +505 -0
  47. flowerpower/pipeline/manager.py +1586 -0
  48. flowerpower/pipeline/registry.py +560 -0
  49. flowerpower/pipeline/runner.py +560 -0
  50. flowerpower/pipeline/visualizer.py +142 -0
  51. flowerpower/plugins/mqtt/__init__.py +12 -0
  52. flowerpower/plugins/mqtt/cfg.py +16 -0
  53. flowerpower/plugins/mqtt/manager.py +789 -0
  54. flowerpower/settings.py +110 -0
  55. flowerpower/utils/logging.py +21 -0
  56. flowerpower/utils/misc.py +57 -9
  57. flowerpower/utils/sql.py +122 -24
  58. flowerpower/utils/templates.py +18 -142
  59. flowerpower/web/app.py +0 -0
  60. flowerpower-1.0.0b1.dist-info/METADATA +324 -0
  61. flowerpower-1.0.0b1.dist-info/RECORD +94 -0
  62. {flowerpower-0.9.12.4.dist-info → flowerpower-1.0.0b1.dist-info}/WHEEL +1 -1
  63. flowerpower/cfg/pipeline/tracker.py +0 -14
  64. flowerpower/cfg/project/open_telemetry.py +0 -8
  65. flowerpower/cfg/project/tracker.py +0 -11
  66. flowerpower/cfg/project/worker.py +0 -19
  67. flowerpower/cli/scheduler.py +0 -309
  68. flowerpower/event_handler.py +0 -23
  69. flowerpower/mqtt.py +0 -525
  70. flowerpower/pipeline.py +0 -2419
  71. flowerpower/scheduler.py +0 -680
  72. flowerpower/tui.py +0 -79
  73. flowerpower/utils/datastore.py +0 -186
  74. flowerpower/utils/eventbroker.py +0 -127
  75. flowerpower/utils/executor.py +0 -58
  76. flowerpower/utils/trigger.py +0 -140
  77. flowerpower-0.9.12.4.dist-info/METADATA +0 -575
  78. flowerpower-0.9.12.4.dist-info/RECORD +0 -70
  79. /flowerpower/{cfg/pipeline/params.py → cli/worker.py} +0 -0
  80. {flowerpower-0.9.12.4.dist-info → flowerpower-1.0.0b1.dist-info}/entry_points.txt +0 -0
  81. {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
+