ddeutil-workflow 0.0.13__tar.gz → 0.0.15__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 (62) hide show
  1. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/PKG-INFO +6 -4
  2. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/README.md +2 -1
  3. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/pyproject.toml +3 -3
  4. ddeutil_workflow-0.0.15/src/ddeutil/workflow/__about__.py +1 -0
  5. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/__init__.py +4 -1
  6. ddeutil_workflow-0.0.15/src/ddeutil/workflow/__types.py +113 -0
  7. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/api.py +2 -2
  8. ddeutil_workflow-0.0.15/src/ddeutil/workflow/conf.py +45 -0
  9. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/cron.py +19 -12
  10. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/job.py +191 -153
  11. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/log.py +28 -14
  12. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/scheduler.py +255 -119
  13. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/stage.py +77 -35
  14. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/utils.py +129 -51
  15. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/PKG-INFO +6 -4
  16. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/SOURCES.txt +13 -10
  17. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/requires.txt +3 -2
  18. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test__regex.py +33 -5
  19. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_job_py.py +12 -15
  20. ddeutil_workflow-0.0.15/tests/test_job_strategy.py +68 -0
  21. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_poke.py +1 -1
  22. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_scheduler.py +14 -12
  23. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage_py.py +15 -17
  24. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage_trigger.py +1 -1
  25. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_utils.py +1 -1
  26. ddeutil_workflow-0.0.15/tests/test_utils_result.py +82 -0
  27. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_utils_template.py +15 -7
  28. ddeutil_workflow-0.0.13/tests/test_pipeline.py → ddeutil_workflow-0.0.15/tests/test_workflow.py +2 -2
  29. ddeutil_workflow-0.0.15/tests/test_workflow_depends.py +74 -0
  30. ddeutil_workflow-0.0.13/tests/test_pipeline_desc.py → ddeutil_workflow-0.0.15/tests/test_workflow_desc.py +1 -1
  31. ddeutil_workflow-0.0.13/tests/test_pipeline_if.py → ddeutil_workflow-0.0.15/tests/test_workflow_if.py +1 -1
  32. ddeutil_workflow-0.0.13/tests/test_pipeline_matrix.py → ddeutil_workflow-0.0.15/tests/test_workflow_matrix.py +4 -4
  33. ddeutil_workflow-0.0.13/tests/test_pipeline_on.py → ddeutil_workflow-0.0.15/tests/test_workflow_on.py +1 -1
  34. ddeutil_workflow-0.0.13/tests/test_pipeline_params.py → ddeutil_workflow-0.0.15/tests/test_workflow_params.py +1 -1
  35. ddeutil_workflow-0.0.15/tests/test_workflow_run.py +127 -0
  36. ddeutil_workflow-0.0.13/tests/test_pipeline_task.py → ddeutil_workflow-0.0.15/tests/test_workflow_task.py +3 -3
  37. ddeutil_workflow-0.0.13/src/ddeutil/workflow/__about__.py +0 -1
  38. ddeutil_workflow-0.0.13/src/ddeutil/workflow/__types.py +0 -64
  39. ddeutil_workflow-0.0.13/tests/test_pipeline_run.py +0 -22
  40. ddeutil_workflow-0.0.13/tests/test_utils_result.py +0 -32
  41. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/LICENSE +0 -0
  42. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/setup.cfg +0 -0
  43. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/cli.py +0 -0
  44. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/exceptions.py +0 -0
  45. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/on.py +0 -0
  46. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/repeat.py +0 -0
  47. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil/workflow/route.py +0 -0
  48. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  49. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  50. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  51. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test__conf_exist.py +0 -0
  52. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_conf.py +0 -0
  53. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_cron.py +0 -0
  54. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_job.py +0 -0
  55. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_log.py +0 -0
  56. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_on.py +0 -0
  57. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_params.py +0 -0
  58. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage.py +0 -0
  59. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage_bash.py +0 -0
  60. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage_condition.py +0 -0
  61. {ddeutil_workflow-0.0.13 → ddeutil_workflow-0.0.15}/tests/test_stage_hook.py +0 -0
  62. /ddeutil_workflow-0.0.13/tests/test_pipeline_run_raise.py → /ddeutil_workflow-0.0.15/tests/test_workflow_run_raise.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.13
