FlowerPower 0.11.6.20__py3-none-any.whl → 0.21.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 (101) hide show
  1. flowerpower/__init__.py +2 -6
  2. flowerpower/cfg/__init__.py +7 -14
  3. flowerpower/cfg/base.py +29 -25
  4. flowerpower/cfg/pipeline/__init__.py +8 -6
  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 +36 -0
  9. flowerpower/cfg/project/__init__.py +11 -24
  10. flowerpower/cfg/project/adapter.py +0 -12
  11. flowerpower/cli/__init__.py +2 -21
  12. flowerpower/cli/cfg.py +0 -3
  13. flowerpower/cli/mqtt.py +0 -6
  14. flowerpower/cli/pipeline.py +22 -415
  15. flowerpower/cli/utils.py +0 -1
  16. flowerpower/flowerpower.py +345 -146
  17. flowerpower/pipeline/__init__.py +2 -0
  18. flowerpower/pipeline/base.py +21 -12
  19. flowerpower/pipeline/io.py +58 -54
  20. flowerpower/pipeline/manager.py +165 -726
  21. flowerpower/pipeline/pipeline.py +643 -0
  22. flowerpower/pipeline/registry.py +285 -18
  23. flowerpower/pipeline/visualizer.py +5 -6
  24. flowerpower/plugins/io/__init__.py +8 -0
  25. flowerpower/plugins/mqtt/__init__.py +7 -11
  26. flowerpower/settings/__init__.py +0 -2
  27. flowerpower/settings/{backend.py → _backend.py} +0 -21
  28. flowerpower/settings/logging.py +1 -1
  29. flowerpower/utils/logging.py +24 -12
  30. flowerpower/utils/misc.py +17 -256
  31. flowerpower/utils/monkey.py +1 -83
  32. flowerpower-0.21.0.dist-info/METADATA +463 -0
  33. flowerpower-0.21.0.dist-info/RECORD +44 -0
  34. flowerpower/cfg/pipeline/schedule.py +0 -74
  35. flowerpower/cfg/project/job_queue.py +0 -238
  36. flowerpower/cli/job_queue.py +0 -1061
  37. flowerpower/fs/__init__.py +0 -29
  38. flowerpower/fs/base.py +0 -662
  39. flowerpower/fs/ext.py +0 -2143
  40. flowerpower/fs/storage_options.py +0 -1420
  41. flowerpower/job_queue/__init__.py +0 -294
  42. flowerpower/job_queue/apscheduler/__init__.py +0 -11
  43. flowerpower/job_queue/apscheduler/_setup/datastore.py +0 -110
  44. flowerpower/job_queue/apscheduler/_setup/eventbroker.py +0 -93
  45. flowerpower/job_queue/apscheduler/manager.py +0 -1051
  46. flowerpower/job_queue/apscheduler/setup.py +0 -554
  47. flowerpower/job_queue/apscheduler/trigger.py +0 -169
  48. flowerpower/job_queue/apscheduler/utils.py +0 -311
  49. flowerpower/job_queue/base.py +0 -413
  50. flowerpower/job_queue/rq/__init__.py +0 -10
  51. flowerpower/job_queue/rq/_trigger.py +0 -37
  52. flowerpower/job_queue/rq/concurrent_workers/gevent_worker.py +0 -226
  53. flowerpower/job_queue/rq/concurrent_workers/thread_worker.py +0 -231
  54. flowerpower/job_queue/rq/manager.py +0 -1582
  55. flowerpower/job_queue/rq/setup.py +0 -154
  56. flowerpower/job_queue/rq/utils.py +0 -69
  57. flowerpower/mqtt.py +0 -12
  58. flowerpower/pipeline/job_queue.py +0 -583
  59. flowerpower/pipeline/runner.py +0 -603
  60. flowerpower/plugins/io/base.py +0 -2520
  61. flowerpower/plugins/io/helpers/datetime.py +0 -298
  62. flowerpower/plugins/io/helpers/polars.py +0 -875
  63. flowerpower/plugins/io/helpers/pyarrow.py +0 -570
  64. flowerpower/plugins/io/helpers/sql.py +0 -202
  65. flowerpower/plugins/io/loader/__init__.py +0 -28
  66. flowerpower/plugins/io/loader/csv.py +0 -37
  67. flowerpower/plugins/io/loader/deltatable.py +0 -190
  68. flowerpower/plugins/io/loader/duckdb.py +0 -19
  69. flowerpower/plugins/io/loader/json.py +0 -37
  70. flowerpower/plugins/io/loader/mqtt.py +0 -159
  71. flowerpower/plugins/io/loader/mssql.py +0 -26
  72. flowerpower/plugins/io/loader/mysql.py +0 -26
  73. flowerpower/plugins/io/loader/oracle.py +0 -26
  74. flowerpower/plugins/io/loader/parquet.py +0 -35
  75. flowerpower/plugins/io/loader/postgres.py +0 -26
  76. flowerpower/plugins/io/loader/pydala.py +0 -19
  77. flowerpower/plugins/io/loader/sqlite.py +0 -23
  78. flowerpower/plugins/io/metadata.py +0 -244
  79. flowerpower/plugins/io/saver/__init__.py +0 -28
  80. flowerpower/plugins/io/saver/csv.py +0 -36
  81. flowerpower/plugins/io/saver/deltatable.py +0 -186
  82. flowerpower/plugins/io/saver/duckdb.py +0 -19
  83. flowerpower/plugins/io/saver/json.py +0 -36
  84. flowerpower/plugins/io/saver/mqtt.py +0 -28
  85. flowerpower/plugins/io/saver/mssql.py +0 -26
  86. flowerpower/plugins/io/saver/mysql.py +0 -26
  87. flowerpower/plugins/io/saver/oracle.py +0 -26
  88. flowerpower/plugins/io/saver/parquet.py +0 -36
  89. flowerpower/plugins/io/saver/postgres.py +0 -26
  90. flowerpower/plugins/io/saver/pydala.py +0 -20
  91. flowerpower/plugins/io/saver/sqlite.py +0 -24
  92. flowerpower/plugins/mqtt/cfg.py +0 -17
  93. flowerpower/plugins/mqtt/manager.py +0 -962
  94. flowerpower/settings/job_queue.py +0 -87
  95. flowerpower/utils/scheduler.py +0 -311
  96. flowerpower-0.11.6.20.dist-info/METADATA +0 -537
  97. flowerpower-0.11.6.20.dist-info/RECORD +0 -102
  98. {flowerpower-0.11.6.20.dist-info → flowerpower-0.21.0.dist-info}/WHEEL +0 -0
  99. {flowerpower-0.11.6.20.dist-info → flowerpower-0.21.0.dist-info}/entry_points.txt +0 -0
  100. {flowerpower-0.11.6.20.dist-info → flowerpower-0.21.0.dist-info}/licenses/LICENSE +0 -0
  101. {flowerpower-0.11.6.20.dist-info → flowerpower-0.21.0.dist-info}/top_level.txt +0 -0
