FlowerPower 0.20.0__py3-none-any.whl → 0.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. flowerpower/__init__.py +2 -6
  2. flowerpower/cfg/__init__.py +4 -11
  3. flowerpower/cfg/base.py +29 -25
  4. flowerpower/cfg/pipeline/__init__.py +3 -3
  5. flowerpower/cfg/pipeline/_schedule.py +32 -0
  6. flowerpower/cfg/pipeline/adapter.py +0 -5
  7. flowerpower/cfg/pipeline/builder.py +377 -0
  8. flowerpower/cfg/pipeline/run.py +89 -0
  9. flowerpower/cfg/project/__init__.py +8 -21
  10. flowerpower/cfg/project/adapter.py +0 -12
  11. flowerpower/cli/__init__.py +2 -28
  12. flowerpower/cli/pipeline.py +10 -4
  13. flowerpower/flowerpower.py +275 -585
  14. flowerpower/pipeline/base.py +19 -10
  15. flowerpower/pipeline/io.py +52 -46
  16. flowerpower/pipeline/manager.py +149 -91
  17. flowerpower/pipeline/pipeline.py +159 -87
  18. flowerpower/pipeline/registry.py +68 -33
  19. flowerpower/pipeline/visualizer.py +4 -4
  20. flowerpower/plugins/{_io → io}/__init__.py +1 -1
  21. flowerpower/settings/__init__.py +0 -2
  22. flowerpower/settings/{backend.py → _backend.py} +0 -19
  23. flowerpower/settings/logging.py +1 -1
  24. flowerpower/utils/logging.py +24 -12
  25. flowerpower/utils/misc.py +17 -0
  26. flowerpower-0.30.0.dist-info/METADATA +451 -0
  27. flowerpower-0.30.0.dist-info/RECORD +42 -0
  28. flowerpower/cfg/pipeline/schedule.py +0 -74
  29. flowerpower/cfg/project/job_queue.py +0 -111
  30. flowerpower/cli/job_queue.py +0 -1329
  31. flowerpower/cli/mqtt.py +0 -174
  32. flowerpower/job_queue/__init__.py +0 -205
  33. flowerpower/job_queue/base.py +0 -611
  34. flowerpower/job_queue/rq/__init__.py +0 -10
  35. flowerpower/job_queue/rq/_trigger.py +0 -37
  36. flowerpower/job_queue/rq/concurrent_workers/gevent_worker.py +0 -226
  37. flowerpower/job_queue/rq/concurrent_workers/thread_worker.py +0 -228
  38. flowerpower/job_queue/rq/manager.py +0 -1893
  39. flowerpower/job_queue/rq/setup.py +0 -154
  40. flowerpower/job_queue/rq/utils.py +0 -69
  41. flowerpower/mqtt.py +0 -12
  42. flowerpower/plugins/mqtt/__init__.py +0 -12
  43. flowerpower/plugins/mqtt/cfg.py +0 -17
  44. flowerpower/plugins/mqtt/manager.py +0 -962
  45. flowerpower/settings/job_queue.py +0 -31
  46. flowerpower-0.20.0.dist-info/METADATA +0 -693
  47. flowerpower-0.20.0.dist-info/RECORD +0 -58
  48. {flowerpower-0.20.0.dist-info → flowerpower-0.30.0.dist-info}/WHEEL +0 -0
  49. {flowerpower-0.20.0.dist-info → flowerpower-0.30.0.dist-info}/entry_points.txt +0 -0
  50. {flowerpower-0.20.0.dist-info → flowerpower-0.30.0.dist-info}/licenses/LICENSE +0 -0
  51. {flowerpower-0.20.0.dist-info → flowerpower-0.30.0.dist-info}/top_level.txt +0 -0