3
+ Version: 0.0.15
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -22,12 +22,13 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Requires-Python: >=3.9.13
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: ddeutil-io>=0.1.12
25
+ Requires-Dist: ddeutil>=0.4.0
26
+ Requires-Dist: ddeutil-io>=0.1.13
26
27
  Requires-Dist: python-dotenv==1.0.1
27
28
  Requires-Dist: typer<1.0.0,==0.12.5
28
29
  Requires-Dist: schedule<2.0.0,==1.2.2
29
30
  Provides-Extra: api
30
- Requires-Dist: fastapi<1.0.0,>=0.114.1; extra == "api"
31
+ Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
31
32
 
32
33
  # Workflow
33
34
 
@@ -62,7 +63,7 @@ configuration. It called **Metadata Driven Data Workflow**.
62
63
 
63
64
  > [!NOTE]
64
65
  > _Disclaimer_: I inspire the dynamic statement from the [**GitHub Action**](https://github.com/features/actions)
65
- > `.yml` files and all of config file from several data orchestration framework
66
+ > with `.yml` files and all of config file from several data orchestration framework
66
67
  > tools from my experience on Data Engineer. :grimacing:
67
68
  >
68
69
  > Other workflow that I interest on them and pick some interested feature to this
@@ -92,6 +93,7 @@ this package with application add-ons, you should add `app` in installation;
92
93
  > | ddeutil-workflow:python3.10 | `3.10` | :x: |
93
94
  > | ddeutil-workflow:python3.11 | `3.11` | :x: |
94
95
  > | ddeutil-workflow:python3.12 | `3.12` | :x: |
96
+ > | ddeutil-workflow:python3.12 | `3.13` | :x: |
95
97
 
96
98
  ## :beers: Usage
97
99
 
@@ -31,7 +31,7 @@ configuration. It called **Metadata Driven Data Workflow**.
31
31
 
32
32
  > [!NOTE]
33
33
  > _Disclaimer_: I inspire the dynamic statement from the [**GitHub Action**](https://github.com/features/actions)
34
- > `.yml` files and all of config file from several data orchestration framework
34
+ > with `.yml` files and all of config file from several data orchestration framework
35
35
  > tools from my experience on Data Engineer. :grimacing:
36
36
  >
37
37
  > Other workflow that I interest on them and pick some interested feature to this
@@ -61,6 +61,7 @@ this package with application add-ons, you should add `app` in installation;
61
61
  > | ddeutil-workflow:python3.10 | `3.10` | :x: |
62
62
  > | ddeutil-workflow:python3.11 | `3.11` | :x: |
63
63
  > | ddeutil-workflow:python3.12 | `3.12` | :x: |
64
+ > | ddeutil-workflow:python3.12 | `3.13` | :x: |
64
65
 
65
66
  ## :beers: Usage
66
67
 
@@ -26,7 +26,8 @@ classifiers = [
26
26
  ]
27
27
  requires-python = ">=3.9.13"
28
28
  dependencies = [
29
- "ddeutil-io>=0.1.12",
29
+ "ddeutil>=0.4.0",
30
+ "ddeutil-io>=0.1.13",
30
31
  "python-dotenv==1.0.1",
31
32
  "typer==0.12.5,<1.0.0",
32
33
  "schedule==1.2.2,<2.0.0",
@@ -35,7 +36,7 @@ dynamic = ["version"]
35
36
 
36
37
  [project.optional-dependencies]
37
38
  api = [
38
- "fastapi>=0.114.1,<1.0.0",
39
+ "fastapi>=0.115.0,<1.0.0",
39
40
  ]
40
41
 
41
42
  [project.urls]
@@ -62,7 +63,6 @@ concurrency = ["thread", "multiprocessing"]
62
63
  source = ["ddeutil.workflow", "tests"]
63
64
  omit = [
64
65
  "src/ddeutil/workflow/__about__.py",
65
- "scripts/",
66
66
  # Omit this files because it does not ready to production.
67
67
  "src/ddeutil/workflow/api.py",
68
68
  "src/ddeutil/workflow/cli.py",
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.15"
@@ -12,7 +12,10 @@ from .exceptions import (
12
12
  )
13
13
  from .job import Job, Strategy
14
14
  from .on import On, interval2crontab
15
- from .scheduler import Workflow
15
+ from .scheduler import (
16
+ Schedule,
17
+ Workflow,
18
+ )
16
19
  from .stage import Stage, handler_result
17
20
  from .utils import (
18
21
  Param,
@@ -0,0 +1,113 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2022 Korawich Anuttra. All rights reserved.
3
+ # Licensed under the MIT License. See LICENSE in the project root for
4
+ # license information.
5
+ # ------------------------------------------------------------------------------
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
11
+ from re import (
12
+ IGNORECASE,
13
+ MULTILINE,
14
+ UNICODE,
15
+ VERBOSE,
16
+ Match,
17
+ Pattern,
18
+ )
19
+ from typing import Any, Optional, Union
20
+
21
+ from typing_extensions import Self
22
+
23
+ TupleStr = tuple[str, ...]
24
+ DictData = dict[str, Any]
25
+ DictStr = dict[str, str]
26
+ Matrix = dict[str, Union[list[str], list[int]]]
27
+ MatrixInclude = list[dict[str, Union[str, int]]]
28
+ MatrixExclude = list[dict[str, Union[str, int]]]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class CallerRe:
33
+ """Caller dataclass that catching result from the matching regex with the
34
+ Re.RE_CALLER value.
35
+ """
36
+
37
+ full: str
38
+ caller: str
39
+ caller_prefix: Optional[str]
40
+ caller_last: str
41
+ post_filters: str
42
+
43
+ @classmethod
44
+ def from_regex(cls, match: Match[str]) -> Self:
45
+ """Class construct from matching result.
46
+
47
+ :rtype: Self
48
+ """
49
+ return cls(full=match.group(0), **match.groupdict())
50
+
51
+
52
+ class Re:
53
+ """Regular expression config for this package."""
54
+
55
+ # NOTE:
56
+ # Regular expression:
57
+ # - Version 1:
58
+ # \${{\s*(?P<caller>[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?)\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
59
+ # - Version 2 (2024-09-30):
60
+ # \${{\s*(?P<caller>(?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
61
+ #
62
+ # Examples:
63
+ # - ${{ params.data_dt }}
64
+ # - ${{ params.source.table }}
65
+ #
66
+ __re_caller: str = r"""
67
+ \$
68
+ {{
69
+ \s*
70
+ (?P<caller>
71
+ (?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)
72
+ (?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+)
73
+ )
74
+ \s*
75
+ (?P<post_filters>
76
+ (?:
77
+ \|\s*
78
+ (?:
79
+ [a-zA-Z0-9_]{3,}
80
+ [a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*
81
+ )\s*
82
+ )*
83
+ )
84
+ }}
85
+ """
86
+ RE_CALLER: Pattern = re.compile(
87
+ __re_caller, MULTILINE | IGNORECASE | UNICODE | VERBOSE
88
+ )
89
+
90
+ # NOTE:
91
+ # Regular expression:
92
+ # - Version 1:
93
+ # ^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$
94
+ #
95
+ # Examples:
96
+ # - tasks/function@dummy
97
+ __re_task_fmt: str = r"""
98
+ ^
99
+ (?P<path>[^/@]+)
100
+ /
101
+ (?P<func>[^@]+)
102
+ @
103
+ (?P<tag>.+)
104
+ $
105
+ """
106
+ RE_TASK_FMT: Pattern = re.compile(
107
+ __re_task_fmt, MULTILINE | IGNORECASE | UNICODE | VERBOSE
108
+ )
109
+
110
+ @classmethod
111
+ def finditer_caller(cls, value) -> Iterator[CallerRe]:
112
+ for found in cls.RE_CALLER.finditer(value):
113
+ yield CallerRe.from_regex(found)
@@ -25,7 +25,7 @@ from pydantic import BaseModel
25
25
  from .__about__ import __version__
26
26
  from .log import get_logger
27
27
  from .repeat import repeat_at, repeat_every
28
- from .scheduler import WorkflowTask
28
+ from .scheduler import WorkflowTaskData
29
29
 
30
30
  load_dotenv()
31
31
  logger = get_logger("ddeutil.workflow")
@@ -36,7 +36,7 @@ class State(TypedDict):
36
36
  upper_result: dict[str, str]
37
37
  scheduler: list[str]
38
38
  workflow_threads: dict[str, Thread]
39
- workflow_tasks: list[WorkflowTask]
39
+ workflow_tasks: list[WorkflowTaskData]
40
40
  workflow_queue: dict[str, list[datetime]]
41
41
  workflow_running: dict[str, list[datetime]]
42
42
 
@@ -0,0 +1,45 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2022 Korawich Anuttra. All rights reserved.
3
+ # Licensed under the MIT License. See LICENSE in the project root for
4
+ # license information.
5
+ # ------------------------------------------------------------------------------
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from zoneinfo import ZoneInfo
10
+
11
+ from ddeutil.core import str2bool
12
+ from dotenv import load_dotenv
13
+
14
+ load_dotenv()
15
+ env = os.getenv
16
+
17
+
18
+ class Config:
19
+ """Config object for keeping application configuration on current session
20
+ without changing when if the application still running.
21
+ """
22
+
23
+ # NOTE: Core
24
+ tz: ZoneInfo = ZoneInfo(env("WORKFLOW_CORE_TIMEZONE", "UTC"))
25
+
26
+ # NOTE: Stage
27
+ stage_raise_error: bool = str2bool(
28
+ env("WORKFLOW_CORE_STAGE_RAISE_ERROR", "true")
29
+ )
30
+ stage_default_id: bool = str2bool(
31
+ env("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false")
32
+ )
33
+
34
+ # NOTE: Workflow
35
+ max_job_parallel: int = int(env("WORKFLOW_CORE_MAX_JOB_PARALLEL", "2"))
36
+
37
+ def __init__(self):
38
+ if self.max_job_parallel < 0:
39
+ raise ValueError(
40
+ f"MAX_JOB_PARALLEL should more than 0 but got "
41
+ f"{self.max_job_parallel}."
42
+ )
43
+
44
+
45
+ config = Config()
@@ -14,7 +14,7 @@ from typing import ClassVar, Optional, Union
14
14
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
15
15
 
16
16
  from ddeutil.core import (
17
- is_int,
17
+ checker,
18
18
  isinstance_check,
19
19
  must_split,
20
20
  )
@@ -38,16 +38,21 @@ class CronYearLimit(Exception): ...
38
38
 
39
39
 
40
40
  def str2cron(value: str) -> str:
41
- """Convert Special String to Crontab.
42
-
43
- @reboot Run once, at system startup
44
- @yearly Run once every year, "0 0 1 1 *"
45
- @annually (same as @yearly)
46
- @monthly Run once every month, "0 0 1 * *"
47
- @weekly Run once every week, "0 0 * * 0"
48
- @daily Run once each day, "0 0 * * *"
49
- @midnight (same as @daily)
50
- @hourly Run once an hour, "0 * * * *"
41
+ """Convert Special String with the @ prefix to Crontab value.
42
+
43
+ :param value: A string value that want to convert to cron value.
44
+ :rtype: str
45
+
46
+ Table:
47
+
48
+ @reboot Run once, at system startup
49
+ @yearly Run once every year, "0 0 1 1 *"
50
+ @annually (same as @yearly)
51
+ @monthly Run once every month, "0 0 1 * *"
52
+ @weekly Run once every week, "0 0 * * 0"
53
+ @daily Run once each day, "0 0 * * *"
54
+ @midnight (same as @daily)
55
+ @hourly Run once an hour, "0 * * * *"
51
56
  """
52
57
  mapping_spacial_str = {
53
58
  "@reboot": "",
@@ -321,7 +326,9 @@ class CronPart:
321
326
  self._parse_range(value_range)
322
327
  )
323
328
 
324
- if (value_step and not is_int(value_step)) or value_step == "":
329
+ if (
330
+ value_step and not checker.is_int(value_step)
331
+ ) or value_step == "":
325
332
  raise ValueError(
326
333
  f"Invalid interval step value {value_step!r} for "
327
334
  f"{self.unit.name!r}"