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.
Files changed (71) hide show
  1. {ddeutil_workflow-0.0.39/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.40}/PKG-INFO +4 -4
  2. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/pyproject.toml +3 -3
  3. ddeutil_workflow-0.0.40/src/ddeutil/workflow/__about__.py +1 -0
  4. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__cron.py +89 -25
  5. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/conf.py +8 -23
  6. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/scheduler.py +40 -1
  7. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/workflow.py +48 -9
  8. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40/src/ddeutil_workflow.egg-info}/PKG-INFO +4 -4
  9. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/requires.txt +3 -3
  10. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule.py +15 -0
  11. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow.py +32 -1
  12. ddeutil_workflow-0.0.39/src/ddeutil/workflow/__about__.py +0 -1
  13. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/LICENSE +0 -0
  14. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/README.md +0 -0
  15. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/setup.cfg +0 -0
  16. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__init__.py +0 -0
  17. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/__types.py +0 -0
  18. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/__init__.py +0 -0
  19. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/api.py +0 -0
  20. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/log.py +0 -0
  21. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/repeat.py +0 -0
  22. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  23. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/job.py +0 -0
  24. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  25. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  26. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  27. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/audit.py +0 -0
  28. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/caller.py +0 -0
  29. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/context.py +0 -0
  30. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/cron.py +0 -0
  31. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/exceptions.py +0 -0
  32. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/job.py +0 -0
  33. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/logs.py +0 -0
  34. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/params.py +0 -0
  35. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/result.py +0 -0
  36. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/stages.py +0 -0
  37. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/templates.py +0 -0
  38. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil/workflow/utils.py +0 -0
  39. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  40. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  41. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  42. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test__cron.py +0 -0
  43. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test__regex.py +0 -0
  44. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_audit.py +0 -0
  45. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_call_tag.py +0 -0
  46. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_conf.py +0 -0
  47. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_context.py +0 -0
  48. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_cron_on.py +0 -0
  49. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job.py +0 -0
  50. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_exec.py +0 -0
  51. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_exec_strategy.py +0 -0
  52. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_job_strategy.py +0 -0
  53. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_logs.py +0 -0
  54. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_params.py +0 -0
  55. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_release.py +0 -0
  56. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_release_queue.py +0 -0
  57. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_result.py +0 -0
  58. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_pending.py +0 -0
  59. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_tasks.py +0 -0
  60. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_schedule_workflow.py +0 -0
  61. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_scheduler_control.py +0 -0
  62. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_stage.py +0 -0
  63. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_stage_handler_exec.py +0 -0
  64. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_templates.py +0 -0
  65. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_templates_filter.py +0 -0
  66. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_utils.py +0 -0
  67. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec.py +0 -0
  68. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_job.py +0 -0
  69. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_poke.py +0 -0
  70. {ddeutil_workflow-0.0.39 → ddeutil_workflow-0.0.40}/tests/test_workflow_exec_release.py +0 -0
  71. {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.39
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.3
27
- Requires-Dist: pydantic==2.10.6
28
- Requires-Dist: python-dotenv==1.0.1
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.3",
31
- "pydantic==2.10.6",
32
- "python-dotenv==1.0.1",
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: tuple[Unit, ...] = (
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: tuple[Unit, ...] = CRON_UNITS + (
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: a int 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: tuple[Unit, ...] = 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.filter_suffix(file, name=name):
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
- for key, data in cls.filter_suffix(file).items():
306
+ if cls.is_ignore(file, conf_path):
307
+ continue
311
308
 
312
- if cls.is_ignore(file, conf_path):
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["type"] == obj.__name__:
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
- ignore_file: Path = conf_path / ".confignore"
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 filter_suffix(cls, file: Path, name: str | None = None) -> DictData:
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
- # NOTE: Prepare `on` data
304
- cls.__bypass_on__(loader_data, externals=externals)
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 externals:
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 Loader and keep on model object to on field
363
+ # NOTE: Pass on value to SimLoad and keep on model object to the on
364
+ # field.
326
365
  data["on"] = [
327
366
  (
328
- Loader(n, externals=(externals or {})).data
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.39
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.3
27
- Requires-Dist: pydantic==2.10.6
28
- Requires-Dist: python-dotenv==1.0.1
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"
@@ -1,7 +1,7 @@
1
1
  ddeutil>=0.4.6
2
- ddeutil-io[toml,yaml]>=0.2.3
3
- pydantic==2.10.6
4
- python-dotenv==1.0.1
2
+ ddeutil-io[toml,yaml]>=0.2.10
3
+ pydantic==2.11.1
4
+ python-dotenv==1.1.0
5
5
  schedule<2.0.0,==1.2.2
6
6
 
7
7
  [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
- # NOTE: Raise if value of the on field does not parsing to the CronJob obj.
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"