ddeutil-workflow 0.0.2__py3-none-any.whl → 0.0.3__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/__types.py +1 -0
- ddeutil/workflow/conn.py +13 -10
- ddeutil/workflow/exceptions.py +0 -20
- ddeutil/workflow/loader.py +39 -11
- ddeutil/workflow/pipeline.py +183 -147
- ddeutil/workflow/schedule.py +7 -7
- ddeutil/workflow/tasks/_pandas.py +1 -1
- ddeutil/workflow/tasks/_polars.py +10 -2
- ddeutil/workflow/utils.py +116 -1
- ddeutil/workflow/vendors/__dataset.py +127 -0
- ddeutil/workflow/vendors/az.py +0 -0
- ddeutil/workflow/vendors/pd.py +13 -0
- ddeutil/workflow/vendors/pg.py +11 -0
- ddeutil/workflow/{dataset.py → vendors/pl.py} +3 -133
- {ddeutil_workflow-0.0.2.dist-info → ddeutil_workflow-0.0.3.dist-info}/METADATA +19 -15
- ddeutil_workflow-0.0.3.dist-info/RECORD +29 -0
- ddeutil_workflow-0.0.2.dist-info/RECORD +0 -25
- /ddeutil/workflow/vendors/{aws_warpped.py → aws.py} +0 -0
- /ddeutil/workflow/vendors/{minio_warpped.py → minio.py} +0 -0
- /ddeutil/workflow/vendors/{sftp_wrapped.py → sftp.py} +0 -0
- {ddeutil_workflow-0.0.2.dist-info → ddeutil_workflow-0.0.3.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.2.dist-info → ddeutil_workflow-0.0.3.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.2.dist-info → ddeutil_workflow-0.0.3.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.3"
|
ddeutil/workflow/__types.py
CHANGED
ddeutil/workflow/conn.py
CHANGED
@@ -43,8 +43,14 @@ class BaseConn(BaseModel):
|
|
43
43
|
]
|
44
44
|
|
45
45
|
@classmethod
|
46
|
-
def from_dict(cls, values: DictData):
|
47
|
-
"""Construct Connection
|
46
|
+
def from_dict(cls, values: DictData) -> Self:
|
47
|
+
"""Construct Connection Model from dict data. This construct is
|
48
|
+
different with ``.model_validate()`` because it will prepare the values
|
49
|
+
before using it if the data dose not have 'url'.
|
50
|
+
|
51
|
+
:param values: A dict data that use to construct this model.
|
52
|
+
"""
|
53
|
+
# NOTE: filter out the fields of this model.
|
48
54
|
filter_data: DictData = {
|
49
55
|
k: values.pop(k)
|
50
56
|
for k in values.copy()
|
@@ -73,15 +79,11 @@ class BaseConn(BaseModel):
|
|
73
79
|
)
|
74
80
|
|
75
81
|
@classmethod
|
76
|
-
def from_loader(
|
77
|
-
cls,
|
78
|
-
name: str,
|
79
|
-
externals: DictData,
|
80
|
-
) -> Self:
|
82
|
+
def from_loader(cls, name: str, externals: DictData) -> Self:
|
81
83
|
"""Construct Connection with Loader object with specific config name.
|
82
84
|
|
83
|
-
:param name:
|
84
|
-
:param externals:
|
85
|
+
:param name: A config name.
|
86
|
+
:param externals: A external data that want to adding to extras.
|
85
87
|
"""
|
86
88
|
loader: Loader = Loader(name, externals=externals)
|
87
89
|
# NOTE: Validate the config type match with current connection model
|
@@ -96,6 +98,7 @@ class BaseConn(BaseModel):
|
|
96
98
|
|
97
99
|
@field_validator("endpoint")
|
98
100
|
def __prepare_slash(cls, value: str) -> str:
|
101
|
+
"""Prepare slash character that map double form URL model loading."""
|
99
102
|
if value.startswith("//"):
|
100
103
|
return value[1:]
|
101
104
|
return value
|
@@ -148,7 +151,7 @@ class SFTP(Conn):
|
|
148
151
|
dialect: Literal["sftp"] = "sftp"
|
149
152
|
|
150
153
|
def __client(self):
|
151
|
-
from .vendors.
|
154
|
+
from .vendors.sftp import WrapSFTP
|
152
155
|
|
153
156
|
return WrapSFTP(
|
154
157
|
host=self.host,
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -9,24 +9,4 @@ Define Errors Object for Node package
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
11
|
|
12
|
-
class BaseError(Exception):
|
13
|
-
"""Base Error Object that use for catch any errors statement of
|
14
|
-
all step in this src
|
15
|
-
"""
|
16
|
-
|
17
|
-
|
18
|
-
class WorkflowBaseError(BaseError):
|
19
|
-
"""Core Base Error object"""
|
20
|
-
|
21
|
-
|
22
|
-
class ConfigNotFound(WorkflowBaseError):
|
23
|
-
"""Error raise for a method not found the config file or data."""
|
24
|
-
|
25
|
-
|
26
|
-
class PyException(Exception): ...
|
27
|
-
|
28
|
-
|
29
|
-
class ShellException(Exception): ...
|
30
|
-
|
31
|
-
|
32
12
|
class TaskException(Exception): ...
|
ddeutil/workflow/loader.py
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
from functools import cached_property
|
9
|
-
from typing import Any, TypeVar
|
9
|
+
from typing import Any, ClassVar, TypeVar
|
10
10
|
|
11
11
|
from ddeutil.core import (
|
12
12
|
getdot,
|
@@ -14,12 +14,12 @@ from ddeutil.core import (
|
|
14
14
|
import_string,
|
15
15
|
)
|
16
16
|
from ddeutil.io import (
|
17
|
-
|
18
|
-
Params,
|
17
|
+
PathData,
|
19
18
|
PathSearch,
|
20
19
|
YamlEnvFl,
|
21
20
|
)
|
22
|
-
from pydantic import BaseModel
|
21
|
+
from pydantic import BaseModel, Field
|
22
|
+
from pydantic.functional_validators import model_validator
|
23
23
|
|
24
24
|
from .__regex import RegexConf
|
25
25
|
from .__types import DictData
|
@@ -29,6 +29,25 @@ BaseModelType = type[BaseModel]
|
|
29
29
|
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
30
30
|
|
31
31
|
|
32
|
+
class Engine(BaseModel):
|
33
|
+
"""Engine Model"""
|
34
|
+
|
35
|
+
paths: PathData = Field(default_factory=PathData)
|
36
|
+
registry: list[str] = Field(default_factory=lambda: ["ddeutil.workflow"])
|
37
|
+
|
38
|
+
@model_validator(mode="before")
|
39
|
+
def __prepare_registry(cls, values: DictData) -> DictData:
|
40
|
+
if (_regis := values.get("registry")) and isinstance(_regis, str):
|
41
|
+
values["registry"] = [_regis]
|
42
|
+
return values
|
43
|
+
|
44
|
+
|
45
|
+
class Params(BaseModel):
|
46
|
+
"""Params Model"""
|
47
|
+
|
48
|
+
engine: Engine = Field(default_factory=Engine)
|
49
|
+
|
50
|
+
|
32
51
|
class SimLoad:
|
33
52
|
"""Simple Load Object that will search config data by name.
|
34
53
|
|
@@ -36,13 +55,11 @@ class SimLoad:
|
|
36
55
|
:param params: A Params model object.
|
37
56
|
:param externals: An external parameters
|
38
57
|
|
39
|
-
|
58
|
+
Noted:
|
40
59
|
The config data should have ``type`` key for engine can know what is
|
41
60
|
config should to do next.
|
42
61
|
"""
|
43
62
|
|
44
|
-
import_prefix: str = "ddeutil.workflow"
|
45
|
-
|
46
63
|
def __init__(
|
47
64
|
self,
|
48
65
|
name: str,
|
@@ -56,7 +73,7 @@ class SimLoad:
|
|
56
73
|
):
|
57
74
|
self.data = data
|
58
75
|
if not self.data:
|
59
|
-
raise
|
76
|
+
raise ValueError(f"Config {name!r} does not found on conf path")
|
60
77
|
self.__conf_params: Params = params
|
61
78
|
self.externals: DictData = externals
|
62
79
|
|
@@ -75,6 +92,11 @@ class SimLoad:
|
|
75
92
|
# NOTE: Auto adding module prefix if it does not set
|
76
93
|
return import_string(f"ddeutil.workflow.{_typ}")
|
77
94
|
except ModuleNotFoundError:
|
95
|
+
for registry in self.conf_params.engine.registry:
|
96
|
+
try:
|
97
|
+
return import_string(f"{registry}.{_typ}")
|
98
|
+
except ModuleNotFoundError:
|
99
|
+
continue
|
78
100
|
return import_string(f"{_typ}")
|
79
101
|
|
80
102
|
def load(self) -> AnyModel:
|
@@ -82,12 +104,14 @@ class SimLoad:
|
|
82
104
|
|
83
105
|
|
84
106
|
class Loader(SimLoad):
|
85
|
-
"""Main Loader Object.
|
107
|
+
"""Main Loader Object that get the config `yaml` file from current path.
|
86
108
|
|
87
109
|
:param name: A name of config data that will read by Yaml Loader object.
|
88
110
|
:param externals: An external parameters
|
89
111
|
"""
|
90
112
|
|
113
|
+
conf_name: ClassVar[str] = "workflows-conf"
|
114
|
+
|
91
115
|
def __init__(
|
92
116
|
self,
|
93
117
|
name: str,
|
@@ -106,12 +130,16 @@ class Loader(SimLoad):
|
|
106
130
|
def config(cls, path: str | None = None) -> Params:
|
107
131
|
"""Load Config data from ``workflows-conf.yaml`` file."""
|
108
132
|
return Params.model_validate(
|
109
|
-
YamlEnvFl(path or "./
|
133
|
+
YamlEnvFl(path or f"./{cls.conf_name}.yaml").read()
|
110
134
|
)
|
111
135
|
|
112
136
|
|
113
137
|
def map_params(value: Any, params: dict[str, Any]) -> Any:
|
114
|
-
"""Map caller value that found from ``RE_CALLER``
|
138
|
+
"""Map caller value that found from ``RE_CALLER`` regular expression.
|
139
|
+
|
140
|
+
:param value: A value that want to mapped with an params
|
141
|
+
:param params: A parameter value that getting with matched regular
|
142
|
+
expression.
|
115
143
|
|
116
144
|
:rtype: Any
|
117
145
|
:returns: An any getter value from the params input.
|
ddeutil/workflow/pipeline.py
CHANGED
@@ -6,38 +6,52 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import inspect
|
9
|
+
import itertools
|
9
10
|
import logging
|
10
11
|
import subprocess
|
12
|
+
import time
|
11
13
|
from abc import ABC, abstractmethod
|
12
|
-
from datetime import date, datetime
|
13
14
|
from inspect import Parameter
|
15
|
+
from queue import Queue
|
14
16
|
from subprocess import CompletedProcess
|
15
|
-
from typing import Any, Callable,
|
17
|
+
from typing import Any, Callable, Optional, Union
|
16
18
|
|
17
|
-
|
19
|
+
import msgspec as spec
|
18
20
|
from pydantic import BaseModel, Field
|
19
21
|
from pydantic.functional_validators import model_validator
|
20
22
|
from typing_extensions import Self
|
21
23
|
|
22
24
|
from .__regex import RegexConf
|
23
|
-
from .__types import DictData
|
24
|
-
from .exceptions import
|
25
|
+
from .__types import DictData, DictStr
|
26
|
+
from .exceptions import TaskException
|
25
27
|
from .loader import Loader, map_params
|
26
|
-
from .utils import make_registry
|
28
|
+
from .utils import Params, make_registry
|
27
29
|
|
28
30
|
|
29
31
|
class BaseStage(BaseModel, ABC):
|
30
|
-
"""Base Stage Model."""
|
31
|
-
|
32
|
-
id: Optional[str] =
|
33
|
-
|
32
|
+
"""Base Stage Model that keep only id and name fields."""
|
33
|
+
|
34
|
+
id: Optional[str] = Field(
|
35
|
+
default=None,
|
36
|
+
description=(
|
37
|
+
"The stage ID that use to keep execution output or getting by job "
|
38
|
+
"owner."
|
39
|
+
),
|
40
|
+
)
|
41
|
+
name: str = Field(
|
42
|
+
description="The stage name that want to logging when start execution."
|
43
|
+
)
|
34
44
|
|
35
45
|
@abstractmethod
|
36
46
|
def execute(self, params: DictData) -> DictData:
|
47
|
+
"""Execute abstraction method that action something by sub-model class.
|
48
|
+
|
49
|
+
:param params: A parameter data that want to use in this execution.
|
50
|
+
"""
|
37
51
|
raise NotImplementedError("Stage should implement ``execute`` method.")
|
38
52
|
|
39
53
|
def set_outputs(self, rs: DictData, params: DictData) -> DictData:
|
40
|
-
"""Set outputs to params"""
|
54
|
+
"""Set an outputs from execution process to an input params."""
|
41
55
|
if self.id is None:
|
42
56
|
return params
|
43
57
|
|
@@ -61,7 +75,7 @@ class ShellStage(BaseStage):
|
|
61
75
|
"""Shell statement stage."""
|
62
76
|
|
63
77
|
shell: str
|
64
|
-
env:
|
78
|
+
env: DictStr = Field(default_factory=dict)
|
65
79
|
|
66
80
|
@staticmethod
|
67
81
|
def __prepare_shell(shell: str):
|
@@ -100,7 +114,7 @@ class ShellStage(BaseStage):
|
|
100
114
|
if rs.returncode > 0:
|
101
115
|
print(f"{rs.stderr}\nRunning Statement:\n---\n{self.shell}")
|
102
116
|
# FIXME: raise err for this execution.
|
103
|
-
# raise
|
117
|
+
# raise TaskException(
|
104
118
|
# f"{rs.stderr}\nRunning Statement:\n---\n"
|
105
119
|
# f"{self.shell}"
|
106
120
|
# )
|
@@ -116,7 +130,7 @@ class PyStage(BaseStage):
|
|
116
130
|
run: str
|
117
131
|
vars: DictData = Field(default_factory=dict)
|
118
132
|
|
119
|
-
def
|
133
|
+
def get_vars(self, params: DictData) -> DictData:
|
120
134
|
"""Return variables"""
|
121
135
|
rs = self.vars.copy()
|
122
136
|
for p, v in self.vars.items():
|
@@ -149,12 +163,12 @@ class PyStage(BaseStage):
|
|
149
163
|
:returns: A parameters from an input that was mapped output if the stage
|
150
164
|
ID was set.
|
151
165
|
"""
|
152
|
-
_globals: DictData = globals() | params | self.
|
166
|
+
_globals: DictData = globals() | params | self.get_vars(params)
|
153
167
|
_locals: DictData = {}
|
154
168
|
try:
|
155
169
|
exec(map_params(self.run, params), _globals, _locals)
|
156
170
|
except Exception as err:
|
157
|
-
raise
|
171
|
+
raise TaskException(
|
158
172
|
f"{err.__class__.__name__}: {err}\nRunning Statement:\n---\n"
|
159
173
|
f"{self.run}"
|
160
174
|
) from None
|
@@ -164,13 +178,17 @@ class PyStage(BaseStage):
|
|
164
178
|
return params | {k: _globals[k] for k in params if k in _globals}
|
165
179
|
|
166
180
|
|
167
|
-
class TaskSearch(
|
168
|
-
"""Task Search
|
181
|
+
class TaskSearch(spec.Struct, kw_only=True, tag="task"):
|
182
|
+
"""Task Search Struct that use the `msgspec` for the best performance."""
|
169
183
|
|
170
184
|
path: str
|
171
185
|
func: str
|
172
186
|
tag: str
|
173
187
|
|
188
|
+
def to_dict(self) -> DictData:
|
189
|
+
"""Return dict data from struct fields."""
|
190
|
+
return {f: getattr(self, f) for f in self.__struct_fields__}
|
191
|
+
|
174
192
|
|
175
193
|
class TaskStage(BaseStage):
|
176
194
|
"""Task executor stage that running the Python function."""
|
@@ -183,7 +201,7 @@ class TaskStage(BaseStage):
|
|
183
201
|
"""Extract Task string value to task function."""
|
184
202
|
if not (found := RegexConf.RE_TASK_FMT.search(task)):
|
185
203
|
raise ValueError("Task does not match with task format regex.")
|
186
|
-
tasks = TaskSearch(**found.groupdict())
|
204
|
+
tasks: TaskSearch = TaskSearch(**found.groupdict())
|
187
205
|
|
188
206
|
# NOTE: Registry object should implement on this package only.
|
189
207
|
# TODO: This prefix value to search registry should dynamic with
|
@@ -238,153 +256,131 @@ Stage = Union[
|
|
238
256
|
|
239
257
|
|
240
258
|
class Strategy(BaseModel):
|
241
|
-
"""Strategy Model
|
259
|
+
"""Strategy Model that will combine a matrix together for running the
|
260
|
+
special job.
|
261
|
+
|
262
|
+
Examples:
|
263
|
+
>>> strategy = {
|
264
|
+
... 'matrix': {
|
265
|
+
... 'first': [1, 2, 3],
|
266
|
+
... 'second': ['foo', 'bar']
|
267
|
+
... },
|
268
|
+
... 'include': [{'first': 4, 'second': 'foo'}],
|
269
|
+
... 'exclude': [{'first': 1, 'second': 'bar'}],
|
270
|
+
... }
|
271
|
+
"""
|
242
272
|
|
243
|
-
|
244
|
-
|
245
|
-
|
273
|
+
fail_fast: bool = Field(default=False)
|
274
|
+
max_parallel: int = Field(default=-1)
|
275
|
+
matrix: dict[str, Union[list[str], list[int]]] = Field(default_factory=dict)
|
276
|
+
include: list[dict[str, Union[str, int]]] = Field(default_factory=list)
|
277
|
+
exclude: list[dict[str, Union[str, int]]] = Field(default_factory=list)
|
278
|
+
|
279
|
+
@model_validator(mode="before")
|
280
|
+
def __prepare_keys(cls, values: DictData) -> DictData:
|
281
|
+
if "max-parallel" in values:
|
282
|
+
values["max_parallel"] = values.pop("max-parallel")
|
283
|
+
if "fail-fast" in values:
|
284
|
+
values["fail_fast"] = values.pop("fail-fast")
|
285
|
+
return values
|
246
286
|
|
247
287
|
|
248
288
|
class Job(BaseModel):
|
249
289
|
"""Job Model"""
|
250
290
|
|
291
|
+
runs_on: Optional[str] = Field(default=None)
|
251
292
|
stages: list[Stage] = Field(default_factory=list)
|
252
293
|
needs: list[str] = Field(default_factory=list)
|
253
294
|
strategy: Strategy = Field(default_factory=Strategy)
|
254
295
|
|
296
|
+
@model_validator(mode="before")
|
297
|
+
def __prepare_keys(cls, values: DictData) -> DictData:
|
298
|
+
if "runs-on" in values:
|
299
|
+
values["runs_on"] = values.pop("runs-on")
|
300
|
+
return values
|
301
|
+
|
255
302
|
def stage(self, stage_id: str) -> Stage:
|
303
|
+
"""Return stage model that match with an input stage ID."""
|
256
304
|
for stage in self.stages:
|
257
305
|
if stage_id == (stage.id or ""):
|
258
306
|
return stage
|
259
307
|
raise ValueError(f"Stage ID {stage_id} does not exists")
|
260
308
|
|
309
|
+
def make_strategy(self) -> list[DictStr]:
|
310
|
+
"""Return List of combination of matrix values that already filter with
|
311
|
+
exclude and add include values.
|
312
|
+
"""
|
313
|
+
if not (mt := self.strategy.matrix):
|
314
|
+
return [{}]
|
315
|
+
final: list[DictStr] = []
|
316
|
+
for r in [
|
317
|
+
{_k: _v for e in mapped for _k, _v in e.items()}
|
318
|
+
for mapped in itertools.product(
|
319
|
+
*[[{k: v} for v in vs] for k, vs in mt.items()]
|
320
|
+
)
|
321
|
+
]:
|
322
|
+
if any(
|
323
|
+
all(r[k] == v for k, v in exclude.items())
|
324
|
+
for exclude in self.strategy.exclude
|
325
|
+
):
|
326
|
+
continue
|
327
|
+
final.append(r)
|
328
|
+
|
329
|
+
if not final:
|
330
|
+
return [{}]
|
331
|
+
|
332
|
+
for include in self.strategy.include:
|
333
|
+
if include.keys() != final[0].keys():
|
334
|
+
raise ValueError("Include should have the keys equal to matrix")
|
335
|
+
if any(all(include[k] == v for k, v in f.items()) for f in final):
|
336
|
+
continue
|
337
|
+
final.append(include)
|
338
|
+
return final
|
339
|
+
|
261
340
|
def execute(self, params: DictData | None = None) -> DictData:
|
262
341
|
"""Execute job with passing dynamic parameters from the pipeline."""
|
263
|
-
for
|
264
|
-
|
265
|
-
|
266
|
-
#
|
267
|
-
|
268
|
-
|
269
|
-
|
342
|
+
for strategy in self.make_strategy():
|
343
|
+
params.update({"matrix": strategy})
|
344
|
+
|
345
|
+
# IMPORTANT: The stage execution only run sequentially one-by-one.
|
346
|
+
for stage in self.stages:
|
347
|
+
logging.info(
|
348
|
+
f"[JOB]: Start execute the stage: "
|
349
|
+
f"{(stage.id if stage.id else stage.name)!r}"
|
350
|
+
)
|
351
|
+
|
352
|
+
# NOTE:
|
353
|
+
# I do not use below syntax because `params` dict be the
|
354
|
+
# reference memory pointer and it was changed when I action
|
355
|
+
# anything like update or re-construct this.
|
356
|
+
# ... params |= stage.execute(params=params)
|
357
|
+
stage.execute(params=params)
|
358
|
+
# TODO: We should not return matrix key to outside
|
270
359
|
return params
|
271
360
|
|
272
361
|
|
273
|
-
class BaseParams(BaseModel, ABC):
|
274
|
-
"""Base Parameter that use to make Params Model."""
|
275
|
-
|
276
|
-
desc: Optional[str] = None
|
277
|
-
required: bool = True
|
278
|
-
type: str
|
279
|
-
|
280
|
-
@abstractmethod
|
281
|
-
def receive(self, value: Optional[Any] = None) -> Any:
|
282
|
-
raise ValueError(
|
283
|
-
"Receive value and validate typing before return valid value."
|
284
|
-
)
|
285
|
-
|
286
|
-
|
287
|
-
class DefaultParams(BaseParams):
|
288
|
-
"""Default Parameter that will check default if it required"""
|
289
|
-
|
290
|
-
default: Optional[str] = None
|
291
|
-
|
292
|
-
@abstractmethod
|
293
|
-
def receive(self, value: Optional[Any] = None) -> Any:
|
294
|
-
raise ValueError(
|
295
|
-
"Receive value and validate typing before return valid value."
|
296
|
-
)
|
297
|
-
|
298
|
-
@model_validator(mode="after")
|
299
|
-
def check_default(self) -> Self:
|
300
|
-
if not self.required and self.default is None:
|
301
|
-
raise ValueError(
|
302
|
-
"Default should set when this parameter does not required."
|
303
|
-
)
|
304
|
-
return self
|
305
|
-
|
306
|
-
|
307
|
-
class DatetimeParams(DefaultParams):
|
308
|
-
"""Datetime parameter."""
|
309
|
-
|
310
|
-
type: Literal["datetime"] = "datetime"
|
311
|
-
required: bool = False
|
312
|
-
default: datetime = Field(default_factory=dt_now)
|
313
|
-
|
314
|
-
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
315
|
-
if value is None:
|
316
|
-
return self.default
|
317
|
-
|
318
|
-
if isinstance(value, datetime):
|
319
|
-
return value
|
320
|
-
elif isinstance(value, date):
|
321
|
-
return datetime(value.year, value.month, value.day)
|
322
|
-
elif not isinstance(value, str):
|
323
|
-
raise ValueError(
|
324
|
-
f"Value that want to convert to datetime does not support for "
|
325
|
-
f"type: {type(value)}"
|
326
|
-
)
|
327
|
-
return datetime.fromisoformat(value)
|
328
|
-
|
329
|
-
|
330
|
-
class StrParams(DefaultParams):
|
331
|
-
"""String parameter."""
|
332
|
-
|
333
|
-
type: Literal["str"] = "str"
|
334
|
-
|
335
|
-
def receive(self, value: Optional[str] = None) -> str | None:
|
336
|
-
if value is None:
|
337
|
-
return self.default
|
338
|
-
return str(value)
|
339
|
-
|
340
|
-
|
341
|
-
class IntParams(DefaultParams):
|
342
|
-
"""Integer parameter."""
|
343
|
-
|
344
|
-
type: Literal["int"] = "int"
|
345
|
-
|
346
|
-
def receive(self, value: Optional[int] = None) -> int | None:
|
347
|
-
if value is None:
|
348
|
-
return self.default
|
349
|
-
if not isinstance(value, int):
|
350
|
-
try:
|
351
|
-
return int(str(value))
|
352
|
-
except TypeError as err:
|
353
|
-
raise ValueError(
|
354
|
-
f"Value that want to convert to integer does not support "
|
355
|
-
f"for type: {type(value)}"
|
356
|
-
) from err
|
357
|
-
return value
|
358
|
-
|
359
|
-
|
360
|
-
class ChoiceParams(BaseParams):
|
361
|
-
type: Literal["choice"] = "choice"
|
362
|
-
options: list[str]
|
363
|
-
|
364
|
-
def receive(self, value: Optional[str] = None) -> str:
|
365
|
-
"""Receive value that match with options."""
|
366
|
-
# NOTE:
|
367
|
-
# Return the first value in options if does not pass any input value
|
368
|
-
if value is None:
|
369
|
-
return self.options[0]
|
370
|
-
if any(value not in self.options):
|
371
|
-
raise ValueError(f"{value} does not match any value in options")
|
372
|
-
return value
|
373
|
-
|
374
|
-
|
375
|
-
Params = Union[
|
376
|
-
ChoiceParams,
|
377
|
-
DatetimeParams,
|
378
|
-
StrParams,
|
379
|
-
]
|
380
|
-
|
381
|
-
|
382
362
|
class Pipeline(BaseModel):
|
383
|
-
"""Pipeline Model
|
363
|
+
"""Pipeline Model this is the main feature of this project because it use to
|
364
|
+
be workflow data for running everywhere that you want. It use lightweight
|
365
|
+
coding line to execute it.
|
366
|
+
"""
|
384
367
|
|
385
368
|
params: dict[str, Params] = Field(default_factory=dict)
|
386
369
|
jobs: dict[str, Job]
|
387
370
|
|
371
|
+
@model_validator(mode="before")
|
372
|
+
def __prepare_params(cls, values: DictData) -> DictData:
|
373
|
+
if params := values.pop("params", {}):
|
374
|
+
values["params"] = {
|
375
|
+
p: (
|
376
|
+
{"type": params[p]}
|
377
|
+
if isinstance(params[p], str)
|
378
|
+
else params[p]
|
379
|
+
)
|
380
|
+
for p in params
|
381
|
+
}
|
382
|
+
return values
|
383
|
+
|
388
384
|
@classmethod
|
389
385
|
def from_loader(
|
390
386
|
cls,
|
@@ -399,6 +395,10 @@ class Pipeline(BaseModel):
|
|
399
395
|
params=loader.data["params"],
|
400
396
|
)
|
401
397
|
|
398
|
+
@model_validator(mode="after")
|
399
|
+
def job_checking_needs(self):
|
400
|
+
return self
|
401
|
+
|
402
402
|
def job(self, name: str) -> Job:
|
403
403
|
"""Return Job model that exists on this pipeline.
|
404
404
|
|
@@ -406,13 +406,23 @@ class Pipeline(BaseModel):
|
|
406
406
|
:type name: str
|
407
407
|
|
408
408
|
:rtype: Job
|
409
|
+
:returns: A job model that exists on this pipeline by input name.
|
409
410
|
"""
|
410
411
|
if name not in self.jobs:
|
411
|
-
raise ValueError(f"Job {name} does not exists")
|
412
|
+
raise ValueError(f"Job {name!r} does not exists")
|
412
413
|
return self.jobs[name]
|
413
414
|
|
414
|
-
def execute(
|
415
|
-
|
415
|
+
def execute(
|
416
|
+
self,
|
417
|
+
params: DictData | None = None,
|
418
|
+
time_out: int = 60,
|
419
|
+
) -> DictData:
|
420
|
+
"""Execute pipeline with passing dynamic parameters to any jobs that
|
421
|
+
included in the pipeline.
|
422
|
+
|
423
|
+
:param params: An input parameters that use on pipeline execution.
|
424
|
+
:param time_out: A time out second value for limit time of this
|
425
|
+
execution.
|
416
426
|
|
417
427
|
See Also:
|
418
428
|
|
@@ -427,8 +437,7 @@ class Pipeline(BaseModel):
|
|
427
437
|
|
428
438
|
"""
|
429
439
|
params: DictData = params or {}
|
430
|
-
check_key
|
431
|
-
if check_key:
|
440
|
+
if check_key := tuple(f"{k!r}" for k in self.params if k not in params):
|
432
441
|
raise ValueError(
|
433
442
|
f"Parameters that needed on pipeline does not pass: "
|
434
443
|
f"{', '.join(check_key)}."
|
@@ -445,12 +454,39 @@ class Pipeline(BaseModel):
|
|
445
454
|
for k in params
|
446
455
|
if k in self.params
|
447
456
|
}
|
448
|
-
)
|
457
|
+
),
|
458
|
+
"jobs": {},
|
449
459
|
}
|
460
|
+
|
461
|
+
jq = Queue()
|
450
462
|
for job_id in self.jobs:
|
451
|
-
|
463
|
+
jq.put(job_id)
|
464
|
+
|
465
|
+
ts: float = time.monotonic()
|
466
|
+
not_time_out_flag = True
|
467
|
+
|
468
|
+
# IMPORTANT: The job execution can run parallel and waiting by needed.
|
469
|
+
while not jq.empty() and (
|
470
|
+
not_time_out_flag := ((time.monotonic() - ts) < time_out)
|
471
|
+
):
|
472
|
+
job_id: str = jq.get()
|
473
|
+
logging.info(f"[PIPELINE]: Start execute the job: {job_id!r}")
|
452
474
|
job: Job = self.jobs[job_id]
|
453
475
|
# TODO: Condition on ``needs`` of this job was set. It should create
|
454
476
|
# multithreading process on this step.
|
477
|
+
# But, I don't know how to handle changes params between each job
|
478
|
+
# execution while its use them together.
|
479
|
+
# ---
|
480
|
+
# >>> import multiprocessing
|
481
|
+
# >>> with multiprocessing.Pool(processes=3) as pool:
|
482
|
+
# ... results = pool.starmap(merge_names, ('', '', ...))
|
483
|
+
if any(params["jobs"].get(need) for need in job.needs):
|
484
|
+
jq.put(job_id)
|
455
485
|
job.execute(params=params)
|
486
|
+
params["jobs"][job_id] = {
|
487
|
+
"stages": params.pop("stages", {}),
|
488
|
+
"matrix": params.pop("matrix", {}),
|
489
|
+
}
|
490
|
+
if not not_time_out_flag:
|
491
|
+
raise RuntimeError("Execution of pipeline was time out")
|
456
492
|
return params
|