@@ -1,962 +0,0 @@
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 Any, Callable
8
-
9
- import mmh3
10
- from fsspec_utils import AbstractFileSystem, BaseStorageOptions, filesystem
11
- from loguru import logger
12
- from munch import Munch
13
- from paho.mqtt.client import (MQTT_ERR_SUCCESS, CallbackAPIVersion, Client,
14
- MQTTMessageInfo)
15
- from paho.mqtt.reasoncodes import ReasonCode
16
-
17
- from ...cfg import ProjectConfig
18
- from ...cfg.pipeline.run import ExecutorConfig, WithAdapterConfig
19
- from ...cfg.project.adapter import AdapterConfig
20
- from ...pipeline.manager import PipelineManager
21
- from ...utils.logging import setup_logging
22
- from .cfg import MqttConfig
23
-
24
- setup_logging()
25
-
26
-
27
- class MqttManager:
28
- def __init__(
29
- self,
30
- username: str | None = None,
31
- password: str | None = None,
32
- host: str | None = "localhost",
33
- port: int | None = 1883,
34
- topic: str | None = None,
35
- first_reconnect_delay: int = 1,
36
- max_reconnect_count: int = 5,
37
- reconnect_rate: int = 2,
38
- max_reconnect_delay: int = 60,
39
- transport: str = "tcp",
40
- clean_session: bool = True,
41
- client_id: str | None = None,
42
- client_id_suffix: str | None = None,
43
- **kwargs,
44
- ):
45
- if "user" in kwargs:
46
- username = kwargs["user"]
47
- if "pw" in kwargs:
48
- password = kwargs["pw"]
49
-
50
- self.topic = topic
51
-
52
- self._username = username
53
- self._password = password
54
- self._host = host
55
- self._port = port
56
- self._first_reconnect_delay = first_reconnect_delay
57
- self._max_reconnect_count = max_reconnect_count
58
- self._reconnect_rate = reconnect_rate
59
- self._max_reconnect_delay = max_reconnect_delay
60
- self._transport = transport
61
-
62
- self._clean_session = clean_session
63
- self._client_id = client_id
64
- self._client_id_suffix = client_id_suffix
65
-
66
- self._client = None
67
-
68
- @classmethod
69
- def from_event_broker(cls, base_dir: str | None = None):
70
- """Create a MqttManager instance from the event broker configuration.
71
-
72
- Args:
73
- base_dir (str | None): Base directory for the project. If None, uses the current working directory.
74
-
75
- Returns:
76
- MqttManager: An instance of MqttManager configured with the event broker settings.
77
- """
78
- base_dir = base_dir or str(Path.cwd())
79
-
80
- jq_backend = ProjectConfig.load(base_dir=base_dir).job_queue.backend
81
- if jq_backend is None:
82
- raise ValueError(
83
- "No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
84
- "If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
85
- "or use the provide a configuration dict to the MQTTManager.from_dict() method."
86
- )
87
- if hasattr(jq_backend, "event_broker") is False:
88
- raise ValueError(
89
- "No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
90
- "If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
91
- "or use the provide a configuration dict to the MQTTManager.from_dict() method."
92
- )
93
- if jq_backend.event_broker.type != "mqtt":
94
- raise ValueError(
95
- "No MQTT event broker configuration found. Recheck the provided `base_dir`.\n"
96
- "If you are not using a MQTT event broker, initialize the MQTT client using the MQTTManager class.\n"
97
- "or use the provide a configuration dict to the MQTTManager.from_dict() method."
98
- )
99
- else:
100
- event_broker_cfg = jq_backend.event_broker
101
- return cls(
102
- **event_broker_cfg.dict(),
103
- )
104
-
105
- @classmethod
106
- def from_config(
107
- cls,
108
- cfg: MqttConfig | None = None,
109
- path: str | None = None,
110
- fs: AbstractFileSystem | None = None,
111
- storage_options: dict | BaseStorageOptions = {},
112
- ):
113
- """Create a MqttManager instance from the provided configuration.
114
-
115
- Args:
116
- cfg (MqttConfig | None): Configuration for the MQTT client. If None, loads from the provided path.
117
- path (str | None): Path to the configuration file. If None, uses the default configuration.
118
- fs (AbstractFileSystem | None): File system to use for loading the configuration.
119
- storage_options (dict | BaseStorageOptions): Storage options for the file system.
120
-
121
- Returns:
122
- MqttManager: An instance of MqttManager configured with the provided settings.
123
- """
124
- if cfg is None:
125
- if path is None:
126
- raise ValueError(
127
- "No configuration provided. Please provide `config` or `path` to the configuration file."
128
- )
129
-
130
- if cfg is None:
131
- import os
132
-
133
- if fs is None:
134
- fs = filesystem(
135
- protocol_or_path=os.path.dirname(path),
136
- storage_options=storage_options,
137
- )
138
-
139
- cfg = MqttConfig.from_yaml(path=os.path.basename(path), fs=fs)
140
-
141
- return cls(
142
- **cfg.dict(),
143
- )
144
-
145
- @classmethod
146
- def from_dict(cls, cfg: dict):
147
- """Create a MqttManager instance from the provided dictionary configuration.
148
-
149
- Args:
150
- cfg (dict): Dictionary containing the configuration for the MQTT client.
151
-
152
- Returns:
153
- MqttManager: An instance of MqttManager configured with the provided settings.
154
- """
155
- return cls(
156
- **cfg,
157
- )
158
-
159
- def __enter__(self) -> "MqttManager":
160
- self.connect()
161
- return self
162
-
163
- def __exit__(
164
- self,
165
- exc_type: type[BaseException] | None,
166
- exc_val: BaseException | None,
167
- exc_tb: TracebackType | None,
168
- ) -> None:
169
- # Add any cleanup code here if needed
170
- self.disconnect()
171
-
172
- @staticmethod
173
- def _on_connect(client, userdata, flags, rc, properties):
174
- if rc == 0:
175
- logger.info(f"Connected to MQTT Broker {userdata.host}!")
176
- logger.info(
177
- f"Connected as {userdata.client_id} with clean session {userdata.clean_session}"
178
- )
179
- else:
180
- logger.error(f"Failed to connect, return code {rc}")
181
-
182
- @staticmethod
183
- def _on_disconnect(client, userdata, disconnect_flags, rc, properties=None):
184
- reconnect_count, reconnect_delay = 0, userdata.first_reconnect_delay
185
-
186
- if userdata.max_reconnect_count == 0:
187
- logger.info("Disconnected successfully!")
188
- return
189
-
190
- while reconnect_count < userdata.max_reconnect_count:
191
- logger.info(f"Reconnecting in {reconnect_delay} seconds...")
192
- time.sleep(reconnect_delay)
193
-
194
- try:
195
- client.reconnect()
196
- logger.info("Reconnected successfully!")
197
- return
198
- except Exception as err:
199
- logger.error(f"{err}. Reconnect failed. Retrying...")
200
-
201
- reconnect_delay *= userdata.reconnect_rate
202
- reconnect_delay = min(reconnect_delay, userdata.max_reconnect_delay)
203
- reconnect_count += 1
204
- logger.info(f"Reconnect failed after {reconnect_count} attempts. Exiting...")
205
-
206
- # @staticmethod
207
- # def _on_publish(client, userdata, mid, rc, properties):
208
- # logger.info(f"Published message id: {mid} with return code: {rc}")
209
- # if rc == 0:
210
- # logger.debug("Message published successfully!")
211
- # else:
212
- # logger.debug(f"Failed to publish message with return code: {rc}")
213
-
214
- @staticmethod
215
- def _on_publish(client, userdata, mid, reason_code_obj, properties):
216
- """Callback function for when a message is published."""
217
- if isinstance(reason_code_obj, ReasonCode):
218
- if not reason_code_obj.is_failure:
219
- logger.info(
220
- f"Broker acknowledged message_id: {mid} (ReasonCode: {reason_code_obj})"
221
- )
222
- else:
223
- if reason_code_obj.value == 16: # MQTTReasonCode.NoMatchingSubscribers
224
- logger.warning(
225
- f"Message_id: {mid} published, but broker reported no matching subscribers (ReasonCode: {reason_code_obj})."
226
- )
227
- else:
228
- logger.error(
229
- f"Broker acknowledgment error for message_id: {mid}. ReasonCode: {reason_code_obj}"
230
- )
231
- elif isinstance(reason_code_obj, int): # Fallback for simpler acks (legacy RCs)
232
- if reason_code_obj == 0: # MQTT_ERR_SUCCESS
233
- logger.info(
234
- f"Broker acknowledged message_id: {mid} (Legacy RC: {reason_code_obj})"
235
- )
236
- else:
237
- logger.error(
238
- f"Broker acknowledgment error for message_id: {mid}. Legacy RC: {reason_code_obj} ({client.error_string(reason_code_obj)})"
239
- )
240
- else:
241
- logger.warning(
242
- f"Message_id: {mid} published. Received unusual RC type in on_publish: {reason_code_obj} (Type: {type(reason_code_obj)})"
243
- )
244
-
245
- @staticmethod
246
- def _on_subscribe(client, userdata, mid, qos, properties):
247
- if isinstance(qos, list):
248
- qos_msg = str(qos[0])
249
- else:
250
- qos_msg = f"and granted QoS {qos[0]}"
251
- logger.info(f"Subscribed {qos_msg}")
252
-
253
- def connect(self) -> Client:
254
- """Connect to the MQTT broker.
255
- Returns:
256
- Client: The connected MQTT client.
257
- """
258
- if self._client_id is None and self._clean_session:
259
- # Random Client ID when clean session is True
260
- self._client_id = f"flowerpower-client-{random.randint(0, 10000)}"
261
- elif self._client_id is None and not self._clean_session:
262
- # Deterministic Client ID when clean session is False
263
- self._client_id = f"flowerpower-client-{
264
- mmh3.hash_bytes(
265
- str(self._host)
266
- + str(self._port)
267
- + str(self.topic)
268
- + str(socket.gethostname())
269
- ).hex()
270
- }"
271
-
272
- if self._client_id_suffix:
273
- self._client_id = f"{self._client_id}-{self._client_id_suffix}"
274
-
275
- logger.debug(
276
- f"Client ID: {self._client_id} - Clean session: {self._clean_session}"
277
- )
278
- client = Client(
279
- CallbackAPIVersion.VERSION2,
280
- client_id=self._client_id,
281
- transport=self._transport,
282
- clean_session=self._clean_session,
283
- userdata=Munch(
284
- user=self._username,
285
- pw=self._password,
286
- host=self._host,
287
- port=self._port,
288
- topic=self.topic,
289
- first_reconnect_delay=self._first_reconnect_delay,
290
- max_reconnect_count=self._max_reconnect_count,
291
- reconnect_rate=self._reconnect_rate,
292
- max_reconnect_delay=self._max_reconnect_delay,
293
- transport=self._transport,
294
- client_id=self._client_id,
295
- clean_session=self._clean_session,
296
- ),
297
- )
298
- if self._password != "" and self._username != "":
299
- client.username_pw_set(self._username, self._password)
300
-
301
- client.on_connect = self._on_connect # self._on_connect
302
- client.on_disconnect = self._on_disconnect # self._on_disconnect
303
- client.on_publish = self._on_publish
304
- client.on_subscribe = self._on_subscribe
305
-
306
- client.connect(self._host, self._port)
307
- self._client = client
308
- # topic = topic or topic
309
- if self.topic:
310
- self.subscribe()
311
-
312
- def disconnect(self):
313
- """Disconnect from the MQTT broker."""
314
- if self._client is None:
315
- logger.warning("Client is not connected. Cannot disconnect.")
316
- return
317
- self._max_reconnect_count = 0
318
- self._client._userdata.max_reconnect_count = 0
319
- self._client.disconnect()
320
-
321
- def reconnect(self):
322
- """Reconnect to the MQTT broker."""
323
- if self._client is None:
324
- logger.warning("Client is not connected. Connecting instead.")
325
- self.connect()
326
- else:
327
- self._client.reconnect()
328
-
329
- def publish(
330
- self, topic: str, payload: Any, qos: int = 0, retain: bool = False
331
- ) -> "MQTTMessageInfo | None":
332
- """
333
- Publish a message to the MQTT broker.
334
-
335
- Args:
336
- topic (str): The topic to publish the message to.
337
- payload (Any): The message payload.
338
- qos (int, optional): The Quality of Service level. Defaults to 0.
339
- retain (bool, optional): Whether to retain the message. Defaults to False.
340
-
341
- Returns:
342
- MQTTMessageInfo | None: Information about the published message, or None if an error occurred.
343
- """
344
- if self._client is None or not self._client.is_connected():
345
- logger.warning(
346
- "Client is not connected. Attempting to connect before publishing."
347
- )
348
- try:
349
- self.connect()
350
- except Exception as e:
351
- logger.error(f"Failed to connect to MQTT broker before publishing: {e}")
352
- return None
353
-
354
- try:
355
- msg_info = self._client.publish(
356
- topic=topic, payload=payload, qos=qos, retain=retain
357
- )
358
- if msg_info.rc == MQTT_ERR_SUCCESS: # 0:
359
- logger.debug(
360
- f"Message published successfully. MID: {msg_info.mid}, Topic: {topic}, QoS: {qos}, Retain: {retain}, RC: {msg_info.rc}"
361
- )
362
- else:
363
- logger.error(
364
- f"Failed to publish message. Topic: {topic}, QoS: {qos}, Retain: {retain}, RC: {msg_info.rc}, Error: {self._client.error_string(msg_info.rc)}"
365
- )
366
- return msg_info
367
- except Exception as e:
368
- logger.error(
369
- f"An error occurred while publishing message to topic {topic}: {e}"
370
- )
371
- return None
372
-
373
- def subscribe(self, topic: str | None = None, qos: int = 2):
374
- """
375
- Subscribe to a topic on the MQTT broker.
376
-
377
- Args:
378
- topic (str | None): The topic to subscribe to. If None, uses the instance's topic.
379
- qos (int): The Quality of Service level.
380
- """
381
- if self._client is None or not self._client.is_connected():
382
- self.connect()
383
- if topic is not None:
384
- self.topic = topic
385
- self._client.subscribe(self.topic, qos=qos)
386
-
387
- def unsubscribe(self, topic: str | None = None):
388
- """
389
- Unsubscribe from a topic on the MQTT broker.
390
-
391
- Args:
392
- topic (str | None): The topic to unsubscribe from. If None, uses the instance's topic.
393
- """
394
- if self._client is None or not self._client.is_connected():
395
- self.connect()
396
- if topic is not None:
397
- self.topic = topic
398
- self._client.unsubscribe(self.topic)
399
-
400
- def register_on_message(self, on_message: Callable):
401
- """
402
- Register a callback function to be called when a message is received.
403
-
404
- Args:
405
- on_message (Callable): The callback function to register.
406
- """
407
- if self._client is None or not self._client.is_connected():
408
- self.connect()
409
- self._client.on_message = on_message
410
-
411
- def run_in_background(
412
- self,
413
- on_message: Callable,
414
- topic: str | None = None,
415
- qos: int = 2,
416
- ) -> None:
417
- """
418
- Run the MQTT client in the background.
419
-
420
- Args:
421
- on_message: Callback function to run when a message is received
422
- topic: MQTT topic to listen to
423
-
424
- Returns:
425
- None
426
-
427
-
428
- """
429
- if self._client is None or not self._client.is_connected():
430
- self.connect()
431
-
432
- if topic:
433
- self.subscribe(topic, qos=qos)
434
-
435
- self._client.on_message = on_message
436
- self._client.loop_start()
437
-
438
- def run_until_break(
439
- self,
440
- on_message: Callable,
441
- topic: str | None = None,
442
- qos: int = 2,
443
- ):
444
- """
445
- Run the MQTT client until a break signal is received.
446
-
447
- Args:
448
- on_message: Callback function to run when a message is received
449
- topic: MQTT topic to listen to
450
-
451
- Returns:
452
- None
453
- """
454
- if self._client is None or not self._client.is_connected():
455
- self.connect()
456
-
457
- if topic:
458
- self.subscribe(topic, qos=qos)
459
-
460
- self._client.on_message = on_message
461
- self._client.loop_forever()
462
-
463
- def start_listener(
464
- self,
465
- on_message: Callable,
466
- topic: str | None = None,
467
- background: bool = False,
468
- qos: int = 2,
469
- ) -> None:
470
- """
471
- Start the MQTT listener.
472
-
473
- Args:
474
- on_message: Callback function to run when a message is received
475
- topic: MQTT topic to listen to
476
- background: Run the listener in the background
477
-
478
- Returns:
479
- None
480
- """
481
- if background:
482
- self.run_in_background(on_message, topic, qos)
483
- else:
484
- self.run_until_break(on_message, topic, qos)
485
-
486
- def stop_listener(
487
- self,
488
- ) -> None:
489
- """
490
- Stop the MQTT listener.
491
-
492
- Returns:
493
- None
494
- """
495
- self._client.loop_stop()
496
- logger.info("Client stopped.")
497
-
498
- # def _run_pipeline(self, **kwargs):
499
- # """
500
- # Internal method to run a pipeline on a message.
501
- # This method is called by the `run_pipeline_on_message` method.
502
- # """
503
- # # This method is intentionally left empty. It is meant to be overridden
504
- # # by the `run_pipeline_on_message` method.
505
- # pm.run(self, **kwargs)
506
-
507
- def run_pipeline_on_message(
508
- self,
509
- name: str,
510
- topic: str | None = None,
511
- inputs: dict | None = None,
512
- final_vars: list | None = None,
513
- config: dict | None = None,
514
- cache: bool | dict = False,
515
- executor_cfg: str | dict | ExecutorConfig | None = None,
516
- with_adapter_cfg: dict | WithAdapterConfig | None = None,
517
- pipeline_adapter_cfg: dict | AdapterConfig | None = None,
518
- project_adapter_cfg: dict | AdapterConfig | None = None,
519
- adapter: dict[str, Any] | None = None,
520
- reload: bool = False,
521
- log_level: str | None = None,
522
- result_ttl: float | dt.timedelta = 0,
523
- run_in: int | str | dt.timedelta | None = None,
524
- max_retries: int | None = None,
525
- retry_delay: float | None = None,
526
- jitter_factor: float | None = None,
527
- retry_exceptions: tuple | list | None = None,
528
- as_job: bool = False,
529
- base_dir: str | None = None,
530
- storage_options: dict = {},
531
- fs: AbstractFileSystem | None = None,
532
- background: bool = False,
533
- qos: int = 2,
534
- config_hook: Callable[[bytes, str], dict] | None = None,
535
- on_success: Callable | tuple[Callable, tuple | None, dict | None] | None = None,
536
- on_failure: Callable | tuple[Callable, tuple | None, dict | None] | None = None,
537
- on_success_pipeline: Callable
538
- | tuple[Callable, tuple | None, dict | None]
539
- | None = None,
540
- on_failure_pipeline: Callable
541
- | tuple[Callable, tuple | None, dict | None]
542
- | None = None,
543
- **kwargs,
544
- ):
545
- """
546
- Start a pipeline listener that listens to a topic and processes the message using a pipeline.
547
-
548
- Args:
549
- name (str): Name of the pipeline
550
- topic (str | None): MQTT topic to listen to
551
- inputs (dict | None): Inputs for the pipeline
552
- final_vars (list | None): Final variables for the pipeline
553
- config (dict | None): Configuration for the pipeline driver
554
- cache (bool | dict): Cache for the pipeline
555
- executor_cfg (str | dict | ExecutorConfig | None): Executor configuration
556
- with_adapter_cfg (dict | WithAdapterConfig | None): With adapter configuration
557
- pipeline_adapter_cfg (dict | AdapterConfig | None): Pipeline adapter configuration
558
- project_adapter_cfg (dict | AdapterConfig | None): Project adapter configuration
559
- adapter (dict[str, Any] | None): Adapter configuration
560
- reload (bool): Reload the pipeline
561
- log_level (str | None): Log level for the pipeline
562
- result_ttl (float | dt.timedelta): Result expiration time for the pipeline
563
- run_in (int | str | dt.timedelta | None): Run in time for the pipeline
564
- max_retries (int | None): Maximum number of retries for the pipeline
565
- retry_delay (float | None): Delay between retries for the pipeline
566
- jitter_factor (float | None): Jitter factor for the pipeline
567
- retry_exceptions (tuple | list | None): Exceptions to retry for the pipeline
568
- as_job (bool): Run the pipeline as a job
569
- base_dir (str | None): Base directory for the pipeline
570
- storage_options (dict): Storage options for the pipeline
571
- fs (AbstractFileSystem | None): File system for the pipeline
572
- background (bool): Run the listener in the background
573
- qos (int): Quality of Service for the MQTT client
574
- config_hook (Callable[[bytes, str], dict] | None): Hook function to modify the configuration of the pipeline
575
- on_success (Callable | tuple[Callable, tuple | None, dict | None] | None): Callback function for successful job creation
576
- on_failure (Callable | tuple[Callable, tuple | None, dict | None] | None): Callback function for failed job creation
577
- on_success_pipeline (Callable | tuple[Callable, tuple | None, dict | None] | None): Callback function for successful pipeline run
578
- on_failure_pipeline (Callable | tuple[Callable, tuple | None, dict | None] | None): Callback function for failed pipeline run
579
- **kwargs: Additional keyword arguments
580
-
581
- Returns:
582
- None
583
-
584
- Raises:
585
- ValueError: If the config_hook is not callable
586
-
587
- Example:
588
- ```python
589
- from flowerpower.plugins.mqtt import MqttManager
590
- mqtt = MqttManager()
591
- mqtt.run_pipeline_on_message(
592
- name="my_pipeline",
593
- topic="my_topic",
594
- inputs={"key": "value"},
595
- config={"param": "value"},
596
- as_job=True,
597
- )
598
- ```
599
- """
600
-
601
- if inputs is None:
602
- inputs = {}
603
-
604
- if config is None:
605
- config = {}
606
-
607
- if config_hook is not None and not callable(config_hook):
608
- raise ValueError("config_hook must be a callable function")
609
-
610
- def on_message(client, userdata, msg):
611
- # logger.info(f"Received message on topic {topic}")
612
- logger.info(
613
- f"Received message on subscribed topic {topic} (exact topic {msg.topic})"
614
- )
615
-
616
- inputs["payload"] = msg.payload
617
- inputs["topic"] = msg.topic
618
-
619
- if config_hook is not None:
620
- # config_ = config_hook(inputs["payload"], inputs["topic"])
621
- try:
622
- config_ = config_hook(inputs["payload"], inputs["topic"])
623
- logger.debug(f"Config from hook: {config_}")
624
- except Exception as e:
625
- # _ = e
626
- logger.warning("Config hook failed. Aborting Message processing")
627
- logger.exception(e)
628
- return
629
-
630
- if any([k in config_ for k in config.keys()]):
631
- logger.warning("Config from hook overwrites config from pipeline")
632
-
633
- config.update(config_)
634
- logger.debug(f"Config after update: {config}")
635
-
636
- with PipelineManager(
637
- storage_options=storage_options, fs=fs, base_dir=base_dir
638
- ) as pipeline:
639
- if as_job:
640
- pipeline.add_job(
641
- name=name,
642
- inputs=inputs,
643
- final_vars=final_vars,
644
- config=config,
645
- cache=cache,
646
- executor_cfg=executor_cfg,
647
- with_adapter_cfg=with_adapter_cfg,
648
- pipeline_adapter_cfg=pipeline_adapter_cfg,
649
- project_adapter_cfg=project_adapter_cfg,
650
- adapter=adapter,
651
- run_in=run_in,
652
- reload=reload,
653
- log_level=log_level,
654
- result_ttl=result_ttl,
655
- max_retries=max_retries,
656
- retry_delay=retry_delay,
657
- jitter_factor=jitter_factor,
658
- retry_exceptions=retry_exceptions,
659
- on_failure=on_failure,
660
- on_success=on_success,
661
- on_failure_pipeline=on_failure_pipeline,
662
- on_success_pipeline=on_success_pipeline,
663
- **kwargs,
664
- )
665
-
666
- else:
667
- pipeline.run(
668
- name=name,
669
- inputs=inputs,
670
- final_vars=final_vars,
671
- config=config,
672
- cache=cache,
673
- executor_cfg=executor_cfg,
674
- with_adapter_cfg=with_adapter_cfg,
675
- pipeline_adapter_cfg=pipeline_adapter_cfg,
676
- project_adapter_cfg=project_adapter_cfg,
677
- adapter=adapter,
678
- reload=reload,
679
- log_level=log_level,
680
- max_retries=max_retries,
681
- retry_delay=retry_delay,
682
- jitter_factor=jitter_factor,
683
- retry_exceptions=retry_exceptions,
684
- on_failure=on_failure_pipeline,
685
- on_success=on_success_pipeline,
686
- )
687
-
688
- self.start_listener(
689
- on_message=on_message, topic=topic, background=background, qos=qos
690
- )
691
-
692
-
693
- def start_listener(
694
- on_message: Callable,
695
- topic: str | None = None,
696
- background: bool = False,
697
- mqtt_cfg: dict | MqttConfig = {},
698
- base_dir: str | None = None,
699
- username: str | None = None,
700
- password: str | None = None,
701
- host: str | None = None,
702
- port: int | None = None,
703
- clean_session: bool = True,
704
- qos: int = 2,
705
- client_id: str | None = None,
706
- client_id_suffix: str | None = None,
707
- config_hook: Callable[[bytes, str], dict] | None = None,
708
- **kwargs,
709
- ) -> None:
710
- """
711
- Start the MQTT listener.
712
-
713
- The connection to the MQTT broker is established using the provided configuration of a
714
- MQTT event broker defined in the project configuration file `conf/project.toml`.
715
- If no configuration is found, you have to provide either the argument `mqtt_cfg`, dict with the
716
- connection parameters or the arguments `username`, `password`, `host`, and `port`.
717
-
718
- Args:
719
- on_message (Callable): Callback function to run when a message is received
720
- topic (str | None): MQTT topic to listen to
721
- background (bool): Run the listener in the background
722
- mqtt_cfg (dict | MqttConfig): MQTT client configuration. Use either this or arguments
723
- username, password, host, and port.
724
- base_dir (str | None): Base directory for the module
725
- username (str | None): Username for the MQTT client
726
- password (str | None): Password for the MQTT client
727
- host (str | None): Host for the MQTT client
728
- port (int | None): Port for the MQTT client
729
- clean_session (bool): Clean session flag for the MQTT client
730
- qos (int): Quality of Service for the MQTT client
731
- client_id (str | None): Client ID for the MQTT client
732
- client_id_suffix (str | None): Client ID suffix for the MQTT client
733
- config_hook (Callable[[bytes, str], dict] | None): Hook function to modify the configuration of the pipeline
734
- **kwargs: Additional keyword arguments
735
-
736
- Returns:
737
- None
738
-
739
- Raises:
740
- ValueError: If the config_hook is not callable
741
- ValueError: If no client configuration is found
742
-
743
- Example:
744
- ```python
745
- from flowerpower.plugins.mqtt import start_listener
746
-
747
- start_listener(
748
- on_message=my_on_message_function,
749
- topic="my_topic",
750
- background=True,
751
- mqtt_cfg={"host": "localhost", "port": 1883},
752
- )
753
- ```
754
- """
755
- try:
756
- client = MqttManager.from_event_broker(base_dir)
757
- except ValueError:
758
- if mqtt_cfg:
759
- if isinstance(mqtt_cfg, MqttConfig):
760
- client = MqttManager.from_config(mqtt_cfg)
761
- elif isinstance(mqtt_cfg, dict):
762
- client = MqttManager.from_dict(mqtt_cfg)
763
- elif host and port:
764
- client = MqttManager(
765
- username=username,
766
- password=password,
767
- host=host,
768
- port=port,
769
- clean_session=clean_session,
770
- client_id=client_id,
771
- client_id_suffix=client_id_suffix,
772
- config_hook=config_hook,
773
- **kwargs,
774
- )
775
- else:
776
- raise ValueError(
777
- "No client configuration found. Please provide a client configuration "
778
- "or a FlowerPower project base directory, in which a event broker is "
779
- "configured in the `config/project.yml` file."
780
- )
781
-
782
- client.start_listener(
783
- on_message=on_message, topic=topic, background=background, qos=qos
784
- )
785
-
786
-
787
- def run_pipeline_on_message(
788
- name: str,
789
- topic: str | None = None,
790
- inputs: dict | None = None,
791
- final_vars: list | None = None,
792
- config: dict | None = None,
793
- cache: bool | dict = False,
794
- executor_cfg: str | dict | ExecutorConfig | None = None,
795
- with_adapter_cfg: dict | WithAdapterConfig | None = None,
796
- pipeline_adapter_cfg: dict | AdapterConfig | None = None,
797
- project_adapter_cfg: dict | AdapterConfig | None = None,
798
- adapter: dict[str, Any] | None = None,
799
- reload: bool = False,
800
- log_level: str | None = None,
801
- result_ttl: float | dt.timedelta = 0,
802
- run_in: int | str | dt.timedelta | None = None,
803
- max_retries: int | None = None,
804
- retry_delay: float | None = None,
805
- jitter_factor: float | None = None,
806
- retry_exceptions: tuple | list | None = None,
807
- as_job: bool = False,
808
- base_dir: str | None = None,
809
- storage_options: dict = {},
810
- fs: AbstractFileSystem | None = None,
811
- background: bool = False,
812
- mqtt_cfg: dict | MqttConfig = {},
813
- host: str | None = None,
814
- port: int | None = None,
815
- username: str | None = None,
816
- password: str | None = None,
817
- clean_session: bool = True,
818
- qos: int = 2,
819
- client_id: str | None = None,
820
- client_id_suffix: str | None = None,
821
- config_hook: Callable[[bytes, str], dict] | None = None,
822
- **kwargs,
823
- ):
824
- """
825
- Start a pipeline listener that listens to a topic and processes the message using a pipeline.
826
-
827
- Args:
828
- name (str): Name of the pipeline
829
- topic (str | None): MQTT topic to listen to
830
- inputs (dict | None): Inputs for the pipeline
831
- final_vars (list | None): Final variables for the pipeline
832
- config (dict | None): Configuration for the pipeline driver
833
- cache (bool | dict): Cache for the pipeline
834
- executor_cfg (str | dict | ExecutorConfig | None): Executor configuration
835
- with_adapter_cfg (dict | WithAdapterConfig | None): With adapter configuration
836
- pipeline_adapter_cfg (dict | AdapterConfig | None): Pipeline adapter configuration
837
- project_adapter_cfg (dict | AdapterConfig | None): Project adapter configuration
838
- adapter (dict[str, Any] | None): Adapter configuration
839
- reload (bool): Reload the pipeline
840
- log_level (str | None): Log level for the pipeline
841
- result_ttl (float | dt.timedelta): Result expiration time for the pipeline
842
- run_in (int | str | dt.timedelta | None): Run in time for the pipeline
843
- max_retries (int | None): Maximum number of retries for the pipeline
844
- retry_delay (float | None): Delay between retries for the pipeline
845
- jitter_factor (float | None): Jitter factor for the pipeline
846
- retry_exceptions (tuple | list | None): Exceptions to retry for the pipeline
847
- as_job (bool): Run the pipeline as a job
848
- base_dir (str | None): Base directory for the pipeline
849
- storage_options (dict): Storage options for the pipeline
850
- fs (AbstractFileSystem | None): File system for the pipeline
851
- background (bool): Run the listener in the background
852
- mqtt_cfg (dict | MqttConfig): MQTT client configuration. Use either this or arguments
853
- username, password, host, and port.
854
- host (str | None): Host for the MQTT client
855
- port (int | None): Port for the MQTT client
856
- username (str | None): Username for the MQTT client
857
- password (str | None): Password for the MQTT client
858
- clean_session (bool): Clean session flag for the MQTT client
859
- qos (int): Quality of Service for the MQTT client
860
- client_id (str | None): Client ID for the MQTT client
861
- client_id_suffix (str | None): Client ID suffix for the MQTT client
862
- config_hook (Callable[[bytes, str], dict] | None): Hook function to modify the configuration of the pipeline
863
- **kwargs: Additional keyword arguments
864
-
865
- Returns:
866
- None
867
-
868
- Raises:
869
- ValueError: If the config_hook is not callable
870
- ValueError: If no client configuration is found
871
-
872
- Example:
873
- ```python
874
- from flowerpower.plugins.mqtt import run_pipeline_on_message
875
-
876
- run_pipeline_on_message(
877
- name="my_pipeline",
878
- topic="my_topic",
879
- inputs={"key": "value"},
880
- config={"param": "value"},
881
- as_job=True,
882
- )
883
- ```
884
- """
885
- try:
886
- client = MqttManager.from_event_broker(base_dir)
887
- except ValueError:
888
- if mqtt_cfg:
889
- if isinstance(mqtt_cfg, MqttConfig):
890
- client = MqttManager.from_config(mqtt_cfg)
891
- elif isinstance(mqtt_cfg, dict):
892
- client = MqttManager.from_dict(mqtt_cfg)
893
- elif host and port:
894
- client = MqttManager(
895
- username=username,
896
- password=password,
897
- host=host,
898
- port=port,
899
- clean_session=clean_session,
900
- client_id=client_id,
901
- client_id_suffix=client_id_suffix,
902
- config_hook=config_hook,
903
- **kwargs,
904
- )
905
- else:
906
- raise ValueError(
907
- "No client configuration found. Please provide a client configuration "
908
- "or a FlowerPower project base directory, in which a event broker is "
909
- "configured in the `config/project.yml` file."
910
- )
911
-
912
- if client._client_id is None and client_id is not None:
913
- client._client_id = client_id
914
-
915
- if client._client_id_suffix is None and client_id_suffix is not None:
916
- client._client_id_suffix = client_id_suffix
917
-
918
- """
919
- cli_clean_session | config_clean_session | result
920
- TRUE TRUE TRUE
921
- FALSE FALSE FALSE
922
- FALSE TRUE FALSE
923
- TRUE FALSE FALSE
924
-
925
- Clean session should only use default value if neither cli nor config source says otherwise
926
- """
927
- client._clean_session = client._clean_session and clean_session
928
-
929
- if client.topic is None and topic is not None:
930
- client.topic = topic
931
-
932
- client.run_pipeline_on_message(
933
- name=name,
934
- topic=topic,
935
- inputs=inputs,
936
- final_vars=final_vars,
937
- config=config,
938
- cache=cache,
939
- executor_cfg=executor_cfg,
940
- with_adapter_cfg=with_adapter_cfg,
941
- pipeline_adapter_cfg=pipeline_adapter_cfg,
942
- project_adapter_cfg=project_adapter_cfg,
943
- adapter=adapter,
944
- reload=reload,
945
- log_level=log_level,
946
- result_ttl=result_ttl,
947
- run_in=run_in,
948
- max_retries=max_retries,
949
- retry_delay=retry_delay,
950
- jitter_factor=jitter_factor,
951
- retry_exceptions=retry_exceptions,
952
- as_job=as_job,
953
- base_dir=base_dir,
954
- storage_options=storage_options,
955
- fs=fs,
956
- background=background,
957
- qos=qos,
958
- client_id=client_id,
959
- client_id_suffix=client_id_suffix,
960
- config_hook=config_hook,
961
- **kwargs,
962
- )