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.
@@ -1 +1 @@
1
- __version__: str = "0.0.22"
1
+ __version__: str = "0.0.23"
@@ -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
- ScheduleWorkflow,
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 .scheduler import WorkflowTaskData
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.id or stage.name
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 Model (Schedule)
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
- """Implement On Year Schedule Model for limit year matrix that use by some
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(
@@ -70,7 +70,7 @@ logging.getLogger("schedule").setLevel(logging.INFO)
70
70
 
71
71
  __all__: TupleStr = (
72
72
  "Schedule",
73
- "ScheduleWorkflow",
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 ScheduleWorkflow(BaseModel):
82
- """Schedule Workflow Pydantic model that use to keep workflow model for the
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 instance value.",
95
+ description="An override the list of On object values.",
96
96
  )
97
- params: DictData = Field(
97
+ values: DictData = Field(
98
98
  default_factory=dict,
99
- description="A parameters that want to use in workflow execution.",
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
- values["name"] = values["name"].replace(" ", "_")
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["name"]
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
- # WARNING:
154
- # if '* * * * *' in set_ons and len(set_ons) > 1:
155
- # raise ValueError(
156
- # "If it has every minute cronjob on value, it should has only "
157
- # "one value in the on field."
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 Model that use to run with scheduler package. It does
164
- not equal the on value in Workflow model but it use same logic to running
165
- release date with crontab interval.
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[ScheduleWorkflow] = Field(
183
+ workflows: list[WorkflowSchedule] = Field(
175
184
  default_factory=list,
176
- description="A list of ScheduleWorkflow models.",
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() if len(sch_wf.on) == 0 else sch_wf.on
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.params,
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 dataclasses import field
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, Literal, Optional, Protocol, TypeVar, Union
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, Field
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 ParamValueException, UtilException
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
 
@@ -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
- elif isinstance(queue, list):
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
- elif all(isinstance(q, WorkflowRelease) for q in queue):
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": rs.context,
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
- futures: list[Future] = []
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
- ) -> None: # pragma: no cov
1089
- """Workflow task release that use the same logic of `workflow.release`
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.workflow.name, unique=True)
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.22
3
+ Version: 0.0.23
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -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,,