ddeutil-workflow 0.0.4__py3-none-any.whl → 0.0.6__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +9 -0
- ddeutil/workflow/__types.py +43 -1
- ddeutil/workflow/exceptions.py +13 -1
- ddeutil/workflow/loader.py +16 -110
- ddeutil/workflow/on.py +195 -0
- ddeutil/workflow/pipeline.py +351 -371
- ddeutil/workflow/{vendors/__schedule.py → scheduler.py} +222 -176
- ddeutil/workflow/stage.py +402 -0
- ddeutil/workflow/utils.py +219 -28
- {ddeutil_workflow-0.0.4.dist-info → ddeutil_workflow-0.0.6.dist-info}/METADATA +118 -90
- ddeutil_workflow-0.0.6.dist-info/RECORD +15 -0
- {ddeutil_workflow-0.0.4.dist-info → ddeutil_workflow-0.0.6.dist-info}/WHEEL +1 -1
- ddeutil/workflow/__regex.py +0 -44
- ddeutil/workflow/conn.py +0 -240
- ddeutil/workflow/schedule.py +0 -82
- ddeutil/workflow/tasks/__init__.py +0 -6
- ddeutil/workflow/tasks/_pandas.py +0 -54
- ddeutil/workflow/tasks/_polars.py +0 -92
- ddeutil/workflow/vendors/__dataset.py +0 -127
- ddeutil/workflow/vendors/__dict.py +0 -333
- ddeutil/workflow/vendors/__init__.py +0 -0
- ddeutil/workflow/vendors/aws.py +0 -185
- ddeutil/workflow/vendors/az.py +0 -0
- ddeutil/workflow/vendors/minio.py +0 -11
- ddeutil/workflow/vendors/pd.py +0 -13
- ddeutil/workflow/vendors/pg.py +0 -11
- ddeutil/workflow/vendors/pl.py +0 -172
- ddeutil/workflow/vendors/sftp.py +0 -209
- ddeutil_workflow-0.0.4.dist-info/RECORD +0 -29
- {ddeutil_workflow-0.0.4.dist-info → ddeutil_workflow-0.0.6.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.4.dist-info → ddeutil_workflow-0.0.6.dist-info}/top_level.txt +0 -0
@@ -7,14 +7,10 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import copy
|
9
9
|
from collections.abc import Iterator
|
10
|
-
from
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
from datetime import datetime, timedelta
|
11
12
|
from functools import partial, total_ordering
|
12
|
-
from typing import
|
13
|
-
Any,
|
14
|
-
Callable,
|
15
|
-
Optional,
|
16
|
-
Union,
|
17
|
-
)
|
13
|
+
from typing import Callable, Optional, Union
|
18
14
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
19
15
|
|
20
16
|
from ddeutil.core import (
|
@@ -37,31 +33,55 @@ WEEKDAYS: dict[str, int] = {
|
|
37
33
|
"Sat": 6,
|
38
34
|
}
|
39
35
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
36
|
+
|
37
|
+
@dataclass(frozen=True)
|
38
|
+
class Unit:
|
39
|
+
name: str
|
40
|
+
range: partial
|
41
|
+
min: int
|
42
|
+
max: int
|
43
|
+
alt: list[str] = field(default_factory=list)
|
44
|
+
|
45
|
+
def __repr__(self) -> str:
|
46
|
+
return (
|
47
|
+
f"{self.__class__}(name={self.name!r}, range={self.range},"
|
48
|
+
f"min={self.min}, max={self.max}"
|
49
|
+
f"{f', alt={self.alt}' if self.alt else ''})"
|
50
|
+
)
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class Options:
|
55
|
+
output_weekday_names: bool = False
|
56
|
+
output_month_names: bool = False
|
57
|
+
output_hashes: bool = False
|
58
|
+
|
59
|
+
|
60
|
+
CRON_UNITS: tuple[Unit, ...] = (
|
61
|
+
Unit(
|
62
|
+
name="minute",
|
63
|
+
range=partial(range, 0, 60),
|
64
|
+
min=0,
|
65
|
+
max=59,
|
66
|
+
),
|
67
|
+
Unit(
|
68
|
+
name="hour",
|
69
|
+
range=partial(range, 0, 24),
|
70
|
+
min=0,
|
71
|
+
max=23,
|
72
|
+
),
|
73
|
+
Unit(
|
74
|
+
name="day",
|
75
|
+
range=partial(range, 1, 32),
|
76
|
+
min=1,
|
77
|
+
max=31,
|
78
|
+
),
|
79
|
+
Unit(
|
80
|
+
name="month",
|
81
|
+
range=partial(range, 1, 13),
|
82
|
+
min=1,
|
83
|
+
max=12,
|
84
|
+
alt=[
|
65
85
|
"JAN",
|
66
86
|
"FEB",
|
67
87
|
"MAR",
|
@@ -75,13 +95,13 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
|
|
75
95
|
"NOV",
|
76
96
|
"DEC",
|
77
97
|
],
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
98
|
+
),
|
99
|
+
Unit(
|
100
|
+
name="weekday",
|
101
|
+
range=partial(range, 0, 7),
|
102
|
+
min=0,
|
103
|
+
max=6,
|
104
|
+
alt=[
|
85
105
|
"SUN",
|
86
106
|
"MON",
|
87
107
|
"TUE",
|
@@ -90,16 +110,16 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
|
|
90
110
|
"FRI",
|
91
111
|
"SAT",
|
92
112
|
],
|
93
|
-
|
113
|
+
),
|
94
114
|
)
|
95
115
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
116
|
+
CRON_UNITS_YEAR: tuple[Unit, ...] = CRON_UNITS + (
|
117
|
+
Unit(
|
118
|
+
name="year",
|
119
|
+
range=partial(range, 1990, 2101),
|
120
|
+
min=1990,
|
121
|
+
max=2100,
|
122
|
+
),
|
103
123
|
)
|
104
124
|
|
105
125
|
|
@@ -115,42 +135,68 @@ class CronPart:
|
|
115
135
|
|
116
136
|
def __init__(
|
117
137
|
self,
|
118
|
-
unit:
|
119
|
-
values:
|
120
|
-
options:
|
121
|
-
):
|
122
|
-
self.unit:
|
123
|
-
self.options:
|
138
|
+
unit: Unit,
|
139
|
+
values: str | list[int],
|
140
|
+
options: Options,
|
141
|
+
) -> None:
|
142
|
+
self.unit: Unit = unit
|
143
|
+
self.options: Options = options
|
144
|
+
|
124
145
|
if isinstance(values, str):
|
125
146
|
values: list[int] = self.from_str(values) if values != "?" else []
|
126
147
|
elif isinstance_check(values, list[int]):
|
127
148
|
values: list[int] = self.replace_weekday(values)
|
128
149
|
else:
|
129
150
|
raise TypeError(f"Invalid type of value in cron part: {values}.")
|
130
|
-
|
151
|
+
|
152
|
+
self.values: list[int] = self.out_of_range(
|
131
153
|
sorted(dict.fromkeys(values))
|
132
154
|
)
|
133
|
-
self.values: list[int] = unique_values
|
134
155
|
|
135
156
|
def __str__(self) -> str:
|
136
|
-
"""
|
137
|
-
|
157
|
+
"""Generate String value from part of cronjob."""
|
158
|
+
_hash: str = "H" if self.options.output_hashes else "*"
|
159
|
+
|
160
|
+
if self.is_full:
|
161
|
+
return _hash
|
162
|
+
|
163
|
+
if self.is_interval:
|
164
|
+
if self.is_full_interval:
|
165
|
+
return f"{_hash}/{self.step}"
|
166
|
+
_hash: str = (
|
167
|
+
f"H({self.filler(self.min)}-{self.filler(self.max)})"
|
168
|
+
if _hash == "H"
|
169
|
+
else f"{self.filler(self.min)}-{self.filler(self.max)}"
|
170
|
+
)
|
171
|
+
return f"{_hash}/{self.step}"
|
172
|
+
|
173
|
+
cron_range_strings: list[str] = []
|
174
|
+
for cron_range in self.ranges():
|
175
|
+
if isinstance(cron_range, list):
|
176
|
+
cron_range_strings.append(
|
177
|
+
f"{self.filler(cron_range[0])}-{self.filler(cron_range[1])}"
|
178
|
+
)
|
179
|
+
else:
|
180
|
+
cron_range_strings.append(f"{self.filler(cron_range)}")
|
181
|
+
return ",".join(cron_range_strings) if cron_range_strings else "?"
|
138
182
|
|
139
|
-
def __repr__(self):
|
183
|
+
def __repr__(self) -> str:
|
140
184
|
return (
|
141
185
|
f"{self.__class__.__name__}"
|
142
|
-
f"(unit={self.unit}, values={self.
|
186
|
+
f"(unit={self.unit}, values={self.__str__()!r})"
|
143
187
|
)
|
144
188
|
|
145
189
|
def __lt__(self, other) -> bool:
|
146
|
-
|
190
|
+
if isinstance(other, CronPart):
|
191
|
+
return self.values < other.values
|
192
|
+
elif isinstance(other, list):
|
193
|
+
return self.values < other
|
147
194
|
|
148
195
|
def __eq__(self, other) -> bool:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
return self.unit["name"] == "weekday"
|
196
|
+
if isinstance(other, CronPart):
|
197
|
+
return self.values == other.values
|
198
|
+
elif isinstance(other, list):
|
199
|
+
return self.values == other
|
154
200
|
|
155
201
|
@property
|
156
202
|
def min(self) -> int:
|
@@ -176,19 +222,17 @@ class CronPart:
|
|
176
222
|
@property
|
177
223
|
def is_full(self) -> bool:
|
178
224
|
"""Returns true if range has all the values of the unit."""
|
179
|
-
return len(self.values) == (
|
180
|
-
self.unit.get("max") - self.unit.get("min") + 1
|
181
|
-
)
|
225
|
+
return len(self.values) == (self.unit.max - self.unit.min + 1)
|
182
226
|
|
183
227
|
def from_str(self, value: str) -> tuple[int, ...]:
|
184
228
|
"""Parses a string as a range of positive integers. The string should
|
185
229
|
include only `-` and `,` special strings.
|
186
230
|
|
187
|
-
:param value:
|
231
|
+
:param value: A string value that want to parse
|
188
232
|
:type value: str
|
189
233
|
|
190
234
|
TODO: support for `L`, `W`, and `#`
|
191
|
-
TODO:
|
235
|
+
TODO: if you didn't care what day of the week the 7th was, you
|
192
236
|
could enter ? in the Day-of-week field.
|
193
237
|
TODO: L : the Day-of-month or Day-of-week fields specifies the last day
|
194
238
|
of the month or week.
|
@@ -198,7 +242,7 @@ class CronPart:
|
|
198
242
|
TODO: # : 3#2 would be the second Tuesday of the month,
|
199
243
|
the 3 refers to Tuesday because it is the third day of each week.
|
200
244
|
|
201
|
-
|
245
|
+
Noted:
|
202
246
|
- 0 10 * * ? *
|
203
247
|
Run at 10:00 am (UTC) every day
|
204
248
|
|
@@ -247,7 +291,7 @@ class CronPart:
|
|
247
291
|
if (value_step and not is_int(value_step)) or value_step == "":
|
248
292
|
raise ValueError(
|
249
293
|
f"Invalid interval step value {value_step!r} for "
|
250
|
-
f
|
294
|
+
f"{self.unit.name!r}"
|
251
295
|
)
|
252
296
|
|
253
297
|
interval_list.append(self._interval(value_range_list, value_step))
|
@@ -255,17 +299,20 @@ class CronPart:
|
|
255
299
|
|
256
300
|
def replace_alternative(self, value: str) -> str:
|
257
301
|
"""Replaces the alternative representations of numbers in a string."""
|
258
|
-
for i, alt in enumerate(self.unit.
|
302
|
+
for i, alt in enumerate(self.unit.alt):
|
259
303
|
if alt in value:
|
260
|
-
value: str = value.replace(alt, str(self.unit
|
304
|
+
value: str = value.replace(alt, str(self.unit.min + i))
|
261
305
|
return value
|
262
306
|
|
263
|
-
def replace_weekday(
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
307
|
+
def replace_weekday(self, values: list[int] | Iterator[int]) -> list[int]:
|
308
|
+
"""Replaces all 7 with 0 as Sunday can be represented by both.
|
309
|
+
|
310
|
+
:param values: list or iter of int that want to mode by 7
|
311
|
+
:rtype: list[int]
|
312
|
+
"""
|
313
|
+
if self.unit.name == "weekday":
|
314
|
+
# NOTE: change weekday value in range 0-6 (div-mod by 7).
|
315
|
+
return [value % 7 for value in values]
|
269
316
|
return list(values)
|
270
317
|
|
271
318
|
def out_of_range(self, values: list[int]) -> list[int]:
|
@@ -277,20 +324,20 @@ class CronPart:
|
|
277
324
|
:rtype: list[int]
|
278
325
|
"""
|
279
326
|
if values:
|
280
|
-
if (first := values[0]) < self.unit
|
327
|
+
if (first := values[0]) < self.unit.min:
|
281
328
|
raise ValueError(
|
282
|
-
f
|
329
|
+
f"Value {first!r} out of range for {self.unit.name!r}"
|
283
330
|
)
|
284
|
-
elif (last := values[-1]) > self.unit
|
331
|
+
elif (last := values[-1]) > self.unit.max:
|
285
332
|
raise ValueError(
|
286
|
-
f
|
333
|
+
f"Value {last!r} out of range for {self.unit.name!r}"
|
287
334
|
)
|
288
335
|
return values
|
289
336
|
|
290
337
|
def _parse_range(self, value: str) -> list[int]:
|
291
338
|
"""Parses a range string."""
|
292
339
|
if value == "*":
|
293
|
-
return list(self.unit
|
340
|
+
return list(self.unit.range())
|
294
341
|
elif value.count("-") > 1:
|
295
342
|
raise ValueError(f"Invalid value {value}")
|
296
343
|
try:
|
@@ -306,7 +353,9 @@ class CronPart:
|
|
306
353
|
return self.replace_weekday(sub_parts)
|
307
354
|
|
308
355
|
def _interval(
|
309
|
-
self,
|
356
|
+
self,
|
357
|
+
values: list[int],
|
358
|
+
step: int | None = None,
|
310
359
|
) -> list[int]:
|
311
360
|
"""Applies an interval step to a collection of values."""
|
312
361
|
if not step:
|
@@ -314,7 +363,7 @@ class CronPart:
|
|
314
363
|
elif (_step := int(step)) < 1:
|
315
364
|
raise ValueError(
|
316
365
|
f"Invalid interval step value {_step!r} for "
|
317
|
-
f
|
366
|
+
f"{self.unit.name!r}"
|
318
367
|
)
|
319
368
|
min_value: int = values[0]
|
320
369
|
return [
|
@@ -340,8 +389,8 @@ class CronPart:
|
|
340
389
|
"""Returns true if the range contains all the interval values."""
|
341
390
|
if step := self.step:
|
342
391
|
return (
|
343
|
-
self.min == self.unit
|
344
|
-
and (self.max + step) > self.unit
|
392
|
+
self.min == self.unit.min
|
393
|
+
and (self.max + step) > self.unit.max
|
345
394
|
and (
|
346
395
|
len(self.values)
|
347
396
|
== (round((self.max - self.min) / step) + 1)
|
@@ -376,33 +425,6 @@ class CronPart:
|
|
376
425
|
start_number: Optional[int] = value
|
377
426
|
return multi_dim_values
|
378
427
|
|
379
|
-
def to_str(self) -> str:
|
380
|
-
"""Returns the cron range as a string value."""
|
381
|
-
_hash: str = "H" if self.options.get("output_hashes") else "*"
|
382
|
-
|
383
|
-
if self.is_full:
|
384
|
-
return _hash
|
385
|
-
|
386
|
-
if self.is_interval:
|
387
|
-
if self.is_full_interval:
|
388
|
-
return f"{_hash}/{self.step}"
|
389
|
-
_hash: str = (
|
390
|
-
f"H({self.filler(self.min)}-{self.filler(self.max)})"
|
391
|
-
if _hash == "H"
|
392
|
-
else f"{self.filler(self.min)}-{self.filler(self.max)}"
|
393
|
-
)
|
394
|
-
return f"{_hash}/{self.step}"
|
395
|
-
|
396
|
-
cron_range_strings: list[str] = []
|
397
|
-
for cron_range in self.ranges():
|
398
|
-
if isinstance(cron_range, list):
|
399
|
-
cron_range_strings.append(
|
400
|
-
f"{self.filler(cron_range[0])}-{self.filler(cron_range[1])}"
|
401
|
-
)
|
402
|
-
else:
|
403
|
-
cron_range_strings.append(f"{self.filler(cron_range)}")
|
404
|
-
return ",".join(cron_range_strings) if cron_range_strings else "?"
|
405
|
-
|
406
428
|
def filler(self, value: int) -> int | str:
|
407
429
|
"""Formats weekday and month names as string when the relevant options
|
408
430
|
are set.
|
@@ -413,15 +435,15 @@ class CronPart:
|
|
413
435
|
:rtype: int | str
|
414
436
|
"""
|
415
437
|
return (
|
416
|
-
self.unit
|
438
|
+
self.unit.alt[value - self.unit.min]
|
417
439
|
if (
|
418
440
|
(
|
419
|
-
self.options
|
420
|
-
and self.unit
|
441
|
+
self.options.output_weekday_names
|
442
|
+
and self.unit.name == "weekday"
|
421
443
|
)
|
422
444
|
or (
|
423
|
-
self.options
|
424
|
-
and self.unit
|
445
|
+
self.options.output_month_names
|
446
|
+
and self.unit.name == "month"
|
425
447
|
)
|
426
448
|
)
|
427
449
|
else value
|
@@ -433,7 +455,7 @@ class CronJob:
|
|
433
455
|
"""The Cron Job Converter object that generate datetime dimension of cron
|
434
456
|
job schedule format,
|
435
457
|
|
436
|
-
* * * * * <command to execute>
|
458
|
+
... * * * * * <command to execute>
|
437
459
|
|
438
460
|
(i) minute (0 - 59)
|
439
461
|
(ii) hour (0 - 23)
|
@@ -447,25 +469,20 @@ class CronJob:
|
|
447
469
|
Support special value with `/`, `*`, `-`, `,`, and `?` (in day of month
|
448
470
|
and day of week value).
|
449
471
|
|
450
|
-
:
|
472
|
+
References:
|
451
473
|
- https://github.com/Sonic0/cron-converter
|
452
474
|
- https://pypi.org/project/python-crontab/
|
453
475
|
"""
|
454
476
|
|
455
477
|
cron_length: int = 5
|
456
|
-
|
457
|
-
options_defaults: dict[str, bool] = {
|
458
|
-
"output_weekday_names": False,
|
459
|
-
"output_month_names": False,
|
460
|
-
"output_hashes": False,
|
461
|
-
}
|
478
|
+
cron_units: tuple[Unit, ...] = CRON_UNITS
|
462
479
|
|
463
480
|
def __init__(
|
464
481
|
self,
|
465
482
|
value: Union[list[list[int]], str],
|
466
483
|
*,
|
467
484
|
option: Optional[dict[str, bool]] = None,
|
468
|
-
):
|
485
|
+
) -> None:
|
469
486
|
if isinstance(value, str):
|
470
487
|
value: list[str] = value.strip().split()
|
471
488
|
elif not isinstance_check(value, list[list[int]]):
|
@@ -473,16 +490,22 @@ class CronJob:
|
|
473
490
|
f"{self.__class__.__name__} cron value does not support "
|
474
491
|
f"type: {type(value)}."
|
475
492
|
)
|
493
|
+
|
494
|
+
# NOTE: Validate length of crontab of this class.
|
476
495
|
if len(value) != self.cron_length:
|
477
496
|
raise ValueError(
|
478
497
|
f"Invalid cron value does not have length equal "
|
479
498
|
f"{self.cron_length}: {value}."
|
480
499
|
)
|
481
|
-
self.
|
482
|
-
|
483
|
-
|
484
|
-
|
500
|
+
self.options: Options = Options(**(option or {}))
|
501
|
+
|
502
|
+
# NOTE: Start initial crontab for each part
|
503
|
+
self.parts: list[CronPart] = [
|
504
|
+
CronPart(unit, values=item, options=self.options)
|
505
|
+
for item, unit in zip(value, self.cron_units)
|
485
506
|
]
|
507
|
+
|
508
|
+
# NOTE: Validate values of `day` and `dow` from parts.
|
486
509
|
if self.day == self.dow == []:
|
487
510
|
raise ValueError(
|
488
511
|
"Invalid cron value when set the `?` on day of month and "
|
@@ -490,12 +513,13 @@ class CronJob:
|
|
490
513
|
)
|
491
514
|
|
492
515
|
def __str__(self) -> str:
|
493
|
-
|
516
|
+
"""Return joining with space of each value in parts."""
|
517
|
+
return " ".join(str(part) for part in self.parts)
|
494
518
|
|
495
519
|
def __repr__(self) -> str:
|
496
520
|
return (
|
497
521
|
f"{self.__class__.__name__}(value={self.__str__()!r}, "
|
498
|
-
f"option={self.
|
522
|
+
f"option={self.options.__dict__})"
|
499
523
|
)
|
500
524
|
|
501
525
|
def __lt__(self, other) -> bool:
|
@@ -510,48 +534,63 @@ class CronJob:
|
|
510
534
|
for part, other_part in zip(self.parts, other.parts)
|
511
535
|
)
|
512
536
|
|
513
|
-
@property
|
514
|
-
def parts(self) -> list[CronPart]:
|
515
|
-
return self._parts
|
516
|
-
|
517
537
|
@property
|
518
538
|
def parts_order(self) -> Iterator[CronPart]:
|
519
539
|
return reversed(self.parts[:3] + [self.parts[4], self.parts[3]])
|
520
540
|
|
521
541
|
@property
|
522
|
-
def minute(self):
|
542
|
+
def minute(self) -> CronPart:
|
523
543
|
"""Return part of minute."""
|
524
|
-
return self.
|
544
|
+
return self.parts[0]
|
525
545
|
|
526
546
|
@property
|
527
|
-
def hour(self):
|
547
|
+
def hour(self) -> CronPart:
|
528
548
|
"""Return part of hour."""
|
529
|
-
return self.
|
549
|
+
return self.parts[1]
|
530
550
|
|
531
551
|
@property
|
532
|
-
def day(self):
|
552
|
+
def day(self) -> CronPart:
|
533
553
|
"""Return part of day."""
|
534
|
-
return self.
|
554
|
+
return self.parts[2]
|
535
555
|
|
536
556
|
@property
|
537
|
-
def month(self):
|
557
|
+
def month(self) -> CronPart:
|
538
558
|
"""Return part of month."""
|
539
|
-
return self.
|
559
|
+
return self.parts[3]
|
540
560
|
|
541
561
|
@property
|
542
|
-
def dow(self):
|
562
|
+
def dow(self) -> CronPart:
|
543
563
|
"""Return part of day of month."""
|
544
|
-
return self.
|
564
|
+
return self.parts[4]
|
545
565
|
|
546
566
|
def to_list(self) -> list[list[int]]:
|
547
567
|
"""Returns the cron schedule as a 2-dimensional list of integers."""
|
548
|
-
return [part.values for part in self.
|
568
|
+
return [part.values for part in self.parts]
|
549
569
|
|
550
570
|
def schedule(
|
551
|
-
self,
|
571
|
+
self,
|
572
|
+
date: datetime | None = None,
|
573
|
+
*,
|
574
|
+
tz: str | None = None,
|
552
575
|
) -> CronRunner:
|
553
|
-
"""Returns the
|
554
|
-
|
576
|
+
"""Returns the schedule datetime runner with this cronjob. It would run
|
577
|
+
``next``, ``prev``, or ``reset`` to generate running date that you want.
|
578
|
+
|
579
|
+
:param date: An initial date that want to mark as the start point.
|
580
|
+
:param tz: A string timezone that want to change on runner.
|
581
|
+
:rtype: CronRunner
|
582
|
+
"""
|
583
|
+
return CronRunner(self, date, tz=tz)
|
584
|
+
|
585
|
+
|
586
|
+
class CronJobYear(CronJob):
|
587
|
+
cron_length = 6
|
588
|
+
cron_units = CRON_UNITS_YEAR
|
589
|
+
|
590
|
+
@property
|
591
|
+
def year(self) -> CronPart:
|
592
|
+
"""Return part of year."""
|
593
|
+
return self.parts[5]
|
555
594
|
|
556
595
|
|
557
596
|
class CronRunner:
|
@@ -569,33 +608,37 @@ class CronRunner:
|
|
569
608
|
|
570
609
|
def __init__(
|
571
610
|
self,
|
572
|
-
cron: CronJob,
|
573
|
-
date:
|
611
|
+
cron: CronJob | CronJobYear,
|
612
|
+
date: datetime | None = None,
|
574
613
|
*,
|
575
|
-
|
614
|
+
tz: str | None = None,
|
576
615
|
) -> None:
|
577
|
-
# NOTE: Prepare
|
578
|
-
self.tz =
|
579
|
-
if
|
616
|
+
# NOTE: Prepare timezone if this value does not set, it will use UTC.
|
617
|
+
self.tz: ZoneInfo = ZoneInfo("UTC")
|
618
|
+
if tz:
|
580
619
|
try:
|
581
|
-
self.tz = ZoneInfo(
|
620
|
+
self.tz = ZoneInfo(tz)
|
582
621
|
except ZoneInfoNotFoundError as err:
|
583
|
-
raise ValueError(f"Invalid timezone: {
|
622
|
+
raise ValueError(f"Invalid timezone: {tz}") from err
|
623
|
+
|
624
|
+
# NOTE: Prepare date
|
584
625
|
if date:
|
585
626
|
if not isinstance(date, datetime):
|
586
627
|
raise ValueError(
|
587
628
|
"Input schedule start time is not a valid datetime object."
|
588
629
|
)
|
589
|
-
|
590
|
-
|
630
|
+
if tz is None:
|
631
|
+
self.tz = date.tzinfo
|
632
|
+
self.date: datetime = date.astimezone(self.tz)
|
591
633
|
else:
|
592
634
|
self.date: datetime = datetime.now(tz=self.tz)
|
593
635
|
|
636
|
+
# NOTE: Add one minute if the second value more than 0.
|
594
637
|
if self.date.second > 0:
|
595
|
-
self.date: datetime = self.date + timedelta(minutes
|
638
|
+
self.date: datetime = self.date + timedelta(minutes=1)
|
596
639
|
|
597
640
|
self.__start_date: datetime = self.date
|
598
|
-
self.cron: CronJob = cron
|
641
|
+
self.cron: CronJob | CronJobYear = cron
|
599
642
|
self.reset_flag: bool = True
|
600
643
|
|
601
644
|
def reset(self) -> None:
|
@@ -620,7 +663,11 @@ class CronRunner:
|
|
620
663
|
return self.find_date(reverse=True)
|
621
664
|
|
622
665
|
def find_date(self, reverse: bool = False) -> datetime:
|
623
|
-
"""Returns the time the schedule would run by `next` or `prev`.
|
666
|
+
"""Returns the time the schedule would run by `next` or `prev`.
|
667
|
+
|
668
|
+
:param reverse: A reverse flag.
|
669
|
+
"""
|
670
|
+
# NOTE: Set reset flag to false if start any action.
|
624
671
|
self.reset_flag: bool = False
|
625
672
|
for _ in range(25):
|
626
673
|
if all(
|
@@ -639,7 +686,7 @@ class CronRunner:
|
|
639
686
|
"minute": "hour",
|
640
687
|
}
|
641
688
|
current_value: int = getattr(self.date, switch[mode])
|
642
|
-
|
689
|
+
_addition_condition: Callable[[], bool] = (
|
643
690
|
(
|
644
691
|
lambda: WEEKDAYS.get(self.date.strftime("%a"))
|
645
692
|
not in self.cron.dow.values
|
@@ -647,21 +694,20 @@ class CronRunner:
|
|
647
694
|
if mode == "day"
|
648
695
|
else lambda: False
|
649
696
|
)
|
697
|
+
# NOTE: Start while-loop for checking this date include in this cronjob.
|
650
698
|
while (
|
651
699
|
getattr(self.date, mode) not in getattr(self.cron, mode).values
|
652
|
-
) or
|
653
|
-
self.date: datetime = next_date(
|
654
|
-
|
655
|
-
)
|
656
|
-
self.date: datetime = replace_date(
|
657
|
-
self.date, mode=mode, reverse=reverse
|
658
|
-
)
|
700
|
+
) or _addition_condition():
|
701
|
+
self.date: datetime = next_date(self.date, mode, reverse=reverse)
|
702
|
+
self.date: datetime = replace_date(self.date, mode, reverse=reverse)
|
659
703
|
if current_value != getattr(self.date, switch[mode]):
|
660
704
|
return mode != "month"
|
661
705
|
return False
|
662
706
|
|
663
707
|
|
664
|
-
__all__
|
708
|
+
__all__ = (
|
665
709
|
"CronJob",
|
710
|
+
"CronJobYear",
|
666
711
|
"CronRunner",
|
712
|
+
"WEEKDAYS",
|
667
713
|
)
|