@@ -1,169 +0,0 @@
1
- import datetime as dt
2
- from enum import Enum
3
- from typing import Any, Dict, Type
4
-
5
- from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger
6
- from apscheduler.triggers.cron import CronTrigger
7
- from apscheduler.triggers.date import DateTrigger
8
- from apscheduler.triggers.interval import IntervalTrigger
9
-
10
- from ..base import BaseTrigger
11
-
12
-
13
- class TriggerType(Enum):
14
- CRON = "cron"
15
- INTERVAL = "interval"
16
- CALENDARINTERVAL = "calendarinterval"
17
- DATE = "date"
18
-
19
-
20
- # Mapping of trigger type to its class and allowed kwargs
21
- TRIGGER_CONFIG: Dict[TriggerType, Dict[str, Any]] = {
22
- TriggerType.CRON: {
23
- "class": CronTrigger,
24
- "kwargs": [
25
- "crontab",
26
- "year",
27
- "month",
28
- "week",
29
- "day",
30
- "day_of_week",
31
- "hour",
32
- "minute",
33
- "second",
34
- "start_time",
35
- "end_time",
36
- "timezone",
37
- ],
38
- },
39
- TriggerType.INTERVAL: {
40
- "class": IntervalTrigger,
41
- "kwargs": [
42
- "weeks",
43
- "days",
44
- "hours",
45
- "minutes",
46
- "seconds",
47
- "microseconds",
48
- "start_time",
49
- "end_time",
50
- ],
51
- },
52
- TriggerType.CALENDARINTERVAL: {
53
- "class": CalendarIntervalTrigger,
54
- "kwargs": [
55
- "years",
56
- "months",
57
- "weeks",
58
- "days",
59
- "hour",
60
- "minute",
61
- "second",
62
- "start_date",
63
- "end_date",
64
- "timezone",
65
- ],
66
- },
67
- TriggerType.DATE: {
68
- "class": DateTrigger,
69
- "kwargs": [
70
- "run_time",
71
- ],
72
- },
73
- }
74
-
75
-
76
- class APSTrigger(BaseTrigger):
77
- """
78
- Implementation of BaseTrigger for APScheduler.
79
-
80
- Provides a factory for creating APScheduler trigger instances
81
- with validation and filtering of keyword arguments.
82
- """
83
-
84
- trigger_type: TriggerType
85
-
86
- def __init__(self, trigger_type: str):
87
- """
88
- Initialize APSchedulerTrigger with a trigger type.
89
-
90
- Args:
91
- trigger_type (str): The type of trigger (cron, interval, calendarinterval, date).
92
-
93
- Raises:
94
- ValueError: If the trigger_type is invalid.
95
- """
96
- try:
97
- self.trigger_type = TriggerType(trigger_type.lower())
98
- except ValueError:
99
- valid_types = [t.value for t in TriggerType]
100
- raise ValueError(
101
- f"Invalid trigger type '{trigger_type}'. Valid types are: {valid_types}"
102
- )
103
-
104
- def _get_allowed_kwargs(self) -> set:
105
- """Return the set of allowed kwargs for the current trigger type."""
106
- return set(TRIGGER_CONFIG[self.trigger_type]["kwargs"])
107
-
108
- def _check_kwargs(self, **kwargs) -> None:
109
- """
110
- Validate that all provided kwargs are allowed for the trigger type.
111
-
112
- Raises:
113
- ValueError: If any kwarg is not allowed.
114
- """
115
- allowed = self._get_allowed_kwargs()
116
- invalid = [k for k in kwargs if k not in allowed]
117
- if invalid:
118
- raise ValueError(
119
- f"Invalid argument(s) for trigger type '{self.trigger_type.value}': {invalid}. "
120
- f"Allowed arguments are: {sorted(allowed)}"
121
- )
122
-
123
- def _filter_kwargs(self, **kwargs) -> Dict[str, Any]:
124
- """
125
- Filter kwargs to only those allowed for the trigger type and not None.
126
-
127
- Returns:
128
- Dict[str, Any]: Filtered kwargs.
129
- """
130
- allowed = self._get_allowed_kwargs()
131
- return {k: v for k, v in kwargs.items() if k in allowed and v is not None}
132
-
133
- def get_trigger_instance(self, **kwargs) -> Any:
134
- """
135
- Create and return an APScheduler trigger instance based on the trigger type.
136
-
137
- Args:
138
- **kwargs: Keyword arguments for the trigger.
139
-
140
- Returns:
141
- Any: An APScheduler trigger instance.
142
-
143
- Raises:
144
- ValueError: If invalid arguments are provided or trigger type is unknown.
145
- """
146
- self._check_kwargs(**kwargs)
147
- filtered_kwargs = self._filter_kwargs(**kwargs)
148
- trigger_cls: Type = TRIGGER_CONFIG[self.trigger_type]["class"]
149
-
150
- if self.trigger_type == TriggerType.CRON:
151
- crontab = filtered_kwargs.pop("crontab", None)
152
- if crontab:
153
- return trigger_cls.from_crontab(crontab)
154
- return trigger_cls(**filtered_kwargs)
155
- elif self.trigger_type == TriggerType.INTERVAL:
156
- return trigger_cls(**filtered_kwargs)
157
- elif self.trigger_type == TriggerType.CALENDARINTERVAL:
158
- return trigger_cls(**filtered_kwargs)
159
- elif self.trigger_type == TriggerType.DATE:
160
- # Default to now if not specified
161
- if "run_time" not in filtered_kwargs:
162
- filtered_kwargs["run_time"] = dt.datetime.now()
163
- return trigger_cls(**filtered_kwargs)
164
- else:
165
- # This should never be reached due to Enum validation in __init__
166
- raise ValueError(f"Unknown trigger type: {self.trigger_type.value}")
167
-
168
-
169
- # End of file
@@ -1,311 +0,0 @@
1
- from operator import attrgetter
2
- from typing import List
3
-
4
- from rich.console import Console
5
- from rich.table import Table
6
-
7
-
8
- def humanize_crontab(minute, hour, day, month, day_of_week):
9
- days = {
10
- "0": "Sunday",
11
- "sun": "Sunday",
12
- "7": "Sunday",
13
- "1": "Monday",
14
- "mon": "Monday",
15
- "2": "Tuesday",
16
- "tue": "Tuesday",
17
- "3": "Wednesday",
18
- "wed": "Wednesday",
19
- "4": "Thursday",
20
- "thu": "Thursday",
21
- "5": "Friday",
22
- "fri": "Friday",
23
- "6": "Saturday",
24
- "sat": "Saturday",
25
- "*": "*",
26
- }
27
- months = {
28
- "1": "January",
29
- "2": "February",
30
- "3": "March",
31
- "4": "April",
32
- "5": "May",
33
- "6": "June",
34
- "7": "July",
35
- "8": "August",
36
- "9": "September",
37
- "10": "October",
38
- "11": "November",
39
- "12": "December",
40
- "*": "*",
41
- }
42
-
43
- def get_day_name(day_input):
44
- day_input = str(day_input).lower().strip()
45
- if "-" in day_input:
46
- start, end = day_input.split("-")
47
- return f"{days.get(start.strip(), start)}-{days.get(end.strip(), end)}"
48
- if "," in day_input:
49
- return ", ".join(
50
- days.get(d.strip(), d.strip()) for d in day_input.split(",")
51
- )
52
- return days.get(day_input, day_input)
53
-
54
- try:
55
- minute, hour, day, month, day_of_week = map(
56
- str.strip, map(str, [minute, hour, day, month, day_of_week])
57
- )
58
-
59
- if "/" in minute:
60
- return f"every {minute.split('/')[1]} minutes"
61
- if "/" in hour:
62
- return f"every {hour.split('/')[1]} hours"
63
-
64
- if all(x == "*" for x in [minute, hour, day, month, day_of_week]):
65
- return "every minute"
66
- if [minute, hour, day, month, day_of_week] == ["0", "*", "*", "*", "*"]:
67
- return "every hour"
68
-
69
- if (
70
- minute == "0"
71
- and hour != "*"
72
- and day == "*"
73
- and month == "*"
74
- and day_of_week == "*"
75
- ):
76
- return (
77
- "every day at midnight"
78
- if hour == "0"
79
- else "every day at noon"
80
- if hour == "12"
81
- else f"every day at {hour}:00"
82
- )
83
-
84
- if (
85
- minute == "0"
86
- and hour == "0"
87
- and day == "*"
88
- and month == "*"
89
- and day_of_week != "*"
90
- ):
91
- return f"every {get_day_name(day_of_week)} at midnight"
92
-
93
- if (
94
- minute == "0"
95
- and hour != "*"
96
- and day == "*"
97
- and month == "*"
98
- and day_of_week != "*"
99
- ):
100
- return (
101
- "every weekday at {hour}:00"
102
- if "-" in day_of_week
103
- and "mon" in day_of_week.lower()
104
- and "fri" in day_of_week.lower()
105
- else f"every {get_day_name(day_of_week)} at {hour}:00"
106
- )
107
-
108
- if (
109
- minute != "*"
110
- and hour != "*"
111
- and day == "*"
112
- and month == "*"
113
- and day_of_week == "*"
114
- ):
115
- return f"every day at {hour}:{minute.zfill(2)}"
116
-
117
- if day != "*" and month != "*" and minute == "0" and hour == "0":
118
- return f"on day {day} of {months.get(month, month)} at midnight"
119
-
120
- if (
121
- minute != "*"
122
- and hour == "*"
123
- and day == "*"
124
- and month == "*"
125
- and day_of_week == "*"
126
- ):
127
- return f"every hour at minute {minute}"
128
-
129
- parts = []
130
- if minute != "*":
131
- parts.append(f"at minute {minute}")
132
- if hour != "*":
133
- parts.append(f"hour {hour}")
134
- if day != "*":
135
- parts.append(f"day {day}")
136
- if month != "*":
137
- parts.append(f"month {months.get(month, month)}")
138
- if day_of_week != "*":
139
- parts.append(f"on {get_day_name(day_of_week)}")
140
-
141
- return f"runs {' '.join(parts)}" if parts else "every minute"
142
- except Exception:
143
- return f"{minute} {hour} {day} {month} {day_of_week}"
144
-
145
-
146
- def format_trigger(trigger):
147
- trigger_type = trigger.__class__.__name__
148
-
149
- if trigger_type == "IntervalTrigger":
150
- for unit in ["seconds", "minutes", "hours", "days"]:
151
- if value := getattr(trigger, unit, None):
152
- return f"Interval: Every {value}{unit[0]}"
153
- return "Interval"
154
-
155
- if trigger_type == "CronTrigger":
156
- try:
157
- cron_parts = dict(
158
- part.split("=")
159
- for part in str(trigger).strip("CronTrigger(").rstrip(")").split(", ")
160
- )
161
- cron_parts = {k: v.strip("'") for k, v in cron_parts.items()}
162
- crontab = f"{cron_parts['minute']} {cron_parts['hour']} {cron_parts['day']} {cron_parts['month']} {cron_parts['day_of_week']}"
163
- human_readable = humanize_crontab(
164
- **{
165
- k: cron_parts[k]
166
- for k in ["minute", "hour", "day", "month", "day_of_week"]
167
- }
168
- )
169
- return f"Cron: {human_readable} ({crontab})"
170
- except Exception:
171
- return f"Cron: {str(trigger)}"
172
-
173
- if trigger_type == "DateTrigger":
174
- return f"Date: Once at {trigger.run_date.strftime('%Y-%m-%d %H:%M:%S')}"
175
-
176
- return f"{trigger_type}: {str(trigger)}"
177
-
178
-
179
- def display_schedules(schedules: List):
180
- console = Console()
181
- total_width = console.width - 10
182
-
183
- width_ratios = {
184
- "id": 0.20,
185
- "task": 0.10,
186
- "trigger": 0.25,
187
- "name": 0.15,
188
- "run_args": 0.15,
189
- "next_fire": 0.08,
190
- "last_fire": 0.08,
191
- "paused": 0.01,
192
- }
193
-
194
- widths = {k: max(10, int(total_width * ratio)) for k, ratio in width_ratios.items()}
195
-
196
- table = Table(
197
- show_header=True,
198
- header_style="bold magenta",
199
- width=total_width,
200
- row_styles=["", "dim"],
201
- border_style="blue",
202
- show_lines=True,
203
- )
204
-
205
- for col, style, width in [
206
- ("ID", "dim", widths["id"]),
207
- ("Task", "cyan", widths["task"]),
208
- ("Trigger", "blue", widths["trigger"]),
209
- ("Name", "yellow", widths["name"]),
210
- ("Run Args", "yellow", widths["run_args"]),
211
- ("Next Fire Time", "green", widths["next_fire"]),
212
- ("Last Fire Time", "red", widths["last_fire"]),
213
- ("Paused", "bold", widths["paused"]),
214
- ]:
215
- table.add_column(col, style=style, width=width)
216
-
217
- for schedule in sorted(schedules, key=attrgetter("next_fire_time")):
218
- table.add_row(
219
- schedule.id,
220
- schedule.task_id.split(":")[-1],
221
- format_trigger(schedule.trigger),
222
- (
223
- str(schedule.args[1])
224
- if schedule.args and len(schedule.args) > 1
225
- else "None"
226
- ),
227
- "\n".join(f"{k}: {v}" for k, v in (schedule.kwargs or {}).items())
228
- or "None",
229
- (
230
- schedule.next_fire_time.strftime("%Y-%m-%d %H:%M:%S")
231
- if schedule.next_fire_time
232
- else "Never"
233
- ),
234
- (
235
- schedule.last_fire_time.strftime("%Y-%m-%d %H:%M:%S")
236
- if schedule.last_fire_time
237
- else "Never"
238
- ),
239
- "✓" if schedule.paused else "✗",
240
- )
241
-
242
- console.print(table)
243
-
244
-
245
- def display_tasks(tasks):
246
- console = Console()
247
- table = Table(title="Tasks")
248
-
249
- widths = {"id": 50, "executor": 15, "max_jobs": 15, "misfire": 20}
250
-
251
- for col, style, width in [
252
- ("ID", "cyan", widths["id"]),
253
- ("Job Executor", "blue", widths["executor"]),
254
- ("Max Running Jobs", "yellow", widths["max_jobs"]),
255
- ("Misfire Grace Time", "green", widths["misfire"]),
256
- ]:
257
- table.add_column(col, style=style, width=width)
258
-
259
- for task in sorted(tasks, key=attrgetter("id")):
260
- table.add_row(
261
- task.id,
262
- str(task.job_executor),
263
- str(task.max_running_jobs or "None"),
264
- str(task.misfire_grace_time or "None"),
265
- )
266
-
267
- console.print(table)
268
-
269
-
270
- def display_jobs(jobs):
271
- console = Console()
272
- table = Table(title="Jobs")
273
-
274
- widths = {
275
- "id": 10,
276
- "task_id": 40,
277
- "args": 20,
278
- "kwargs": 20,
279
- "schedule": 15,
280
- "created": 25,
281
- "status": 15,
282
- }
283
-
284
- for col, style, width in [
285
- ("ID", "cyan", widths["id"]),
286
- ("Task ID", "blue", widths["task_id"]),
287
- ("Args", "yellow", widths["args"]),
288
- ("Kwargs", "yellow", widths["kwargs"]),
289
- ("Schedule ID", "green", widths["schedule"]),
290
- ("Created At", "magenta", widths["created"]),
291
- ("Status", "red", widths["status"]),
292
- ]:
293
- table.add_column(col, style=style, width=width)
294
-
295
- for job in sorted(jobs, key=attrgetter("id")):
296
- status = "Running" if job.acquired_by else "Pending"
297
- table.add_row(
298
- str(job.id),
299
- job.task_id,
300
- str(job.args if job.args else "None"),
301
- (
302
- "\n".join(f"{k}: {v}" for k, v in job.kwargs.items())
303
- if job.kwargs
304
- else "None"
305
- ),
306
- str(job.schedule_id or "None"),
307
- job.created_at.strftime("%Y-%m-%d %H:%M:%S"),
308
- status,
309
- )
310
-
311
- console.print(table)