ddeutil-workflow 0.0.39__tar.gz → 0.0.40__tar.gz
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-0.0.39/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.40}/PKG-INFO +4 -4
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/pyproject.toml +3 -3
- ddeutil_workflow-0.0.40/src/ddeutil/workflow/__about__.py +1 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__cron.py +89 -25
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/conf.py +8 -23
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/scheduler.py +40 -1
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/workflow.py +48 -9
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40/src/ddeutil_workflow.egg-info}/PKG-INFO +4 -4
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/requires.txt +3 -3
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule.py +15 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow.py +32 -1
- ddeutil_workflow-0.0.39/src/ddeutil/workflow/__about__.py +0 -1
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/LICENSE +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/README.md +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__init__.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/api.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/log.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/repeat.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/logs.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/audit.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/caller.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/context.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/cron.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/exceptions.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/job.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/logs.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/result.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/stages.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/templates.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/utils.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_audit.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_call_tag.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_context.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_cron_on.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_exec.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_strategy.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_logs.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_release.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_release_queue.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_result.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_pending.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_tasks.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_workflow.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_scheduler_control.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_stage.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_stage_handler_exec.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_templates.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_templates_filter.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_poke.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_release.py +0 -0
- {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.40
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -23,9 +23,9 @@ Requires-Python: >=3.9.13
|
|
23
23
|
Description-Content-Type: text/markdown
|
24
24
|
License-File: LICENSE
|
25
25
|
Requires-Dist: ddeutil>=0.4.6
|
26
|
-
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.
|
27
|
-
Requires-Dist: pydantic==2.
|
28
|
-
Requires-Dist: python-dotenv==1.0
|
26
|
+
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.10
|
27
|
+
Requires-Dist: pydantic==2.11.1
|
28
|
+
Requires-Dist: python-dotenv==1.1.0
|
29
29
|
Requires-Dist: schedule<2.0.0,==1.2.2
|
30
30
|
Provides-Extra: api
|
31
31
|
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
|
@@ -27,9 +27,9 @@ classifiers = [
|
|
27
27
|
requires-python = ">=3.9.13"
|
28
28
|
dependencies = [
|
29
29
|
"ddeutil>=0.4.6",
|
30
|
-
"ddeutil-io[yaml,toml]>=0.2.
|
31
|
-
"pydantic==2.
|
32
|
-
"python-dotenv==1.0
|
30
|
+
"ddeutil-io[yaml,toml]>=0.2.10",
|
31
|
+
"pydantic==2.11.1",
|
32
|
+
"python-dotenv==1.1.0",
|
33
33
|
"schedule==1.2.2,<2.0.0",
|
34
34
|
]
|
35
35
|
dynamic = ["version"]
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.40"
|
@@ -37,7 +37,7 @@ class CronYearLimit(Exception): ...
|
|
37
37
|
def str2cron(value: str) -> str: # pragma: no cov
|
38
38
|
"""Convert Special String with the @ prefix to Crontab value.
|
39
39
|
|
40
|
-
:param value: A string value that want to convert to cron value.
|
40
|
+
:param value: (str) A string value that want to convert to cron value.
|
41
41
|
:rtype: str
|
42
42
|
|
43
43
|
Table:
|
@@ -82,6 +82,9 @@ class Unit:
|
|
82
82
|
)
|
83
83
|
|
84
84
|
|
85
|
+
Units = tuple[Unit, ...]
|
86
|
+
|
87
|
+
|
85
88
|
@dataclass
|
86
89
|
class Options:
|
87
90
|
"""Options dataclass for config CronPart object."""
|
@@ -91,7 +94,7 @@ class Options:
|
|
91
94
|
output_hashes: bool = False
|
92
95
|
|
93
96
|
|
94
|
-
CRON_UNITS:
|
97
|
+
CRON_UNITS: Units = (
|
95
98
|
Unit(
|
96
99
|
name="minute",
|
97
100
|
range=partial(range, 0, 60),
|
@@ -147,7 +150,7 @@ CRON_UNITS: tuple[Unit, ...] = (
|
|
147
150
|
),
|
148
151
|
)
|
149
152
|
|
150
|
-
CRON_UNITS_YEAR:
|
153
|
+
CRON_UNITS_YEAR: Units = CRON_UNITS + (
|
151
154
|
Unit(
|
152
155
|
name="year",
|
153
156
|
range=partial(range, 1990, 2101),
|
@@ -220,18 +223,21 @@ class CronPart:
|
|
220
223
|
return ",".join(cron_range_strings) if cron_range_strings else "?"
|
221
224
|
|
222
225
|
def __repr__(self) -> str:
|
226
|
+
"""Override __repr__ method."""
|
223
227
|
return (
|
224
228
|
f"{self.__class__.__name__}"
|
225
229
|
f"(unit={self.unit}, values={self.__str__()!r})"
|
226
230
|
)
|
227
231
|
|
228
232
|
def __lt__(self, other) -> bool:
|
233
|
+
"""Override __lt__ method."""
|
229
234
|
if isinstance(other, CronPart):
|
230
235
|
return self.values < other.values
|
231
236
|
elif isinstance(other, list):
|
232
237
|
return self.values < other
|
233
238
|
|
234
239
|
def __eq__(self, other) -> bool:
|
240
|
+
"""Override __eq__ method."""
|
235
241
|
if isinstance(other, CronPart):
|
236
242
|
return self.values == other.values
|
237
243
|
elif isinstance(other, list):
|
@@ -239,18 +245,26 @@ class CronPart:
|
|
239
245
|
|
240
246
|
@property
|
241
247
|
def min(self) -> int:
|
242
|
-
"""Returns the smallest value in the range.
|
248
|
+
"""Returns the smallest value in the range.
|
249
|
+
|
250
|
+
:rtype: int
|
251
|
+
"""
|
243
252
|
return self.values[0]
|
244
253
|
|
245
254
|
@property
|
246
255
|
def max(self) -> int:
|
247
|
-
"""Returns the largest value in the range.
|
256
|
+
"""Returns the largest value in the range.
|
257
|
+
|
258
|
+
:rtype: int
|
259
|
+
"""
|
248
260
|
return self.values[-1]
|
249
261
|
|
250
262
|
@property
|
251
263
|
def step(self) -> Optional[int]:
|
252
264
|
"""Returns the difference between first and second elements in the
|
253
265
|
range.
|
266
|
+
|
267
|
+
:rtype: Optional[int]
|
254
268
|
"""
|
255
269
|
if (
|
256
270
|
len(self.values) > 2
|
@@ -260,15 +274,17 @@ class CronPart:
|
|
260
274
|
|
261
275
|
@property
|
262
276
|
def is_full(self) -> bool:
|
263
|
-
"""Returns true if range has all the values of the unit.
|
277
|
+
"""Returns true if range has all the values of the unit.
|
278
|
+
|
279
|
+
:rtype: bool
|
280
|
+
"""
|
264
281
|
return len(self.values) == (self.unit.max - self.unit.min + 1)
|
265
282
|
|
266
283
|
def from_str(self, value: str) -> tuple[int, ...]:
|
267
284
|
"""Parses a string as a range of positive integers. The string should
|
268
285
|
include only `-` and `,` special strings.
|
269
286
|
|
270
|
-
:param value: A string value that want to parse
|
271
|
-
:type value: str
|
287
|
+
:param value: (str) A string value that want to parse
|
272
288
|
|
273
289
|
TODO: support for `L`, `W`, and `#`
|
274
290
|
---
|
@@ -351,6 +367,8 @@ class CronPart:
|
|
351
367
|
For example if value == 'JAN,AUG' it will replace to '1,8'.
|
352
368
|
|
353
369
|
:param value: A string value that want to replace alternative to int.
|
370
|
+
|
371
|
+
:rtype: str
|
354
372
|
"""
|
355
373
|
for i, alt in enumerate(self.unit.alt):
|
356
374
|
if alt in value:
|
@@ -361,6 +379,7 @@ class CronPart:
|
|
361
379
|
"""Replaces all 7 with 0 as Sunday can be represented by both.
|
362
380
|
|
363
381
|
:param values: list or iter of int that want to mode by 7
|
382
|
+
|
364
383
|
:rtype: list[int]
|
365
384
|
"""
|
366
385
|
if self.unit.name == "weekday":
|
@@ -388,7 +407,13 @@ class CronPart:
|
|
388
407
|
return values
|
389
408
|
|
390
409
|
def _parse_range(self, value: str) -> list[int]:
|
391
|
-
"""Parses a range string.
|
410
|
+
"""Parses a range string from a cron-part.
|
411
|
+
|
412
|
+
:param value: (str) A cron-part string value that want to parse.
|
413
|
+
|
414
|
+
:rtype: list[int]
|
415
|
+
:return: A list of parse range.
|
416
|
+
"""
|
392
417
|
if value == "*":
|
393
418
|
return list(self.unit.range())
|
394
419
|
elif value.count("-") > 1:
|
@@ -410,7 +435,13 @@ class CronPart:
|
|
410
435
|
values: list[int],
|
411
436
|
step: int | None = None,
|
412
437
|
) -> list[int]:
|
413
|
-
"""Applies an interval step to a collection of values.
|
438
|
+
"""Applies an interval step to a collection of values.
|
439
|
+
|
440
|
+
:param values:
|
441
|
+
:param step:
|
442
|
+
|
443
|
+
:rtype: list[int]
|
444
|
+
"""
|
414
445
|
if not step:
|
415
446
|
return values
|
416
447
|
elif (_step := int(step)) < 1:
|
@@ -427,7 +458,10 @@ class CronPart:
|
|
427
458
|
|
428
459
|
@property
|
429
460
|
def is_interval(self) -> bool:
|
430
|
-
"""Returns true if the range can be represented as an interval.
|
461
|
+
"""Returns true if the range can be represented as an interval.
|
462
|
+
|
463
|
+
:rtype: bool
|
464
|
+
"""
|
431
465
|
if not (step := self.step):
|
432
466
|
return False
|
433
467
|
for idx, value in enumerate(self.values):
|
@@ -439,7 +473,10 @@ class CronPart:
|
|
439
473
|
|
440
474
|
@property
|
441
475
|
def is_full_interval(self) -> bool:
|
442
|
-
"""Returns true if the range contains all the interval values.
|
476
|
+
"""Returns true if the range contains all the interval values.
|
477
|
+
|
478
|
+
:rtype: bool
|
479
|
+
"""
|
443
480
|
if step := self.step:
|
444
481
|
return (
|
445
482
|
self.min == self.unit.min
|
@@ -482,8 +519,7 @@ class CronPart:
|
|
482
519
|
"""Formats weekday and month names as string when the relevant options
|
483
520
|
are set.
|
484
521
|
|
485
|
-
:param value:
|
486
|
-
:type value: int
|
522
|
+
:param value: (int) An int value that want to get from the unit.
|
487
523
|
|
488
524
|
:rtype: int | str
|
489
525
|
"""
|
@@ -538,8 +574,8 @@ class CronJob:
|
|
538
574
|
monitor-data-warehouse-schedule.html
|
539
575
|
"""
|
540
576
|
|
541
|
-
cron_length: int = 5
|
542
|
-
cron_units:
|
577
|
+
cron_length: ClassVar[int] = 5
|
578
|
+
cron_units: ClassVar[Units] = CRON_UNITS
|
543
579
|
|
544
580
|
def __init__(
|
545
581
|
self,
|
@@ -600,38 +636,64 @@ class CronJob:
|
|
600
636
|
|
601
637
|
@property
|
602
638
|
def parts_order(self) -> Iterator[CronPart]:
|
639
|
+
"""Return iterator of CronPart instance.
|
640
|
+
|
641
|
+
:rtype: Iterator[CronPart]
|
642
|
+
"""
|
603
643
|
return reversed(self.parts[:3] + [self.parts[4], self.parts[3]])
|
604
644
|
|
605
645
|
@property
|
606
646
|
def minute(self) -> CronPart:
|
607
|
-
"""Return part of minute.
|
647
|
+
"""Return part of minute with the CronPart instance.
|
648
|
+
|
649
|
+
:rtype: CronPart
|
650
|
+
"""
|
608
651
|
return self.parts[0]
|
609
652
|
|
610
653
|
@property
|
611
654
|
def hour(self) -> CronPart:
|
612
|
-
"""Return part of hour.
|
655
|
+
"""Return part of hour with the CronPart instance.
|
656
|
+
|
657
|
+
:rtype: CronPart
|
658
|
+
"""
|
613
659
|
return self.parts[1]
|
614
660
|
|
615
661
|
@property
|
616
662
|
def day(self) -> CronPart:
|
617
|
-
"""Return part of day.
|
663
|
+
"""Return part of day with the CronPart instance.
|
664
|
+
|
665
|
+
:rtype: CronPart
|
666
|
+
"""
|
618
667
|
return self.parts[2]
|
619
668
|
|
620
669
|
@property
|
621
670
|
def month(self) -> CronPart:
|
622
|
-
"""Return part of month.
|
671
|
+
"""Return part of month with the CronPart instance.
|
672
|
+
|
673
|
+
:rtype: CronPart
|
674
|
+
"""
|
623
675
|
return self.parts[3]
|
624
676
|
|
625
677
|
@property
|
626
678
|
def dow(self) -> CronPart:
|
627
|
-
"""Return part of day of month.
|
679
|
+
"""Return part of day of month with the CronPart instance.
|
680
|
+
|
681
|
+
:rtype: CronPart
|
682
|
+
"""
|
628
683
|
return self.parts[4]
|
629
684
|
|
630
685
|
def to_list(self) -> list[list[int]]:
|
631
|
-
"""Returns the cron schedule as a 2-dimensional list of integers.
|
686
|
+
"""Returns the cron schedule as a 2-dimensional list of integers.
|
687
|
+
|
688
|
+
:rtype: list[list[int]]
|
689
|
+
"""
|
632
690
|
return [part.values for part in self.parts]
|
633
691
|
|
634
692
|
def check(self, date: datetime, mode: str) -> bool:
|
693
|
+
"""Check the date value with the mode.
|
694
|
+
|
695
|
+
:rtype: bool
|
696
|
+
"""
|
635
697
|
assert mode in ("year", "month", "day", "hour", "minute")
|
636
698
|
return getattr(date, mode) in getattr(self, mode).values
|
637
699
|
|
@@ -667,12 +729,14 @@ class CronJobYear(CronJob):
|
|
667
729
|
(vi) year (1990 - 2100)
|
668
730
|
"""
|
669
731
|
|
670
|
-
cron_length = 6
|
671
|
-
cron_units = CRON_UNITS_YEAR
|
732
|
+
cron_length: ClassVar[int] = 6
|
733
|
+
cron_units: ClassVar[Units] = CRON_UNITS_YEAR
|
672
734
|
|
673
735
|
@property
|
674
736
|
def year(self) -> CronPart:
|
675
|
-
"""Return part of year.
|
737
|
+
"""Return part of year with the CronPart instance.
|
738
|
+
|
739
|
+
:rtype: CronPart"""
|
676
740
|
return self.parts[5]
|
677
741
|
|
678
742
|
|
@@ -16,6 +16,7 @@ from zoneinfo import ZoneInfo
|
|
16
16
|
|
17
17
|
from ddeutil.core import str2bool
|
18
18
|
from ddeutil.io import YamlFlResolve
|
19
|
+
from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
19
20
|
|
20
21
|
from .__types import DictData, TupleStr
|
21
22
|
|
@@ -26,10 +27,6 @@ def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
|
26
27
|
return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
|
27
28
|
|
28
29
|
|
29
|
-
def glob_files(path: Path) -> Iterator[Path]: # pragma: no cov
|
30
|
-
yield from (file for file in path.rglob("*") if file.is_file())
|
31
|
-
|
32
|
-
|
33
30
|
__all__: TupleStr = (
|
34
31
|
"env",
|
35
32
|
"get_logger",
|
@@ -37,7 +34,6 @@ __all__: TupleStr = (
|
|
37
34
|
"SimLoad",
|
38
35
|
"Loader",
|
39
36
|
"config",
|
40
|
-
"glob_files",
|
41
37
|
)
|
42
38
|
|
43
39
|
|
@@ -273,7 +269,7 @@ class SimLoad:
|
|
273
269
|
if self.is_ignore(file, conf_path):
|
274
270
|
continue
|
275
271
|
|
276
|
-
if data := self.
|
272
|
+
if data := self.filter_yaml(file, name=name):
|
277
273
|
self.data = data
|
278
274
|
|
279
275
|
# VALIDATE: check the data that reading should not empty.
|
@@ -307,15 +303,15 @@ class SimLoad:
|
|
307
303
|
exclude: list[str] = excluded or []
|
308
304
|
for file in glob_files(conf_path):
|
309
305
|
|
310
|
-
|
306
|
+
if cls.is_ignore(file, conf_path):
|
307
|
+
continue
|
311
308
|
|
312
|
-
|
313
|
-
continue
|
309
|
+
for key, data in cls.filter_yaml(file).items():
|
314
310
|
|
315
311
|
if key in exclude:
|
316
312
|
continue
|
317
313
|
|
318
|
-
if data
|
314
|
+
if data.get("type", "") == obj.__name__:
|
319
315
|
yield key, (
|
320
316
|
{k: data[k] for k in data if k in included}
|
321
317
|
if included
|
@@ -324,24 +320,13 @@ class SimLoad:
|
|
324
320
|
|
325
321
|
@classmethod
|
326
322
|
def is_ignore(cls, file: Path, conf_path: Path) -> bool:
|
327
|
-
|
328
|
-
ignore: list[str] = []
|
329
|
-
if ignore_file.exists():
|
330
|
-
ignore = ignore_file.read_text(encoding="utf-8").splitlines()
|
331
|
-
|
332
|
-
if any(
|
333
|
-
(file.match(f"**/{pattern}/*") or file.match(f"**/{pattern}*"))
|
334
|
-
for pattern in ignore
|
335
|
-
):
|
336
|
-
return True
|
337
|
-
return False
|
323
|
+
return is_ignored(file, read_ignore(conf_path / ".confignore"))
|
338
324
|
|
339
325
|
@classmethod
|
340
|
-
def
|
326
|
+
def filter_yaml(cls, file: Path, name: str | None = None) -> DictData:
|
341
327
|
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")):
|
342
328
|
values: DictData = YamlFlResolve(file).read()
|
343
329
|
return values.get(name, {}) if name else values
|
344
|
-
|
345
330
|
return {}
|
346
331
|
|
347
332
|
@cached_property
|
@@ -31,6 +31,7 @@ from concurrent.futures import (
|
|
31
31
|
from datetime import datetime, timedelta
|
32
32
|
from functools import wraps
|
33
33
|
from heapq import heappop, heappush
|
34
|
+
from pathlib import Path
|
34
35
|
from textwrap import dedent
|
35
36
|
from threading import Thread
|
36
37
|
from typing import Callable, Optional, TypedDict, Union
|
@@ -52,7 +53,7 @@ except ImportError: # pragma: no cov
|
|
52
53
|
from .__cron import CronRunner
|
53
54
|
from .__types import DictData, TupleStr
|
54
55
|
from .audit import Audit, get_audit
|
55
|
-
from .conf import Loader, config, get_logger
|
56
|
+
from .conf import Loader, SimLoad, config, get_logger
|
56
57
|
from .cron import On
|
57
58
|
from .exceptions import ScheduleException, WorkflowException
|
58
59
|
from .result import Result, Status
|
@@ -266,6 +267,8 @@ class Schedule(BaseModel):
|
|
266
267
|
:param externals: An external parameters that want to pass to Loader
|
267
268
|
object.
|
268
269
|
|
270
|
+
:raise ValueError: If the type does not match with current object.
|
271
|
+
|
269
272
|
:rtype: Self
|
270
273
|
"""
|
271
274
|
loader: Loader = Loader(name, externals=(externals or {}))
|
@@ -281,6 +284,42 @@ class Schedule(BaseModel):
|
|
281
284
|
|
282
285
|
return cls.model_validate(obj=loader_data)
|
283
286
|
|
287
|
+
@classmethod
|
288
|
+
def from_path(
|
289
|
+
cls,
|
290
|
+
name: str,
|
291
|
+
path: Path,
|
292
|
+
externals: DictData | None = None,
|
293
|
+
) -> Self:
|
294
|
+
"""Create Schedule instance from the SimLoad object that receive an
|
295
|
+
input schedule name and conf path. The loader object will use this
|
296
|
+
schedule name to searching configuration data of this schedule model
|
297
|
+
in conf path.
|
298
|
+
|
299
|
+
:param name: (str) A schedule name that want to pass to Loader object.
|
300
|
+
:param path: (Path) A config path that want to search.
|
301
|
+
:param externals: An external parameters that want to pass to Loader
|
302
|
+
object.
|
303
|
+
|
304
|
+
:raise ValueError: If the type does not match with current object.
|
305
|
+
|
306
|
+
:rtype: Self
|
307
|
+
"""
|
308
|
+
loader: SimLoad = SimLoad(
|
309
|
+
name, conf_path=path, externals=(externals or {})
|
310
|
+
)
|
311
|
+
|
312
|
+
# NOTE: Validate the config type match with current connection model
|
313
|
+
if loader.type != cls.__name__:
|
314
|
+
raise ValueError(f"Type {loader.type} does not match with {cls}")
|
315
|
+
|
316
|
+
loader_data: DictData = copy.deepcopy(loader.data)
|
317
|
+
|
318
|
+
# NOTE: Add name to loader data
|
319
|
+
loader_data["name"] = name.replace(" ", "_")
|
320
|
+
|
321
|
+
return cls.model_validate(obj=loader_data)
|
322
|
+
|
284
323
|
def tasks(
|
285
324
|
self,
|
286
325
|
start_date: datetime,
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""A Workflow module."""
|
6
|
+
"""A Workflow module that is the core model of this package."""
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
9
|
import copy
|
@@ -18,6 +18,7 @@ from datetime import datetime, timedelta
|
|
18
18
|
from enum import Enum
|
19
19
|
from functools import partial, total_ordering
|
20
20
|
from heapq import heappop, heappush
|
21
|
+
from pathlib import Path
|
21
22
|
from queue import Queue
|
22
23
|
from textwrap import dedent
|
23
24
|
from threading import Event
|
@@ -31,7 +32,7 @@ from typing_extensions import Self
|
|
31
32
|
from .__cron import CronJob, CronRunner
|
32
33
|
from .__types import DictData, TupleStr
|
33
34
|
from .audit import Audit, get_audit
|
34
|
-
from .conf import Loader, config, get_logger
|
35
|
+
from .conf import Loader, SimLoad, config, get_logger
|
35
36
|
from .cron import On
|
36
37
|
from .exceptions import JobException, WorkflowException
|
37
38
|
from .job import Job, TriggerState
|
@@ -299,33 +300,71 @@ class Workflow(BaseModel):
|
|
299
300
|
|
300
301
|
# NOTE: Add name to loader data
|
301
302
|
loader_data["name"] = name.replace(" ", "_")
|
303
|
+
cls.__bypass_on__(
|
304
|
+
loader_data, path=loader.conf_path, externals=externals
|
305
|
+
)
|
306
|
+
return cls.model_validate(obj=loader_data)
|
307
|
+
|
308
|
+
@classmethod
|
309
|
+
def from_path(
|
310
|
+
cls,
|
311
|
+
name: str,
|
312
|
+
path: Path,
|
313
|
+
externals: DictData | None = None,
|
314
|
+
) -> Self:
|
315
|
+
"""Create Workflow instance from the specific path. The loader object
|
316
|
+
will use this workflow name and path to searching configuration data of
|
317
|
+
this workflow model.
|
318
|
+
|
319
|
+
:param name: (str) A workflow name that want to pass to Loader object.
|
320
|
+
:param path: (Path) A config path that want to search.
|
321
|
+
:param externals: An external parameters that want to pass to Loader
|
322
|
+
object.
|
323
|
+
|
324
|
+
:raise ValueError: If the type does not match with current object.
|
302
325
|
|
303
|
-
|
304
|
-
|
326
|
+
:rtype: Self
|
327
|
+
"""
|
328
|
+
loader: SimLoad = SimLoad(
|
329
|
+
name, conf_path=path, externals=(externals or {})
|
330
|
+
)
|
331
|
+
# NOTE: Validate the config type match with current connection model
|
332
|
+
if loader.type != cls.__name__:
|
333
|
+
raise ValueError(f"Type {loader.type} does not match with {cls}")
|
334
|
+
|
335
|
+
loader_data: DictData = copy.deepcopy(loader.data)
|
336
|
+
|
337
|
+
# NOTE: Add name to loader data
|
338
|
+
loader_data["name"] = name.replace(" ", "_")
|
339
|
+
cls.__bypass_on__(loader_data, path=path, externals=externals)
|
305
340
|
return cls.model_validate(obj=loader_data)
|
306
341
|
|
307
342
|
@classmethod
|
308
343
|
def __bypass_on__(
|
309
344
|
cls,
|
310
345
|
data: DictData,
|
346
|
+
path: Path,
|
311
347
|
externals: DictData | None = None,
|
312
348
|
) -> DictData:
|
313
349
|
"""Bypass the on data to loaded config data.
|
314
350
|
|
315
|
-
:param data:
|
316
|
-
:param
|
351
|
+
:param data: A data to construct to this Workflow model.
|
352
|
+
:param path: A config path.
|
353
|
+
:param externals: An external parameters that want to pass to SimLoad
|
354
|
+
object.
|
317
355
|
:rtype: DictData
|
318
356
|
"""
|
319
357
|
if on := data.pop("on", []):
|
320
358
|
if isinstance(on, str):
|
321
|
-
on = [on]
|
359
|
+
on: list[str] = [on]
|
322
360
|
if any(not isinstance(i, (dict, str)) for i in on):
|
323
361
|
raise TypeError("The ``on`` key should be list of str or dict")
|
324
362
|
|
325
|
-
# NOTE: Pass on value to
|
363
|
+
# NOTE: Pass on value to SimLoad and keep on model object to the on
|
364
|
+
# field.
|
326
365
|
data["on"] = [
|
327
366
|
(
|
328
|
-
|
367
|
+
SimLoad(n, conf_path=path, externals=(externals or {})).data
|
329
368
|
if isinstance(n, str)
|
330
369
|
else n
|
331
370
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.40
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -23,9 +23,9 @@ Requires-Python: >=3.9.13
|
|
23
23
|
Description-Content-Type: text/markdown
|
24
24
|
License-File: LICENSE
|
25
25
|
Requires-Dist: ddeutil>=0.4.6
|
26
|
-
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.
|
27
|
-
Requires-Dist: pydantic==2.
|
28
|
-
Requires-Dist: python-dotenv==1.0
|
26
|
+
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.10
|
27
|
+
Requires-Dist: pydantic==2.11.1
|
28
|
+
Requires-Dist: python-dotenv==1.1.0
|
29
29
|
Requires-Dist: schedule<2.0.0,==1.2.2
|
30
30
|
Provides-Extra: api
|
31
31
|
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
|
@@ -47,6 +47,9 @@ def test_schedule_from_loader_raise(test_path):
|
|
47
47
|
with pytest.raises(ValueError):
|
48
48
|
Schedule.from_loader("schedule-raise-wf")
|
49
49
|
|
50
|
+
with pytest.raises(ValueError):
|
51
|
+
Schedule.from_path("schedule-raise-wf", path=test_path / "conf")
|
52
|
+
|
50
53
|
with test_file.open(mode="w") as f:
|
51
54
|
yaml.dump(
|
52
55
|
{
|
@@ -69,6 +72,9 @@ def test_schedule_from_loader_raise(test_path):
|
|
69
72
|
with pytest.raises(TypeError):
|
70
73
|
Schedule.from_loader("schedule-raise-wf")
|
71
74
|
|
75
|
+
with pytest.raises(TypeError):
|
76
|
+
Schedule.from_path("schedule-raise-wf", path=test_path / "conf")
|
77
|
+
|
72
78
|
with test_file.open(mode="w") as f:
|
73
79
|
yaml.dump(
|
74
80
|
{
|
@@ -91,6 +97,9 @@ def test_schedule_from_loader_raise(test_path):
|
|
91
97
|
with pytest.raises(ValidationError):
|
92
98
|
Schedule.from_loader("schedule-raise-wf")
|
93
99
|
|
100
|
+
with pytest.raises(ValidationError):
|
101
|
+
Schedule.from_path("schedule-raise-wf", path=test_path / "conf")
|
102
|
+
|
94
103
|
test_file.unlink(missing_ok=True)
|
95
104
|
|
96
105
|
|
@@ -110,6 +119,12 @@ def test_schedule_default_on(test_path):
|
|
110
119
|
for sch_wf in schedule.workflows:
|
111
120
|
assert sch_wf.on == []
|
112
121
|
|
122
|
+
schedule = Schedule.from_path(
|
123
|
+
"tmp-schedule-default-wf", path=test_path / "conf"
|
124
|
+
)
|
125
|
+
for sch_wf in schedule.workflows:
|
126
|
+
assert sch_wf.on == []
|
127
|
+
|
113
128
|
|
114
129
|
def test_schedule_remove_workflow_task():
|
115
130
|
pipeline_tasks: list[WorkflowTask] = []
|
@@ -114,6 +114,19 @@ def test_workflow_desc():
|
|
114
114
|
|
115
115
|
def test_workflow_from_loader_without_job():
|
116
116
|
workflow = Workflow.from_loader(name="wf-without-jobs")
|
117
|
+
assert workflow.name == "wf-without-jobs"
|
118
|
+
|
119
|
+
rs = workflow.execute({})
|
120
|
+
assert rs.context == {}
|
121
|
+
|
122
|
+
|
123
|
+
def test_workflow_from_path(test_path):
|
124
|
+
workflow = Workflow.from_path(
|
125
|
+
name="wf-without-jobs",
|
126
|
+
path=test_path / "conf",
|
127
|
+
)
|
128
|
+
assert workflow.name == "wf-without-jobs"
|
129
|
+
|
117
130
|
rs = workflow.execute({})
|
118
131
|
assert rs.context == {}
|
119
132
|
|
@@ -139,6 +152,12 @@ def test_workflow_from_loader_raise(test_path):
|
|
139
152
|
with pytest.raises(ValueError):
|
140
153
|
Workflow.from_loader(name="wf-run-from-loader-raise")
|
141
154
|
|
155
|
+
with pytest.raises(ValueError):
|
156
|
+
Workflow.from_path(
|
157
|
+
name="wf-run-from-loader-raise",
|
158
|
+
path=test_path / "conf",
|
159
|
+
)
|
160
|
+
|
142
161
|
# NOTE: Raise if type of the on field does not valid with str or dict.
|
143
162
|
dump_yaml(
|
144
163
|
test_file,
|
@@ -161,7 +180,13 @@ def test_workflow_from_loader_raise(test_path):
|
|
161
180
|
with pytest.raises(TypeError):
|
162
181
|
Workflow.from_loader(name="wf-run-from-loader-raise")
|
163
182
|
|
164
|
-
|
183
|
+
with pytest.raises(TypeError):
|
184
|
+
Workflow.from_path(
|
185
|
+
name="wf-run-from-loader-raise",
|
186
|
+
path=test_path / "conf",
|
187
|
+
)
|
188
|
+
|
189
|
+
# NOTE: Raise if value of the on field does not parse to the CronJob obj.
|
165
190
|
dump_yaml(
|
166
191
|
test_file,
|
167
192
|
data={
|
@@ -182,6 +207,12 @@ def test_workflow_from_loader_raise(test_path):
|
|
182
207
|
with pytest.raises(WorkflowException):
|
183
208
|
Workflow.from_loader(name="wf-run-from-loader-raise")
|
184
209
|
|
210
|
+
with pytest.raises(WorkflowException):
|
211
|
+
Workflow.from_path(
|
212
|
+
name="wf-run-from-loader-raise",
|
213
|
+
path=test_path / "conf",
|
214
|
+
)
|
215
|
+
|
185
216
|
# NOTE: Remove the testing file on the demo path.
|
186
217
|
test_file.unlink(missing_ok=True)
|
187
218
|
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__: str = "0.0.39"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/schedules.py
RENAMED
File without changes
|
{ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/workflows.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|