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,560 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Pipeline Registry for discovery, listing, creation, and deletion."""
3
+ import os
4
+ import datetime as dt
5
+ import posixpath
6
+ from typing import TYPE_CHECKING
7
+
8
+ import rich
9
+ from loguru import logger
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.syntax import Syntax
13
+ from rich.table import Table
14
+ from rich.tree import Tree
15
+
16
+ from .. import settings
17
+ # Import necessary config types and utility functions
18
+ from ..cfg import PipelineConfig, ProjectConfig
19
+ from ..fs import AbstractFileSystem
20
+ from ..utils.logging import setup_logging
21
+ # Assuming view_img might be used indirectly or needed later
22
+ from ..utils.templates import PIPELINE_PY_TEMPLATE, HOOK_TEMPLATE__MQTT_BUILD_CONFIG
23
+
24
+ if TYPE_CHECKING:
25
+ # Keep this for type hinting if needed elsewhere, though Config is imported directly now
26
+ pass
27
+
28
+ from enum import Enum
29
+
30
+ class HookType(str, Enum):
31
+ MQTT_BUILD_CONFIG = "mqtt-build-config"
32
+
33
+ def default_function_name(self) -> str:
34
+ match self.value:
35
+ case HookType.MQTT_BUILD_CONFIG:
36
+ return self.value.replace("-", "_")
37
+ case _:
38
+ return self.value
39
+
40
+ def __str__(self) -> str:
41
+ return self.value
42
+
43
+ setup_logging(level=settings.LOG_LEVEL)
44
+
45
+
46
+ class PipelineRegistry:
47
+ """Manages discovery, listing, creation, and deletion of pipelines."""
48
+
49
+ def __init__(
50
+ self,
51
+ project_cfg: ProjectConfig,
52
+ fs: AbstractFileSystem,
53
+ cfg_dir: str,
54
+ pipelines_dir: str,
55
+ ):
56
+ """
57
+ Initializes the PipelineRegistry.
58
+
59
+ Args:
60
+ project_cfg: The project configuration object.
61
+ fs: The filesystem instance.
62
+ cfg_dir: The configuration directory path.
63
+ pipelines_dir: The pipelines directory path.
64
+ """
65
+ self.project_cfg = project_cfg
66
+ self._fs = fs
67
+ self._cfg_dir = cfg_dir
68
+ self._pipelines_dir = pipelines_dir
69
+ self._console = Console()
70
+
71
+ # --- Methods moved from PipelineManager ---
72
+ def new(self, name: str, overwrite: bool = False):
73
+ """
74
+ Adds a pipeline with the given name.
75
+
76
+ Args:
77
+ name (str): The name of the pipeline.
78
+ overwrite (bool): Whether to overwrite an existing pipeline. Defaults to False.
79
+ job_queue_type (str | None): The type of worker to use. Defaults to None.
80
+
81
+ Raises:
82
+ ValueError: If the configuration or pipeline path does not exist, or if the pipeline already exists.
83
+
84
+ Examples:
85
+ >>> pm = PipelineManager()
86
+ >>> pm.new("my_pipeline")
87
+ """
88
+ # Use attributes derived from self.project_cfg
89
+ for dir_path, label in (
90
+ (self._cfg_dir, "configuration"),
91
+ (self._pipelines_dir, "pipeline"),
92
+ ):
93
+ if not self._fs.exists(dir_path):
94
+ raise ValueError(
95
+ f"{label.capitalize()} path {dir_path} does not exist. Please run flowerpower init first."
96
+ )
97
+
98
+ formatted_name = name.replace(".", "/").replace("-", "_")
99
+ pipeline_file = posixpath.join(self._pipelines_dir, f"{formatted_name}.py")
100
+ cfg_file = posixpath.join(self._cfg_dir, "pipelines", f"{formatted_name}.yml")
101
+
102
+ def check_and_handle(path: str):
103
+ if self._fs.exists(path):
104
+ if overwrite:
105
+ self._fs.rm(path)
106
+ else:
107
+ raise ValueError(
108
+ f"Pipeline {self.project_cfg.name}.{formatted_name} already exists. Use `overwrite=True` to overwrite."
109
+ )
110
+
111
+ check_and_handle(pipeline_file)
112
+ check_and_handle(cfg_file)
113
+
114
+ # Ensure directories for the new files exist
115
+ for file_path in (pipeline_file, cfg_file):
116
+ self._fs.makedirs(file_path.rsplit("/", 1)[0], exist_ok=True)
117
+
118
+ # Write pipeline code template
119
+ with self._fs.open(pipeline_file, "w") as f:
120
+ f.write(
121
+ PIPELINE_PY_TEMPLATE.format(
122
+ name=name,
123
+ date=dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
124
+ )
125
+ )
126
+
127
+ # Create default pipeline config and save it directly
128
+ new_pipeline_cfg = PipelineConfig(name=name)
129
+ new_pipeline_cfg.save(fs=self._fs) # Save only the pipeline part
130
+
131
+ rich.print(
132
+ f"🔧 Created new pipeline [bold blue]{self.project_cfg.name}.{name}[/bold blue]"
133
+ )
134
+
135
+ def delete(self, name: str, cfg: bool = True, module: bool = False):
136
+ """
137
+ Delete a pipeline.
138
+
139
+ Args:
140
+ name (str): The name of the pipeline.
141
+ cfg (bool, optional): Whether to delete the config file. Defaults to True.
142
+ module (bool, optional): Whether to delete the module file. Defaults to False.
143
+
144
+ Returns:
145
+ None
146
+
147
+ Raises:
148
+ FileNotFoundError: If the specified files do not exist.
149
+
150
+ Examples:
151
+ >>> pm = PipelineManager()
152
+ >>> pm.delete("my_pipeline")
153
+ """
154
+ deleted_files = []
155
+ if cfg:
156
+ pipeline_cfg_path = posixpath.join(
157
+ self._cfg_dir, "pipelines", f"{name}.yml"
158
+ )
159
+ if self._fs.exists(pipeline_cfg_path):
160
+ self._fs.rm(pipeline_cfg_path)
161
+ deleted_files.append(pipeline_cfg_path)
162
+ logger.debug(
163
+ f"Deleted pipeline config: {pipeline_cfg_path}"
164
+ ) # Changed to DEBUG
165
+ else:
166
+ logger.warning(
167
+ f"Config file not found, skipping deletion: {pipeline_cfg_path}"
168
+ )
169
+
170
+ if module:
171
+ pipeline_py_path = posixpath.join(self._pipelines_dir, f"{name}.py")
172
+ if self._fs.exists(pipeline_py_path):
173
+ self._fs.rm(pipeline_py_path)
174
+ deleted_files.append(pipeline_py_path)
175
+ logger.debug(
176
+ f"Deleted pipeline module: {pipeline_py_path}"
177
+ ) # Changed to DEBUG
178
+ else:
179
+ logger.warning(
180
+ f"Module file not found, skipping deletion: {pipeline_py_path}"
181
+ )
182
+
183
+ if not deleted_files:
184
+ logger.warning(
185
+ f"No files found or specified for deletion for pipeline '{name}'."
186
+ )
187
+
188
+ # Sync filesystem if needed (using _fs)
189
+ if hasattr(self._fs, "sync") and callable(getattr(self._fs, "sync")):
190
+ self._fs.sync()
191
+
192
+ def _get_files(self) -> list[str]:
193
+ """
194
+ Get the list of pipeline files.
195
+
196
+ Returns:
197
+ list[str]: The list of pipeline files.
198
+ """
199
+ try:
200
+ return self._fs.glob(posixpath.join(self._pipelines_dir, "*.py"))
201
+ except Exception as e:
202
+ logger.error(
203
+ f"Error accessing pipeline directory {self._pipelines_dir}: {e}"
204
+ )
205
+ return []
206
+
207
+ def _get_names(self) -> list[str]:
208
+ """
209
+ Get the list of pipeline names.
210
+
211
+ Returns:
212
+ list[str]: The list of pipeline names.
213
+ """
214
+ files = self._get_files()
215
+ return [posixpath.basename(f).replace(".py", "") for f in files]
216
+
217
+ def get_summary(
218
+ self,
219
+ name: str | None = None,
220
+ cfg: bool = True,
221
+ code: bool = True,
222
+ project: bool = True,
223
+ ) -> dict[str, dict | str]:
224
+ """
225
+ Get a summary of the pipelines.
226
+
227
+ Args:
228
+ name (str | None, optional): The name of the pipeline. Defaults to None.
229
+ cfg (bool, optional): Whether to show the configuration. Defaults to True.
230
+ code (bool, optional): Whether to show the module. Defaults to True.
231
+ project (bool, optional): Whether to show the project configuration. Defaults to True.
232
+ Returns:
233
+ dict[str, dict | str]: A dictionary containing the pipeline summary.
234
+
235
+ Examples:
236
+ ```python
237
+ pm = PipelineManager()
238
+ summary=pm.get_summary()
239
+ ```
240
+ """
241
+ if name:
242
+ pipeline_names = [name]
243
+ else:
244
+ pipeline_names = self._get_names()
245
+
246
+ summary = {}
247
+ summary["pipelines"] = {}
248
+
249
+ if project:
250
+ # Use self.project_cfg directly
251
+ summary["project"] = self.project_cfg.to_dict()
252
+
253
+ for name in pipeline_names:
254
+ # Load pipeline config directly
255
+
256
+ pipeline_summary = {}
257
+ if cfg:
258
+ pipeline_cfg = PipelineConfig.load(name=name, fs=self._fs)
259
+ pipeline_summary["cfg"] = pipeline_cfg.to_dict()
260
+ if code:
261
+ try:
262
+ module_content = self._fs.cat(
263
+ posixpath.join(self._pipelines_dir, f"{name}.py")
264
+ ).decode()
265
+ pipeline_summary["module"] = module_content
266
+ except FileNotFoundError:
267
+ logger.warning(f"Module file not found for pipeline '{name}'")
268
+ pipeline_summary["module"] = "# Module file not found"
269
+ except Exception as e:
270
+ logger.error(
271
+ f"Error reading module file for pipeline '{name}': {e}"
272
+ )
273
+ pipeline_summary["module"] = f"# Error reading module file: {e}"
274
+
275
+ if pipeline_summary: # Only add if cfg or code was requested and found
276
+ summary["pipelines"][name] = pipeline_summary
277
+ return summary
278
+
279
+ def show_summary(
280
+ self,
281
+ name: str | None = None,
282
+ cfg: bool = True,
283
+ code: bool = True,
284
+ project: bool = True,
285
+ to_html: bool = False,
286
+ to_svg: bool = False,
287
+ ) -> None | str:
288
+ """
289
+ Show a summary of the pipelines.
290
+
291
+ Args:
292
+ name (str | None, optional): The name of the pipeline. Defaults to None.
293
+ cfg (bool, optional): Whether to show the configuration. Defaults to True.
294
+ code (bool, optional): Whether to show the module. Defaults to True.
295
+ project (bool, optional): Whether to show the project configuration. Defaults to True.
296
+ to_html (bool, optional): Whether to export the summary to HTML. Defaults to False.
297
+ to_svg (bool, optional): Whether to export the summary to SVG. Defaults to False.
298
+
299
+ Returns:
300
+ None | str: The summary of the pipelines. If `to_html` is True, returns the HTML string.
301
+ If `to_svg` is True, returns the SVG string.
302
+
303
+ Examples:
304
+ ```python
305
+ pm = PipelineManager()
306
+ pm.show_summary()
307
+ ```
308
+ """
309
+
310
+ summary = self.get_summary(name=name, cfg=cfg, code=code, project=project)
311
+ project_summary = summary.get("project", {})
312
+ pipeline_summary = summary["pipelines"]
313
+
314
+ def add_dict_to_tree(tree, dict_data, style="green"):
315
+ for key, value in dict_data.items():
316
+ if isinstance(value, dict):
317
+ branch = tree.add(f"[cyan]{key}:", style="bold cyan")
318
+ add_dict_to_tree(branch, value, style)
319
+ else:
320
+ tree.add(f"[cyan]{key}:[/] [green]{value}[/]")
321
+
322
+ console = Console()
323
+
324
+ if project:
325
+ # Create tree for project config
326
+ project_tree = Tree("📁 Project Configuration", style="bold magenta")
327
+ add_dict_to_tree(project_tree, project_summary)
328
+
329
+ # Print project configuration
330
+ console.print(
331
+ Panel(
332
+ project_tree,
333
+ title="Project Configuration",
334
+ border_style="blue",
335
+ padding=(2, 2),
336
+ )
337
+ )
338
+ console.print("\n")
339
+
340
+ for pipeline, info in pipeline_summary.items():
341
+ # Create tree for config
342
+ config_tree = Tree("📋 Pipeline Configuration", style="bold magenta")
343
+ add_dict_to_tree(config_tree, info["cfg"])
344
+
345
+ # Create syntax-highlighted code view
346
+ code_view = Syntax(
347
+ info["module"],
348
+ "python",
349
+ theme="default",
350
+ line_numbers=False,
351
+ word_wrap=True,
352
+ code_width=80,
353
+ padding=2,
354
+ )
355
+
356
+ if cfg:
357
+ # console.print(f"🔄 Pipeline: {pipeline}", style="bold blue")
358
+ console.print(
359
+ Panel(
360
+ config_tree,
361
+ title=f"🔄 Pipeline: {pipeline}",
362
+ subtitle="Configuration",
363
+ border_style="blue",
364
+ padding=(2, 2),
365
+ )
366
+ )
367
+ console.print("\n")
368
+
369
+ if code:
370
+ # console.print(f"🔄 Pipeline: {pipeline}", style="bold blue")
371
+ console.print(
372
+ Panel(
373
+ code_view,
374
+ title=f"🔄 Pipeline: {pipeline}",
375
+ subtitle="Module",
376
+ border_style="blue",
377
+ padding=(2, 2),
378
+ )
379
+ )
380
+ console.print("\n")
381
+ if to_html:
382
+ return console.export_html()
383
+ elif to_svg:
384
+ return console.export_svg()
385
+
386
+ @property
387
+ def summary(self) -> dict[str, dict | str]:
388
+ """
389
+ Get a summary of the pipelines.
390
+
391
+ Returns:
392
+ dict: A dictionary containing the pipeline summary.
393
+ """
394
+ return self.get_summary()
395
+
396
+ def _all_pipelines(
397
+ self, show: bool = True, to_html: bool = False, to_svg: bool = False
398
+ ) -> list[str] | None:
399
+ """
400
+ Print all available pipelines in a formatted table.
401
+
402
+ Args:
403
+ show (bool, optional): Whether to print the table. Defaults to True.
404
+ to_html (bool, optional): Whether to export the table to HTML. Defaults to False.
405
+ to_svg (bool, optional): Whether to export the table to SVG. Defaults to False.
406
+
407
+ Returns:
408
+ list[str] | None: A list of pipeline names if `show` is False.
409
+
410
+ Examples:
411
+ ```python
412
+ pm = PipelineManager()
413
+ all_pipelines = pm._pipelines(show=False)
414
+ ```
415
+ """
416
+ if to_html or to_svg:
417
+ show = True
418
+
419
+ pipeline_files = [
420
+ f for f in self._fs.ls(self._pipelines_dir) if f.endswith(".py")
421
+ ]
422
+ pipeline_names = [
423
+ posixpath.splitext(posixpath.basename(f))[0] for f in pipeline_files
424
+ ] # Simplified name extraction
425
+
426
+ if not pipeline_files:
427
+ rich.print("[yellow]No pipelines found[/yellow]")
428
+ return [] # Return empty list for consistency
429
+
430
+ pipeline_info = []
431
+
432
+ for path, name in zip(pipeline_files, pipeline_names):
433
+ try:
434
+ mod_time = self._fs.modified(path).strftime("%Y-%m-%d %H:%M:%S")
435
+ except NotImplementedError:
436
+ mod_time = "N/A"
437
+ try:
438
+ size_bytes = self._fs.size(path)
439
+ size = f"{size_bytes / 1024:.1f} KB" if size_bytes else "0.0 KB"
440
+ except NotImplementedError:
441
+ size = "N/A"
442
+ except Exception as e:
443
+ logger.warning(f"Could not get size for {path}: {e}")
444
+ size = "Error"
445
+
446
+ pipeline_info.append({
447
+ "name": name,
448
+ "path": path,
449
+ "mod_time": mod_time,
450
+ "size": size,
451
+ })
452
+
453
+ if show:
454
+ table = Table(title="Available Pipelines")
455
+ table.add_column("Pipeline Name", style="blue")
456
+ table.add_column("Path", style="magenta")
457
+ table.add_column("Last Modified", style="green")
458
+ table.add_column("Size", style="cyan")
459
+
460
+ for info in pipeline_info:
461
+ table.add_row(
462
+ info["name"], info["path"], info["mod_time"], info["size"]
463
+ )
464
+ console = Console(record=True)
465
+ console.print(table)
466
+ if to_html:
467
+ return console.export_html()
468
+ elif to_svg:
469
+ return console.export_svg()
470
+
471
+ else:
472
+ return pipeline_info
473
+
474
+ def show_pipelines(self) -> None:
475
+ """
476
+ Print all available pipelines in a formatted table.
477
+
478
+ Examples:
479
+ ```python
480
+ pm = PipelineManager()
481
+ pm.show_pipelines()
482
+ ```
483
+ """
484
+ self._all_pipelines(show=True)
485
+
486
+ def list_pipelines(self) -> list[str]:
487
+ """
488
+ Get a list of all available pipelines.
489
+
490
+ Returns:
491
+ list[str] | None: A list of pipeline names.
492
+
493
+ Examples:
494
+ ```python
495
+ pm = PipelineManager()
496
+ pipelines = pm.list_pipelines()
497
+ ```
498
+ """
499
+ return self._all_pipelines(show=False)
500
+
501
+ @property
502
+ def pipelines(self) -> list[str]:
503
+ """
504
+ Get a list of all available pipelines.
505
+
506
+ Returns:
507
+ list[str] | None: A list of pipeline names.
508
+
509
+ Examples:
510
+ ```python
511
+ pm = PipelineManager()
512
+ pipelines = pm.pipelines
513
+ ```
514
+ """
515
+ return self._all_pipelines(show=False)
516
+
517
+ def add_hook(self, name: str, type: HookType, to: str | None = None, function_name: str|None = None):
518
+ """
519
+ Add a hook to the pipeline module.
520
+
521
+ Args:
522
+ name (str): The name of the pipeline
523
+ type (HookType): The type of the hook.
524
+ to (str | None, optional): The name of the file to add the hook to. Defaults to the hook.py file in the pipelines hooks folder.
525
+ function_name (str | None, optional): The name of the function. If not provided uses default name of hook type.
526
+
527
+ Returns:
528
+ None
529
+
530
+ Examples:
531
+ ```python
532
+ pm = PipelineManager()
533
+ pm.add_hook(HookType.PRE_EXECUTE)
534
+ ```
535
+ """
536
+
537
+
538
+ if to is None:
539
+ to = f"hooks/{name}/hook.py"
540
+ else:
541
+ to = f"hooks/{name}/{to}"
542
+
543
+ match type:
544
+ case HookType.MQTT_BUILD_CONFIG:
545
+ template = HOOK_TEMPLATE__MQTT_BUILD_CONFIG
546
+
547
+ if function_name is None:
548
+ function_name = type.default_function_name()
549
+
550
+ if not self._fs.exists(to):
551
+ self._fs.makedirs(os.path.dirname(to), exist_ok=True)
552
+
553
+ with self._fs.open(to, "a") as f:
554
+ f.write(
555
+ template.format(
556
+ function_name=function_name
557
+ )
558
+ )
559
+
560
+ rich.print(f"🔧 Added hook [bold blue]{type.value}[/bold blue] to {to} as {function_name} for {name}")