ddeutil-workflow 0.0.22__py3-none-any.whl → 0.0.23__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 -8
- ddeutil/workflow/api.py +1 -1
- ddeutil/workflow/job.py +2 -2
- ddeutil/workflow/on.py +3 -3
- ddeutil/workflow/params.py +176 -0
- ddeutil/workflow/result.py +102 -0
- ddeutil/workflow/route.py +1 -1
- ddeutil/workflow/scheduler.py +38 -26
- ddeutil/workflow/stage.py +1 -1
- ddeutil/workflow/utils.py +4 -241
- ddeutil/workflow/workflow.py +48 -19
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.23.dist-info}/METADATA +1 -1
- ddeutil_workflow-0.0.23.dist-info/RECORD +24 -0
- ddeutil_workflow-0.0.22.dist-info/RECORD +0 -22
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.23.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.23.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.23.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.23.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.23"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -24,9 +24,17 @@ from .on import (
|
|
24
24
|
YearOn,
|
25
25
|
interval2crontab,
|
26
26
|
)
|
27
|
+
from .params import (
|
28
|
+
ChoiceParam,
|
29
|
+
DatetimeParam,
|
30
|
+
IntParam,
|
31
|
+
Param,
|
32
|
+
StrParam,
|
33
|
+
)
|
34
|
+
from .result import Result
|
27
35
|
from .scheduler import (
|
28
36
|
Schedule,
|
29
|
-
|
37
|
+
WorkflowSchedule,
|
30
38
|
)
|
31
39
|
from .stage import (
|
32
40
|
BashStage,
|
@@ -39,16 +47,9 @@ from .stage import (
|
|
39
47
|
)
|
40
48
|
from .utils import (
|
41
49
|
FILTERS,
|
42
|
-
ChoiceParam,
|
43
|
-
DatetimeParam,
|
44
|
-
DefaultParam,
|
45
50
|
FilterFunc,
|
46
51
|
FilterRegistry,
|
47
|
-
IntParam,
|
48
|
-
Param,
|
49
|
-
Result,
|
50
52
|
ReturnTagFunc,
|
51
|
-
StrParam,
|
52
53
|
TagFunc,
|
53
54
|
batch,
|
54
55
|
cross_product,
|
ddeutil/workflow/api.py
CHANGED
@@ -23,7 +23,7 @@ from pydantic import BaseModel
|
|
23
23
|
from .__about__ import __version__
|
24
24
|
from .conf import config, get_logger
|
25
25
|
from .repeat import repeat_at, repeat_every
|
26
|
-
from .
|
26
|
+
from .workflow import WorkflowTaskData
|
27
27
|
|
28
28
|
load_dotenv()
|
29
29
|
logger = get_logger("ddeutil.workflow")
|
ddeutil/workflow/job.py
CHANGED
@@ -36,9 +36,9 @@ from .exceptions import (
|
|
36
36
|
StageException,
|
37
37
|
UtilException,
|
38
38
|
)
|
39
|
+
from .result import Result
|
39
40
|
from .stage import Stage
|
40
41
|
from .utils import (
|
41
|
-
Result,
|
42
42
|
cross_product,
|
43
43
|
cut_id,
|
44
44
|
dash2underscore,
|
@@ -313,7 +313,7 @@ class Job(BaseModel):
|
|
313
313
|
# VALIDATE: Validate stage id should not duplicate.
|
314
314
|
rs: list[str] = []
|
315
315
|
for stage in value:
|
316
|
-
name: str = stage.
|
316
|
+
name: str = stage.iden
|
317
317
|
if name in rs:
|
318
318
|
raise ValueError(
|
319
319
|
"Stage name in jobs object should not be duplicate."
|
ddeutil/workflow/on.py
CHANGED
@@ -61,7 +61,7 @@ def interval2crontab(
|
|
61
61
|
|
62
62
|
|
63
63
|
class On(BaseModel):
|
64
|
-
"""On
|
64
|
+
"""On Pydantic model (Warped crontab object by model).
|
65
65
|
|
66
66
|
See Also:
|
67
67
|
* ``generate()`` is the main usecase of this schedule object.
|
@@ -197,8 +197,8 @@ class On(BaseModel):
|
|
197
197
|
|
198
198
|
|
199
199
|
class YearOn(On):
|
200
|
-
"""
|
201
|
-
data schedule tools like AWS Glue.
|
200
|
+
"""On with enhance Year Pydantic model for limit year matrix that use by
|
201
|
+
some data schedule tools like AWS Glue.
|
202
202
|
"""
|
203
203
|
|
204
204
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -0,0 +1,176 @@
|
|
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 logging
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
from datetime import date, datetime
|
11
|
+
from typing import Any, Literal, Optional, Union
|
12
|
+
|
13
|
+
from pydantic import BaseModel, Field
|
14
|
+
|
15
|
+
from .__types import TupleStr
|
16
|
+
from .exceptions import ParamValueException
|
17
|
+
from .utils import get_dt_now
|
18
|
+
|
19
|
+
logger = logging.getLogger("ddeutil.workflow")
|
20
|
+
|
21
|
+
__all__: TupleStr = (
|
22
|
+
"ChoiceParam",
|
23
|
+
"DatetimeParam",
|
24
|
+
"IntParam",
|
25
|
+
"Param",
|
26
|
+
"StrParam",
|
27
|
+
)
|
28
|
+
|
29
|
+
|
30
|
+
class BaseParam(BaseModel, ABC):
|
31
|
+
"""Base Parameter that use to make any Params Model. The type will dynamic
|
32
|
+
with the type field that made from literal string."""
|
33
|
+
|
34
|
+
desc: Optional[str] = Field(
|
35
|
+
default=None, description="A description of parameter providing."
|
36
|
+
)
|
37
|
+
required: bool = Field(
|
38
|
+
default=True,
|
39
|
+
description="A require flag that force to pass this parameter value.",
|
40
|
+
)
|
41
|
+
type: str = Field(description="A type of parameter.")
|
42
|
+
|
43
|
+
@abstractmethod
|
44
|
+
def receive(self, value: Optional[Any] = None) -> Any:
|
45
|
+
raise NotImplementedError(
|
46
|
+
"Receive value and validate typing before return valid value."
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
class DefaultParam(BaseParam):
|
51
|
+
"""Default Parameter that will check default if it required. This model do
|
52
|
+
not implement the receive method.
|
53
|
+
"""
|
54
|
+
|
55
|
+
required: bool = Field(
|
56
|
+
default=False,
|
57
|
+
description="A require flag for the default-able parameter value.",
|
58
|
+
)
|
59
|
+
default: Optional[str] = Field(
|
60
|
+
default=None,
|
61
|
+
description="A default value if parameter does not pass.",
|
62
|
+
)
|
63
|
+
|
64
|
+
@abstractmethod
|
65
|
+
def receive(self, value: Optional[Any] = None) -> Any:
|
66
|
+
raise NotImplementedError(
|
67
|
+
"Receive value and validate typing before return valid value."
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
class DatetimeParam(DefaultParam):
|
72
|
+
"""Datetime parameter."""
|
73
|
+
|
74
|
+
type: Literal["datetime"] = "datetime"
|
75
|
+
default: datetime = Field(default_factory=get_dt_now)
|
76
|
+
|
77
|
+
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
78
|
+
"""Receive value that match with datetime. If a input value pass with
|
79
|
+
None, it will use default value instead.
|
80
|
+
|
81
|
+
:param value: A value that want to validate with datetime parameter
|
82
|
+
type.
|
83
|
+
:rtype: datetime
|
84
|
+
"""
|
85
|
+
if value is None:
|
86
|
+
return self.default
|
87
|
+
|
88
|
+
if isinstance(value, datetime):
|
89
|
+
return value
|
90
|
+
elif isinstance(value, date):
|
91
|
+
return datetime(value.year, value.month, value.day)
|
92
|
+
elif not isinstance(value, str):
|
93
|
+
raise ParamValueException(
|
94
|
+
f"Value that want to convert to datetime does not support for "
|
95
|
+
f"type: {type(value)}"
|
96
|
+
)
|
97
|
+
try:
|
98
|
+
return datetime.fromisoformat(value)
|
99
|
+
except ValueError:
|
100
|
+
raise ParamValueException(
|
101
|
+
f"Invalid isoformat string: {value!r}"
|
102
|
+
) from None
|
103
|
+
|
104
|
+
|
105
|
+
class StrParam(DefaultParam):
|
106
|
+
"""String parameter."""
|
107
|
+
|
108
|
+
type: Literal["str"] = "str"
|
109
|
+
|
110
|
+
def receive(self, value: str | None = None) -> str | None:
|
111
|
+
"""Receive value that match with str.
|
112
|
+
|
113
|
+
:param value: A value that want to validate with string parameter type.
|
114
|
+
:rtype: str | None
|
115
|
+
"""
|
116
|
+
if value is None:
|
117
|
+
return self.default
|
118
|
+
return str(value)
|
119
|
+
|
120
|
+
|
121
|
+
class IntParam(DefaultParam):
|
122
|
+
"""Integer parameter."""
|
123
|
+
|
124
|
+
type: Literal["int"] = "int"
|
125
|
+
default: Optional[int] = Field(
|
126
|
+
default=None,
|
127
|
+
description="A default value if parameter does not pass.",
|
128
|
+
)
|
129
|
+
|
130
|
+
def receive(self, value: int | None = None) -> int | None:
|
131
|
+
"""Receive value that match with int.
|
132
|
+
|
133
|
+
:param value: A value that want to validate with integer parameter type.
|
134
|
+
:rtype: int | None
|
135
|
+
"""
|
136
|
+
if value is None:
|
137
|
+
return self.default
|
138
|
+
if not isinstance(value, int):
|
139
|
+
try:
|
140
|
+
return int(str(value))
|
141
|
+
except ValueError as err:
|
142
|
+
raise ParamValueException(
|
143
|
+
f"Value can not convert to int, {value}, with base 10"
|
144
|
+
) from err
|
145
|
+
return value
|
146
|
+
|
147
|
+
|
148
|
+
class ChoiceParam(BaseParam):
|
149
|
+
"""Choice parameter."""
|
150
|
+
|
151
|
+
type: Literal["choice"] = "choice"
|
152
|
+
options: list[str] = Field(description="A list of choice parameters.")
|
153
|
+
|
154
|
+
def receive(self, value: str | None = None) -> str:
|
155
|
+
"""Receive value that match with options.
|
156
|
+
|
157
|
+
:param value: A value that want to select from the options field.
|
158
|
+
:rtype: str
|
159
|
+
"""
|
160
|
+
# NOTE:
|
161
|
+
# Return the first value in options if does not pass any input value
|
162
|
+
if value is None:
|
163
|
+
return self.options[0]
|
164
|
+
if value not in self.options:
|
165
|
+
raise ParamValueException(
|
166
|
+
f"{value!r} does not match any value in choice options."
|
167
|
+
)
|
168
|
+
return value
|
169
|
+
|
170
|
+
|
171
|
+
Param = Union[
|
172
|
+
ChoiceParam,
|
173
|
+
DatetimeParam,
|
174
|
+
IntParam,
|
175
|
+
StrParam,
|
176
|
+
]
|
@@ -0,0 +1,102 @@
|
|
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
|
+
from dataclasses import field
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
from pydantic.dataclasses import dataclass
|
12
|
+
from pydantic.functional_validators import model_validator
|
13
|
+
from typing_extensions import Self
|
14
|
+
|
15
|
+
from .__types import DictData, TupleStr
|
16
|
+
from .utils import gen_id
|
17
|
+
|
18
|
+
__all__: TupleStr = ("Result",)
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class Result:
|
23
|
+
"""Result Pydantic Model for passing and receiving data context from any
|
24
|
+
module execution process like stage execution, job execution, or workflow
|
25
|
+
execution.
|
26
|
+
|
27
|
+
For comparison property, this result will use ``status``, ``context``,
|
28
|
+
and ``_run_id`` fields to comparing with other result instance.
|
29
|
+
"""
|
30
|
+
|
31
|
+
status: int = field(default=2)
|
32
|
+
context: DictData = field(default_factory=dict)
|
33
|
+
run_id: Optional[str] = field(default=None)
|
34
|
+
|
35
|
+
# NOTE: Ignore this field to compare another result model with __eq__.
|
36
|
+
parent_run_id: Optional[str] = field(default=None, compare=False)
|
37
|
+
|
38
|
+
@model_validator(mode="after")
|
39
|
+
def __prepare_run_id(self) -> Self:
|
40
|
+
"""Prepare running ID which use default ID if it initialize at the first
|
41
|
+
time
|
42
|
+
|
43
|
+
:rtype: Self
|
44
|
+
"""
|
45
|
+
self._run_id = gen_id("manual", unique=True)
|
46
|
+
return self
|
47
|
+
|
48
|
+
def set_run_id(self, running_id: str) -> Self:
|
49
|
+
"""Set a running ID.
|
50
|
+
|
51
|
+
:param running_id: A running ID that want to update on this model.
|
52
|
+
:rtype: Self
|
53
|
+
"""
|
54
|
+
self.run_id = running_id
|
55
|
+
return self
|
56
|
+
|
57
|
+
def set_parent_run_id(self, running_id: str) -> Self:
|
58
|
+
"""Set a parent running ID.
|
59
|
+
|
60
|
+
:param running_id: A running ID that want to update on this model.
|
61
|
+
:rtype: Self
|
62
|
+
"""
|
63
|
+
self.parent_run_id: str = running_id
|
64
|
+
return self
|
65
|
+
|
66
|
+
def catch(self, status: int, context: DictData) -> Self:
|
67
|
+
"""Catch the status and context to current data."""
|
68
|
+
self.__dict__["status"] = status
|
69
|
+
self.__dict__["context"].update(context)
|
70
|
+
return self
|
71
|
+
|
72
|
+
def receive(self, result: Result) -> Self:
|
73
|
+
"""Receive context from another result object.
|
74
|
+
|
75
|
+
:rtype: Self
|
76
|
+
"""
|
77
|
+
self.__dict__["status"] = result.status
|
78
|
+
self.__dict__["context"].update(result.context)
|
79
|
+
|
80
|
+
# NOTE: Update running ID from an incoming result.
|
81
|
+
self.parent_run_id = result.parent_run_id
|
82
|
+
self.run_id = result.run_id
|
83
|
+
return self
|
84
|
+
|
85
|
+
def receive_jobs(self, result: Result) -> Self:
|
86
|
+
"""Receive context from another result object that use on the workflow
|
87
|
+
execution which create a ``jobs`` keys on the context if it do not
|
88
|
+
exist.
|
89
|
+
|
90
|
+
:rtype: Self
|
91
|
+
"""
|
92
|
+
self.__dict__["status"] = result.status
|
93
|
+
|
94
|
+
# NOTE: Check the context has jobs key.
|
95
|
+
if "jobs" not in self.__dict__["context"]:
|
96
|
+
self.__dict__["context"]["jobs"] = {}
|
97
|
+
self.__dict__["context"]["jobs"].update(result.context)
|
98
|
+
|
99
|
+
# NOTE: Update running ID from an incoming result.
|
100
|
+
self.parent_run_id: str = result.parent_run_id
|
101
|
+
self.run_id: str = result.run_id
|
102
|
+
return self
|
ddeutil/workflow/route.py
CHANGED
@@ -17,8 +17,8 @@ from pydantic import BaseModel
|
|
17
17
|
from . import Workflow
|
18
18
|
from .__types import DictData
|
19
19
|
from .conf import Loader, config, get_logger
|
20
|
+
from .result import Result
|
20
21
|
from .scheduler import Schedule
|
21
|
-
from .utils import Result
|
22
22
|
|
23
23
|
logger = get_logger("ddeutil.workflow")
|
24
24
|
workflow = APIRouter(
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -70,7 +70,7 @@ logging.getLogger("schedule").setLevel(logging.INFO)
|
|
70
70
|
|
71
71
|
__all__: TupleStr = (
|
72
72
|
"Schedule",
|
73
|
-
"
|
73
|
+
"WorkflowSchedule",
|
74
74
|
"workflow_task_release",
|
75
75
|
"workflow_monitor",
|
76
76
|
"workflow_control",
|
@@ -78,25 +78,29 @@ __all__: TupleStr = (
|
|
78
78
|
)
|
79
79
|
|
80
80
|
|
81
|
-
class
|
82
|
-
"""Schedule
|
83
|
-
Schedule model. it should not use Workflow model directly because on the
|
81
|
+
class WorkflowSchedule(BaseModel):
|
82
|
+
"""Workflow Schedule Pydantic model that use to keep workflow model for
|
83
|
+
the Schedule model. it should not use Workflow model directly because on the
|
84
84
|
schedule config it can adjust crontab value that different from the Workflow
|
85
85
|
model.
|
86
86
|
"""
|
87
87
|
|
88
88
|
alias: Optional[str] = Field(
|
89
89
|
default=None,
|
90
|
-
description="An alias name of workflow.",
|
90
|
+
description="An alias name of workflow that use for schedule model.",
|
91
91
|
)
|
92
92
|
name: str = Field(description="A workflow name.")
|
93
93
|
on: list[On] = Field(
|
94
94
|
default_factory=list,
|
95
|
-
description="An override On
|
95
|
+
description="An override the list of On object values.",
|
96
96
|
)
|
97
|
-
|
97
|
+
values: DictData = Field(
|
98
98
|
default_factory=dict,
|
99
|
-
description=
|
99
|
+
description=(
|
100
|
+
"A value that want to pass to the workflow parameters when "
|
101
|
+
"calling release method."
|
102
|
+
),
|
103
|
+
alias="params",
|
100
104
|
)
|
101
105
|
|
102
106
|
@model_validator(mode="before")
|
@@ -105,10 +109,13 @@ class ScheduleWorkflow(BaseModel):
|
|
105
109
|
|
106
110
|
:rtype: DictData
|
107
111
|
"""
|
108
|
-
|
112
|
+
# VALIDATE: Prepare a workflow name that should not include space.
|
113
|
+
if name := values.get("name"):
|
114
|
+
values["name"] = name.replace(" ", "_")
|
109
115
|
|
116
|
+
# VALIDATE: Add default the alias field with the name.
|
110
117
|
if not values.get("alias"):
|
111
|
-
values["alias"] = values
|
118
|
+
values["alias"] = values.get("name")
|
112
119
|
|
113
120
|
cls.__bypass_on(values)
|
114
121
|
return values
|
@@ -135,6 +142,7 @@ class ScheduleWorkflow(BaseModel):
|
|
135
142
|
Loader(n, externals={}).data if isinstance(n, str) else n
|
136
143
|
for n in on
|
137
144
|
]
|
145
|
+
|
138
146
|
return data
|
139
147
|
|
140
148
|
@field_validator("on", mode="after")
|
@@ -150,19 +158,20 @@ class ScheduleWorkflow(BaseModel):
|
|
150
158
|
"The on fields should not contain duplicate on value."
|
151
159
|
)
|
152
160
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
161
|
+
if len(set_ons) > config.max_on_per_workflow:
|
162
|
+
raise ValueError(
|
163
|
+
f"The number of the on should not more than "
|
164
|
+
f"{config.max_on_per_workflow} crontab."
|
165
|
+
)
|
166
|
+
|
159
167
|
return value
|
160
168
|
|
161
169
|
|
162
170
|
class Schedule(BaseModel):
|
163
|
-
"""Schedule Pydantic
|
164
|
-
|
165
|
-
|
171
|
+
"""Schedule Pydantic model that use to run with any scheduler package.
|
172
|
+
|
173
|
+
It does not equal the on value in Workflow model but it use same logic
|
174
|
+
to running release date with crontab interval.
|
166
175
|
"""
|
167
176
|
|
168
177
|
desc: Optional[str] = Field(
|
@@ -171,9 +180,9 @@ class Schedule(BaseModel):
|
|
171
180
|
"A schedule description that can be string of markdown content."
|
172
181
|
),
|
173
182
|
)
|
174
|
-
workflows: list[
|
183
|
+
workflows: list[WorkflowSchedule] = Field(
|
175
184
|
default_factory=list,
|
176
|
-
description="A list of
|
185
|
+
description="A list of WorkflowSchedule models.",
|
177
186
|
)
|
178
187
|
|
179
188
|
@field_validator("desc", mode="after")
|
@@ -181,6 +190,7 @@ class Schedule(BaseModel):
|
|
181
190
|
"""Prepare description string that was created on a template.
|
182
191
|
|
183
192
|
:param value: A description string value that want to dedent.
|
193
|
+
|
184
194
|
:rtype: str
|
185
195
|
"""
|
186
196
|
return dedent(value)
|
@@ -224,6 +234,9 @@ class Schedule(BaseModel):
|
|
224
234
|
"""Return the list of WorkflowTaskData object from the specific input
|
225
235
|
datetime that mapping with the on field.
|
226
236
|
|
237
|
+
This task creation need queue to tracking release date already
|
238
|
+
mapped or not.
|
239
|
+
|
227
240
|
:param start_date: A start date that get from the workflow schedule.
|
228
241
|
:param queue: A mapping of name and list of datetime for queue.
|
229
242
|
:param externals: An external parameters that pass to the Loader object.
|
@@ -232,22 +245,21 @@ class Schedule(BaseModel):
|
|
232
245
|
:return: Return the list of WorkflowTaskData object from the specific
|
233
246
|
input datetime that mapping with the on field.
|
234
247
|
"""
|
235
|
-
|
236
|
-
# NOTE: Create pair of workflow and on.
|
237
248
|
workflow_tasks: list[WorkflowTaskData] = []
|
238
249
|
extras: DictData = externals or {}
|
239
250
|
|
240
251
|
for sch_wf in self.workflows:
|
241
252
|
|
253
|
+
# NOTE: Loading workflow model from the name of workflow.
|
242
254
|
wf: Workflow = Workflow.from_loader(sch_wf.name, externals=extras)
|
243
255
|
|
244
|
-
# NOTE: Create default list of release datetime.
|
256
|
+
# NOTE: Create default list of release datetime by empty list.
|
245
257
|
if sch_wf.alias not in queue:
|
246
258
|
queue[sch_wf.alias]: list[datetime] = []
|
247
259
|
|
248
260
|
# IMPORTANT: Create the default 'on' value if it does not passing
|
249
261
|
# the on field to the Schedule object.
|
250
|
-
ons: list[On] = wf.on.copy()
|
262
|
+
ons: list[On] = sch_wf.on or wf.on.copy()
|
251
263
|
|
252
264
|
for on in ons:
|
253
265
|
|
@@ -263,7 +275,7 @@ class Schedule(BaseModel):
|
|
263
275
|
alias=sch_wf.alias,
|
264
276
|
workflow=wf,
|
265
277
|
runner=runner,
|
266
|
-
params=sch_wf.
|
278
|
+
params=sch_wf.values,
|
267
279
|
),
|
268
280
|
)
|
269
281
|
|
ddeutil/workflow/stage.py
CHANGED
@@ -51,9 +51,9 @@ from typing_extensions import Self
|
|
51
51
|
from .__types import DictData, DictStr, Re, TupleStr
|
52
52
|
from .conf import config, get_logger
|
53
53
|
from .exceptions import StageException
|
54
|
+
from .result import Result
|
54
55
|
from .utils import (
|
55
56
|
Registry,
|
56
|
-
Result,
|
57
57
|
TagFunc,
|
58
58
|
cut_id,
|
59
59
|
gen_id,
|
ddeutil/workflow/utils.py
CHANGED
@@ -9,11 +9,9 @@ import inspect
|
|
9
9
|
import logging
|
10
10
|
import stat
|
11
11
|
import time
|
12
|
-
from abc import ABC, abstractmethod
|
13
12
|
from ast import Call, Constant, Expr, Module, Name, parse
|
14
13
|
from collections.abc import Iterator
|
15
|
-
from
|
16
|
-
from datetime import date, datetime, timedelta
|
14
|
+
from datetime import datetime, timedelta
|
17
15
|
from functools import wraps
|
18
16
|
from hashlib import md5
|
19
17
|
from importlib import import_module
|
@@ -21,7 +19,7 @@ from inspect import isfunction
|
|
21
19
|
from itertools import chain, islice, product
|
22
20
|
from pathlib import Path
|
23
21
|
from random import randrange
|
24
|
-
from typing import Any, Callable,
|
22
|
+
from typing import Any, Callable, Protocol, TypeVar, Union
|
25
23
|
from zoneinfo import ZoneInfo
|
26
24
|
|
27
25
|
try:
|
@@ -31,14 +29,11 @@ except ImportError:
|
|
31
29
|
|
32
30
|
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy
|
33
31
|
from ddeutil.io import search_env_replace
|
34
|
-
from pydantic import BaseModel
|
35
|
-
from pydantic.dataclasses import dataclass
|
36
|
-
from pydantic.functional_validators import model_validator
|
37
|
-
from typing_extensions import Self
|
32
|
+
from pydantic import BaseModel
|
38
33
|
|
39
34
|
from .__types import DictData, Matrix, Re
|
40
35
|
from .conf import config
|
41
|
-
from .exceptions import
|
36
|
+
from .exceptions import UtilException
|
42
37
|
|
43
38
|
T = TypeVar("T")
|
44
39
|
P = ParamSpec("P")
|
@@ -198,238 +193,6 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
198
193
|
return rs
|
199
194
|
|
200
195
|
|
201
|
-
class BaseParam(BaseModel, ABC):
|
202
|
-
"""Base Parameter that use to make Params Model."""
|
203
|
-
|
204
|
-
desc: Optional[str] = Field(
|
205
|
-
default=None, description="A description of parameter providing."
|
206
|
-
)
|
207
|
-
required: bool = Field(
|
208
|
-
default=True,
|
209
|
-
description="A require flag that force to pass this parameter value.",
|
210
|
-
)
|
211
|
-
type: str = Field(description="A type of parameter.")
|
212
|
-
|
213
|
-
@abstractmethod
|
214
|
-
def receive(self, value: Optional[Any] = None) -> Any:
|
215
|
-
raise NotImplementedError(
|
216
|
-
"Receive value and validate typing before return valid value."
|
217
|
-
)
|
218
|
-
|
219
|
-
|
220
|
-
class DefaultParam(BaseParam):
|
221
|
-
"""Default Parameter that will check default if it required. This model do
|
222
|
-
not implement the receive method.
|
223
|
-
"""
|
224
|
-
|
225
|
-
required: bool = Field(
|
226
|
-
default=False,
|
227
|
-
description="A require flag for the default-able parameter value.",
|
228
|
-
)
|
229
|
-
default: Optional[str] = Field(
|
230
|
-
default=None,
|
231
|
-
description="A default value if parameter does not pass.",
|
232
|
-
)
|
233
|
-
|
234
|
-
@abstractmethod
|
235
|
-
def receive(self, value: Optional[Any] = None) -> Any:
|
236
|
-
raise NotImplementedError(
|
237
|
-
"Receive value and validate typing before return valid value."
|
238
|
-
)
|
239
|
-
|
240
|
-
|
241
|
-
class DatetimeParam(DefaultParam):
|
242
|
-
"""Datetime parameter."""
|
243
|
-
|
244
|
-
type: Literal["datetime"] = "datetime"
|
245
|
-
default: datetime = Field(default_factory=get_dt_now)
|
246
|
-
|
247
|
-
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
248
|
-
"""Receive value that match with datetime. If a input value pass with
|
249
|
-
None, it will use default value instead.
|
250
|
-
|
251
|
-
:param value: A value that want to validate with datetime parameter
|
252
|
-
type.
|
253
|
-
:rtype: datetime
|
254
|
-
"""
|
255
|
-
if value is None:
|
256
|
-
return self.default
|
257
|
-
|
258
|
-
if isinstance(value, datetime):
|
259
|
-
return value
|
260
|
-
elif isinstance(value, date):
|
261
|
-
return datetime(value.year, value.month, value.day)
|
262
|
-
elif not isinstance(value, str):
|
263
|
-
raise ParamValueException(
|
264
|
-
f"Value that want to convert to datetime does not support for "
|
265
|
-
f"type: {type(value)}"
|
266
|
-
)
|
267
|
-
try:
|
268
|
-
return datetime.fromisoformat(value)
|
269
|
-
except ValueError:
|
270
|
-
raise ParamValueException(
|
271
|
-
f"Invalid isoformat string: {value!r}"
|
272
|
-
) from None
|
273
|
-
|
274
|
-
|
275
|
-
class StrParam(DefaultParam):
|
276
|
-
"""String parameter."""
|
277
|
-
|
278
|
-
type: Literal["str"] = "str"
|
279
|
-
|
280
|
-
def receive(self, value: str | None = None) -> str | None:
|
281
|
-
"""Receive value that match with str.
|
282
|
-
|
283
|
-
:param value: A value that want to validate with string parameter type.
|
284
|
-
:rtype: str | None
|
285
|
-
"""
|
286
|
-
if value is None:
|
287
|
-
return self.default
|
288
|
-
return str(value)
|
289
|
-
|
290
|
-
|
291
|
-
class IntParam(DefaultParam):
|
292
|
-
"""Integer parameter."""
|
293
|
-
|
294
|
-
type: Literal["int"] = "int"
|
295
|
-
default: Optional[int] = Field(
|
296
|
-
default=None,
|
297
|
-
description="A default value if parameter does not pass.",
|
298
|
-
)
|
299
|
-
|
300
|
-
def receive(self, value: int | None = None) -> int | None:
|
301
|
-
"""Receive value that match with int.
|
302
|
-
|
303
|
-
:param value: A value that want to validate with integer parameter type.
|
304
|
-
:rtype: int | None
|
305
|
-
"""
|
306
|
-
if value is None:
|
307
|
-
return self.default
|
308
|
-
if not isinstance(value, int):
|
309
|
-
try:
|
310
|
-
return int(str(value))
|
311
|
-
except ValueError as err:
|
312
|
-
raise ParamValueException(
|
313
|
-
f"Value can not convert to int, {value}, with base 10"
|
314
|
-
) from err
|
315
|
-
return value
|
316
|
-
|
317
|
-
|
318
|
-
class ChoiceParam(BaseParam):
|
319
|
-
"""Choice parameter."""
|
320
|
-
|
321
|
-
type: Literal["choice"] = "choice"
|
322
|
-
options: list[str] = Field(description="A list of choice parameters.")
|
323
|
-
|
324
|
-
def receive(self, value: str | None = None) -> str:
|
325
|
-
"""Receive value that match with options.
|
326
|
-
|
327
|
-
:param value: A value that want to select from the options field.
|
328
|
-
:rtype: str
|
329
|
-
"""
|
330
|
-
# NOTE:
|
331
|
-
# Return the first value in options if does not pass any input value
|
332
|
-
if value is None:
|
333
|
-
return self.options[0]
|
334
|
-
if value not in self.options:
|
335
|
-
raise ParamValueException(
|
336
|
-
f"{value!r} does not match any value in choice options."
|
337
|
-
)
|
338
|
-
return value
|
339
|
-
|
340
|
-
|
341
|
-
Param = Union[
|
342
|
-
ChoiceParam,
|
343
|
-
DatetimeParam,
|
344
|
-
IntParam,
|
345
|
-
StrParam,
|
346
|
-
]
|
347
|
-
|
348
|
-
|
349
|
-
@dataclass
|
350
|
-
class Result:
|
351
|
-
"""Result Pydantic Model for passing and receiving data context from any
|
352
|
-
module execution process like stage execution, job execution, or workflow
|
353
|
-
execution.
|
354
|
-
|
355
|
-
For comparison property, this result will use ``status``, ``context``,
|
356
|
-
and ``_run_id`` fields to comparing with other result instance.
|
357
|
-
"""
|
358
|
-
|
359
|
-
status: int = field(default=2)
|
360
|
-
context: DictData = field(default_factory=dict)
|
361
|
-
|
362
|
-
# NOTE: Ignore this field to compare another result model with __eq__.
|
363
|
-
run_id: Optional[str] = field(default=None)
|
364
|
-
parent_run_id: Optional[str] = field(default=None, compare=False)
|
365
|
-
|
366
|
-
@model_validator(mode="after")
|
367
|
-
def __prepare_run_id(self) -> Self:
|
368
|
-
"""Prepare running ID which use default ID if it initialize at the first
|
369
|
-
time
|
370
|
-
|
371
|
-
:rtype: Self
|
372
|
-
"""
|
373
|
-
self._run_id = gen_id("manual", unique=True)
|
374
|
-
return self
|
375
|
-
|
376
|
-
def set_run_id(self, running_id: str) -> Self:
|
377
|
-
"""Set a running ID.
|
378
|
-
|
379
|
-
:param running_id: A running ID that want to update on this model.
|
380
|
-
:rtype: Self
|
381
|
-
"""
|
382
|
-
self.run_id = running_id
|
383
|
-
return self
|
384
|
-
|
385
|
-
def set_parent_run_id(self, running_id: str) -> Self:
|
386
|
-
"""Set a parent running ID.
|
387
|
-
|
388
|
-
:param running_id: A running ID that want to update on this model.
|
389
|
-
:rtype: Self
|
390
|
-
"""
|
391
|
-
self.parent_run_id: str = running_id
|
392
|
-
return self
|
393
|
-
|
394
|
-
def catch(self, status: int, context: DictData) -> Self:
|
395
|
-
"""Catch the status and context to current data."""
|
396
|
-
self.__dict__["status"] = status
|
397
|
-
self.__dict__["context"].update(context)
|
398
|
-
return self
|
399
|
-
|
400
|
-
def receive(self, result: Result) -> Self:
|
401
|
-
"""Receive context from another result object.
|
402
|
-
|
403
|
-
:rtype: Self
|
404
|
-
"""
|
405
|
-
self.__dict__["status"] = result.status
|
406
|
-
self.__dict__["context"].update(result.context)
|
407
|
-
|
408
|
-
# NOTE: Update running ID from an incoming result.
|
409
|
-
self.parent_run_id = result.parent_run_id
|
410
|
-
self.run_id = result.run_id
|
411
|
-
return self
|
412
|
-
|
413
|
-
def receive_jobs(self, result: Result) -> Self:
|
414
|
-
"""Receive context from another result object that use on the workflow
|
415
|
-
execution which create a ``jobs`` keys on the context if it do not
|
416
|
-
exist.
|
417
|
-
|
418
|
-
:rtype: Self
|
419
|
-
"""
|
420
|
-
self.__dict__["status"] = result.status
|
421
|
-
|
422
|
-
# NOTE: Check the context has jobs key.
|
423
|
-
if "jobs" not in self.__dict__["context"]:
|
424
|
-
self.__dict__["context"]["jobs"] = {}
|
425
|
-
self.__dict__["context"]["jobs"].update(result.context)
|
426
|
-
|
427
|
-
# NOTE: Update running ID from an incoming result.
|
428
|
-
self.parent_run_id: str = result.parent_run_id
|
429
|
-
self.run_id: str = result.run_id
|
430
|
-
return self
|
431
|
-
|
432
|
-
|
433
196
|
def make_exec(path: str | Path) -> None:
|
434
197
|
"""Change mode of file to be executable file.
|
435
198
|
|
ddeutil/workflow/workflow.py
CHANGED
@@ -33,7 +33,7 @@ from functools import total_ordering
|
|
33
33
|
from heapq import heappop, heappush
|
34
34
|
from queue import Queue
|
35
35
|
from textwrap import dedent
|
36
|
-
from typing import Optional
|
36
|
+
from typing import Any, Optional
|
37
37
|
|
38
38
|
from pydantic import BaseModel, ConfigDict, Field
|
39
39
|
from pydantic.dataclasses import dataclass
|
@@ -46,9 +46,9 @@ from .conf import FileLog, Loader, Log, config, get_logger
|
|
46
46
|
from .exceptions import JobException, WorkflowException
|
47
47
|
from .job import Job
|
48
48
|
from .on import On
|
49
|
+
from .params import Param
|
50
|
+
from .result import Result
|
49
51
|
from .utils import (
|
50
|
-
Param,
|
51
|
-
Result,
|
52
52
|
cut_id,
|
53
53
|
delay,
|
54
54
|
gen_id,
|
@@ -86,13 +86,16 @@ class WorkflowRelease:
|
|
86
86
|
return f"{self.date:%Y-%m-%d %H:%M:%S}"
|
87
87
|
|
88
88
|
@classmethod
|
89
|
-
def from_dt(cls, dt: datetime) -> Self:
|
89
|
+
def from_dt(cls, dt: datetime | str) -> Self:
|
90
90
|
"""Construct WorkflowRelease via datetime object only.
|
91
91
|
|
92
92
|
:param dt: A datetime object.
|
93
93
|
|
94
94
|
:rtype: Self
|
95
95
|
"""
|
96
|
+
if isinstance(dt, str):
|
97
|
+
dt: datetime = datetime.fromisoformat(dt)
|
98
|
+
|
96
99
|
return cls(
|
97
100
|
date=dt,
|
98
101
|
offset=0,
|
@@ -132,7 +135,7 @@ class WorkflowQueue:
|
|
132
135
|
|
133
136
|
@classmethod
|
134
137
|
def from_list(
|
135
|
-
cls, queue: list[datetime] | list[WorkflowRelease] | None
|
138
|
+
cls, queue: list[datetime] | list[WorkflowRelease] | None = None
|
136
139
|
) -> Self:
|
137
140
|
"""Construct WorkflowQueue object from an input queue value that passing
|
138
141
|
with list of datetime or list of WorkflowRelease.
|
@@ -143,12 +146,13 @@ class WorkflowQueue:
|
|
143
146
|
"""
|
144
147
|
if queue is None:
|
145
148
|
return cls()
|
146
|
-
|
149
|
+
|
150
|
+
if isinstance(queue, list):
|
147
151
|
|
148
152
|
if all(isinstance(q, datetime) for q in queue):
|
149
153
|
return cls(queue=[WorkflowRelease.from_dt(q) for q in queue])
|
150
154
|
|
151
|
-
|
155
|
+
if all(isinstance(q, WorkflowRelease) for q in queue):
|
152
156
|
return cls(queue=queue)
|
153
157
|
|
154
158
|
raise TypeError(
|
@@ -518,12 +522,15 @@ class Workflow(BaseModel):
|
|
518
522
|
queue.remove_running(release)
|
519
523
|
heappush(queue.complete, release)
|
520
524
|
|
525
|
+
context: dict[str, Any] = rs.context
|
526
|
+
context.pop("params")
|
527
|
+
|
521
528
|
return rs_release.catch(
|
522
529
|
status=0,
|
523
530
|
context={
|
524
531
|
"params": params,
|
525
532
|
"release": {"status": "success", "logical_date": release.date},
|
526
|
-
"outputs":
|
533
|
+
"outputs": context,
|
527
534
|
},
|
528
535
|
)
|
529
536
|
|
@@ -654,9 +661,10 @@ class Workflow(BaseModel):
|
|
654
661
|
)
|
655
662
|
|
656
663
|
params: DictData = {} if params is None else params
|
657
|
-
wf_queue: WorkflowQueue = WorkflowQueue()
|
658
664
|
results: list[Result] = []
|
659
|
-
|
665
|
+
|
666
|
+
# NOTE: Create empty WorkflowQueue object.
|
667
|
+
wf_queue: WorkflowQueue = WorkflowQueue()
|
660
668
|
|
661
669
|
# NOTE: Make queue to the workflow queue object.
|
662
670
|
self.queue_poking(
|
@@ -680,6 +688,8 @@ class Workflow(BaseModel):
|
|
680
688
|
thread_name_prefix="wf_poking_",
|
681
689
|
) as executor:
|
682
690
|
|
691
|
+
futures: list[Future] = []
|
692
|
+
|
683
693
|
while wf_queue.is_queued:
|
684
694
|
|
685
695
|
# NOTE: Pop the latest WorkflowRelease object from queue.
|
@@ -1017,7 +1027,7 @@ class Workflow(BaseModel):
|
|
1017
1027
|
not_timeout_flag: bool = True
|
1018
1028
|
timeout: int = timeout or config.max_job_exec_timeout
|
1019
1029
|
logger.debug(
|
1020
|
-
f"({cut_id(run_id)}) [WORKFLOW]: Run {self.name} with "
|
1030
|
+
f"({cut_id(run_id)}) [WORKFLOW]: Run {self.name!r} with "
|
1021
1031
|
f"non-threading."
|
1022
1032
|
)
|
1023
1033
|
|
@@ -1075,7 +1085,7 @@ class WorkflowTaskData:
|
|
1075
1085
|
alias: str
|
1076
1086
|
workflow: Workflow
|
1077
1087
|
runner: CronRunner
|
1078
|
-
params: DictData
|
1088
|
+
params: DictData = field(default_factory=dict)
|
1079
1089
|
|
1080
1090
|
def release(
|
1081
1091
|
self,
|
@@ -1085,20 +1095,24 @@ class WorkflowTaskData:
|
|
1085
1095
|
*,
|
1086
1096
|
waiting_sec: int = 60,
|
1087
1097
|
sleep_interval: int = 15,
|
1088
|
-
) ->
|
1089
|
-
"""
|
1090
|
-
method
|
1098
|
+
) -> Result: # pragma: no cov
|
1099
|
+
"""Release the workflow task data that use the same logic of
|
1100
|
+
`workflow.release` method but use different the queue object for
|
1101
|
+
tracking release datetime to run.
|
1091
1102
|
|
1092
|
-
:param queue:
|
1103
|
+
:param queue: A mapping of alias name and list of release datetime.
|
1093
1104
|
:param log: A log object for saving result logging from workflow
|
1094
1105
|
execution process.
|
1095
1106
|
:param run_id: A workflow running ID for this release.
|
1096
1107
|
:param waiting_sec: A second period value that allow workflow execute.
|
1097
1108
|
:param sleep_interval: A second value that want to waiting until time
|
1098
1109
|
to execute.
|
1110
|
+
|
1111
|
+
:rtype: Result
|
1099
1112
|
"""
|
1100
|
-
log: Log = log or FileLog
|
1101
|
-
run_id: str = run_id or gen_id(self.
|
1113
|
+
log: type[Log] = log or FileLog
|
1114
|
+
run_id: str = run_id or gen_id(self.alias, unique=True)
|
1115
|
+
rs_release: Result = Result(run_id=run_id)
|
1102
1116
|
runner: CronRunner = self.runner
|
1103
1117
|
|
1104
1118
|
# NOTE: get next schedule time that generate from now.
|
@@ -1129,7 +1143,7 @@ class WorkflowTaskData:
|
|
1129
1143
|
queue[self.alias].remove(next_time)
|
1130
1144
|
|
1131
1145
|
time.sleep(0.2)
|
1132
|
-
return
|
1146
|
+
return rs_release.catch(status=0, context={})
|
1133
1147
|
|
1134
1148
|
logger.debug(
|
1135
1149
|
f"({cut_id(run_id)}) [CORE]: {self.workflow.name!r} : "
|
@@ -1153,6 +1167,9 @@ class WorkflowTaskData:
|
|
1153
1167
|
release_params: DictData = {
|
1154
1168
|
"release": {
|
1155
1169
|
"logical_date": next_time,
|
1170
|
+
"execute_date": datetime.now(tz=config.tz),
|
1171
|
+
"run_id": run_id,
|
1172
|
+
"timezone": runner.tz,
|
1156
1173
|
},
|
1157
1174
|
}
|
1158
1175
|
|
@@ -1198,6 +1215,18 @@ class WorkflowTaskData:
|
|
1198
1215
|
# NOTE: Queue next release date.
|
1199
1216
|
logger.debug(f"[CORE]: {'-' * 100}")
|
1200
1217
|
|
1218
|
+
context: dict[str, Any] = rs.context
|
1219
|
+
context.pop("params")
|
1220
|
+
|
1221
|
+
return rs_release.catch(
|
1222
|
+
status=0,
|
1223
|
+
context={
|
1224
|
+
"params": self.params,
|
1225
|
+
"release": {"status": "success", "logical_date": next_time},
|
1226
|
+
"outputs": context,
|
1227
|
+
},
|
1228
|
+
)
|
1229
|
+
|
1201
1230
|
def __eq__(self, other: WorkflowTaskData) -> bool:
|
1202
1231
|
"""Override equal property that will compare only the same type."""
|
1203
1232
|
if isinstance(other, WorkflowTaskData):
|
@@ -0,0 +1,24 @@
|
|
1
|
+
ddeutil/workflow/__about__.py,sha256=xc6s739CYU9opnZq2_D6Mx3ekymsbYOQKM2zsgw85oc,28
|
2
|
+
ddeutil/workflow/__cron.py,sha256=_2P9nmGOwGdv5bLgf9TpML2HBgqLv_qRgiO1Rulo1PA,26693
|
3
|
+
ddeutil/workflow/__init__.py,sha256=dH3T06kO8aEiRHAiL-d8a3IvOS0Fx80lS3AWz6rGdQk,1443
|
4
|
+
ddeutil/workflow/__types.py,sha256=Ia7f38kvL3NibwmRKi0wQ1ud_45Z-SojYGhNJwIqcu8,3713
|
5
|
+
ddeutil/workflow/api.py,sha256=ceTJfjXIw-3cgw4yx2QCcGLuA3STb0o7ELcVR_tfZFI,4700
|
6
|
+
ddeutil/workflow/cli.py,sha256=baHhvtI8snbHYHeThoX401Cd6SMB2boyyCbCtTrIl3E,3278
|
7
|
+
ddeutil/workflow/conf.py,sha256=GsbuJDQfQoAGiR4keUEoB4lKfZxdkaiZ4N4FfIHc0xY,15814
|
8
|
+
ddeutil/workflow/exceptions.py,sha256=NqnQJP52S59XIYMeXbTDbr4xH2UZ5EA3ejpU5Z4g6cQ,894
|
9
|
+
ddeutil/workflow/job.py,sha256=cvSLMdc1sMl1MeU7so7Oe2SdRYxQwt6hm55mLV1iP-Y,24219
|
10
|
+
ddeutil/workflow/on.py,sha256=0SxC3SH-8V1idgAEFOY-gYFEQPjK_zymmc5XqPoX_0I,7504
|
11
|
+
ddeutil/workflow/params.py,sha256=uPGkZx18E-iZ8BteqQ2ONgg0frhF3ZmP5cOyfK2j59U,5280
|
12
|
+
ddeutil/workflow/repeat.py,sha256=s0azh-f5JQeow7kpxM8GKlqgAmKL7oU6St3L4Ggx4cY,4925
|
13
|
+
ddeutil/workflow/result.py,sha256=WIC8MsnfLiWNpZomT6jS4YCdYhlbIVVBjtGGe2dkoKk,3404
|
14
|
+
ddeutil/workflow/route.py,sha256=bH5IT90JVjCDe9A0gIefpQQBEfcd-o1uCHE9AvNglvU,6754
|
15
|
+
ddeutil/workflow/scheduler.py,sha256=MYHf1bz8nsT8tJYcXgC-UycWbJ56Hx_zXwUAwWICimM,19141
|
16
|
+
ddeutil/workflow/stage.py,sha256=y6gjNzQy7xAM0n-lwqAEoC4x0lopH0K-Y77a_gvq4t8,26505
|
17
|
+
ddeutil/workflow/utils.py,sha256=ZVQh5vArWHNfCFYWYjHvkVD5aH-350ycfcZxDewELHM,18578
|
18
|
+
ddeutil/workflow/workflow.py,sha256=jmJxAEkczuanmj041OLgeBUDz9Y8XfFNVP_B3Xi7QDY,42557
|
19
|
+
ddeutil_workflow-0.0.23.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
20
|
+
ddeutil_workflow-0.0.23.dist-info/METADATA,sha256=4KqKSGCbc8dWHwWSCSgU4m_wsOouFHsiCGtdyo0Mf5U,14017
|
21
|
+
ddeutil_workflow-0.0.23.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
22
|
+
ddeutil_workflow-0.0.23.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
|
23
|
+
ddeutil_workflow-0.0.23.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
24
|
+
ddeutil_workflow-0.0.23.dist-info/RECORD,,
|
@@ -1,22 +0,0 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=hJavfsPLTnuXMwKFo9HZgsq6b7tJpWgyfttwaxzMujE,28
|
2
|
-
ddeutil/workflow/__cron.py,sha256=_2P9nmGOwGdv5bLgf9TpML2HBgqLv_qRgiO1Rulo1PA,26693
|
3
|
-
ddeutil/workflow/__init__.py,sha256=DCSN0foPFlFLN_Q4uoWa_EBBlKeMHXGpOdr-lWHISrQ,1422
|
4
|
-
ddeutil/workflow/__types.py,sha256=Ia7f38kvL3NibwmRKi0wQ1ud_45Z-SojYGhNJwIqcu8,3713
|
5
|
-
ddeutil/workflow/api.py,sha256=vUT2RVS9sF3hvY-IrzAEnahxwq4ZFYP0G3xfctHbNsw,4701
|
6
|
-
ddeutil/workflow/cli.py,sha256=baHhvtI8snbHYHeThoX401Cd6SMB2boyyCbCtTrIl3E,3278
|
7
|
-
ddeutil/workflow/conf.py,sha256=GsbuJDQfQoAGiR4keUEoB4lKfZxdkaiZ4N4FfIHc0xY,15814
|
8
|
-
ddeutil/workflow/exceptions.py,sha256=NqnQJP52S59XIYMeXbTDbr4xH2UZ5EA3ejpU5Z4g6cQ,894
|
9
|
-
ddeutil/workflow/job.py,sha256=liu8M_pUhAGHZ_Ez922jI94LCC3yioI-Tw5o71Zy88w,24216
|
10
|
-
ddeutil/workflow/on.py,sha256=wxKfL2u-bBhPbDtZbhqE2lZoPVukHA1zq-qrg0ldic0,7469
|
11
|
-
ddeutil/workflow/repeat.py,sha256=s0azh-f5JQeow7kpxM8GKlqgAmKL7oU6St3L4Ggx4cY,4925
|
12
|
-
ddeutil/workflow/route.py,sha256=JALwOH6xKu5rnII7DgA1Lbp_E5ehCoBbOW_eKqB_Olk,6753
|
13
|
-
ddeutil/workflow/scheduler.py,sha256=B2uXsqzmp32nIbya8EDePYyRhpwcxCMeoibPABCuMOA,18750
|
14
|
-
ddeutil/workflow/stage.py,sha256=ADFqExFmD8Y00A86TSS05HpabvsLV7_dbLrzD31TkK8,26490
|
15
|
-
ddeutil/workflow/utils.py,sha256=0GaHpRL1HuyES1NS7r56DFgloOVftYVAvAdVgIbPA_k,26001
|
16
|
-
ddeutil/workflow/workflow.py,sha256=fzhKJx9s-RF95FQ0tAvAQ1nsL8dsp_py2Ea5TGnjsOk,41542
|
17
|
-
ddeutil_workflow-0.0.22.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
18
|
-
ddeutil_workflow-0.0.22.dist-info/METADATA,sha256=J_VrfU8ZBPAa7OrhMV_c4sLbQ0g3Nc0MQSdYUgmxF6I,14017
|
19
|
-
ddeutil_workflow-0.0.22.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
20
|
-
ddeutil_workflow-0.0.22.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
|
21
|
-
ddeutil_workflow-0.0.22.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
22
|
-
ddeutil_workflow-0.0.22.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|