ddeutil-workflow 0.0.9__py3-none-any.whl → 0.0.10__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/api.py +16 -16
- ddeutil/workflow/cli.py +105 -22
- ddeutil/workflow/cron.py +116 -26
- ddeutil/workflow/exceptions.py +3 -0
- ddeutil/workflow/log.py +66 -59
- ddeutil/workflow/on.py +10 -4
- ddeutil/workflow/pipeline.py +267 -223
- ddeutil/workflow/repeat.py +66 -39
- ddeutil/workflow/route.py +59 -38
- ddeutil/workflow/scheduler.py +355 -187
- ddeutil/workflow/stage.py +15 -11
- ddeutil/workflow/utils.py +142 -6
- {ddeutil_workflow-0.0.9.dist-info → ddeutil_workflow-0.0.10.dist-info}/METADATA +17 -108
- ddeutil_workflow-0.0.10.dist-info/RECORD +21 -0
- ddeutil_workflow-0.0.10.dist-info/entry_points.txt +2 -0
- ddeutil/workflow/loader.py +0 -132
- ddeutil_workflow-0.0.9.dist-info/RECORD +0 -22
- ddeutil_workflow-0.0.9.dist-info/entry_points.txt +0 -2
- {ddeutil_workflow-0.0.9.dist-info → ddeutil_workflow-0.0.10.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.9.dist-info → ddeutil_workflow-0.0.10.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.9.dist-info → ddeutil_workflow-0.0.10.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stage.py
CHANGED
@@ -19,7 +19,6 @@ from __future__ import annotations
|
|
19
19
|
|
20
20
|
import contextlib
|
21
21
|
import inspect
|
22
|
-
import logging
|
23
22
|
import os
|
24
23
|
import subprocess
|
25
24
|
import sys
|
@@ -46,6 +45,7 @@ from typing_extensions import Self
|
|
46
45
|
|
47
46
|
from .__types import DictData, DictStr, Re, TupleStr
|
48
47
|
from .exceptions import StageException
|
48
|
+
from .log import get_logger
|
49
49
|
from .utils import (
|
50
50
|
Registry,
|
51
51
|
Result,
|
@@ -58,6 +58,9 @@ from .utils import (
|
|
58
58
|
)
|
59
59
|
|
60
60
|
P = ParamSpec("P")
|
61
|
+
logger = get_logger("ddeutil.workflow")
|
62
|
+
|
63
|
+
|
61
64
|
__all__: TupleStr = (
|
62
65
|
"Stage",
|
63
66
|
"EmptyStage",
|
@@ -86,7 +89,7 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
|
|
86
89
|
return func(self, *args, **kwargs).set_run_id(self.run_id)
|
87
90
|
except Exception as err:
|
88
91
|
# NOTE: Start catching error from the stage execution.
|
89
|
-
|
92
|
+
logger.error(
|
90
93
|
f"({self.run_id}) [STAGE]: {err.__class__.__name__}: {err}"
|
91
94
|
)
|
92
95
|
if str2bool(
|
@@ -141,6 +144,7 @@ class BaseStage(BaseModel, ABC):
|
|
141
144
|
default=None,
|
142
145
|
description="A running stage ID.",
|
143
146
|
repr=False,
|
147
|
+
exclude=True,
|
144
148
|
)
|
145
149
|
|
146
150
|
@model_validator(mode="after")
|
@@ -191,7 +195,7 @@ class BaseStage(BaseModel, ABC):
|
|
191
195
|
self.id
|
192
196
|
or str2bool(os.getenv("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false"))
|
193
197
|
):
|
194
|
-
|
198
|
+
logger.debug(
|
195
199
|
f"({self.run_id}) [STAGE]: Output does not set because this "
|
196
200
|
f"stage does not set ID or default stage ID config flag not be "
|
197
201
|
f"True."
|
@@ -208,7 +212,7 @@ class BaseStage(BaseModel, ABC):
|
|
208
212
|
_id: str = gen_id(param2template(self.name, params=to))
|
209
213
|
|
210
214
|
# NOTE: Set the output to that stage generated ID.
|
211
|
-
|
215
|
+
logger.debug(
|
212
216
|
f"({self.run_id}) [STAGE]: Set output complete with stage ID: {_id}"
|
213
217
|
)
|
214
218
|
to["stages"][_id] = {"outputs": output}
|
@@ -231,7 +235,7 @@ class BaseStage(BaseModel, ABC):
|
|
231
235
|
raise TypeError("Return type of condition does not be boolean")
|
232
236
|
return not rs
|
233
237
|
except Exception as err:
|
234
|
-
|
238
|
+
logger.error(f"({self.run_id}) [STAGE]: {err}")
|
235
239
|
raise StageException(f"{err.__class__.__name__}: {err}") from err
|
236
240
|
|
237
241
|
|
@@ -258,7 +262,7 @@ class EmptyStage(BaseStage):
|
|
258
262
|
:param params: A context data that want to add output result. But this
|
259
263
|
stage does not pass any output.
|
260
264
|
"""
|
261
|
-
|
265
|
+
logger.info(
|
262
266
|
f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
|
263
267
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
264
268
|
)
|
@@ -314,7 +318,7 @@ class BashStage(BaseStage):
|
|
314
318
|
# NOTE: Make this .sh file able to executable.
|
315
319
|
make_exec(f"./{f_name}")
|
316
320
|
|
317
|
-
|
321
|
+
logger.debug(
|
318
322
|
f"({self.run_id}) [STAGE]: Start create `.sh` file and running a "
|
319
323
|
f"bash statement."
|
320
324
|
)
|
@@ -336,7 +340,7 @@ class BashStage(BaseStage):
|
|
336
340
|
with self.__prepare_bash(
|
337
341
|
bash=bash, env=param2template(self.env, params)
|
338
342
|
) as sh:
|
339
|
-
|
343
|
+
logger.info(f"({self.run_id}) [STAGE]: Shell-Execute: {sh}")
|
340
344
|
rs: CompletedProcess = subprocess.run(
|
341
345
|
sh,
|
342
346
|
shell=False,
|
@@ -424,7 +428,7 @@ class PyStage(BaseStage):
|
|
424
428
|
_locals: DictData = {}
|
425
429
|
|
426
430
|
# NOTE: Start exec the run statement.
|
427
|
-
|
431
|
+
logger.info(f"({self.run_id}) [STAGE]: Py-Execute: {self.name}")
|
428
432
|
exec(run, _globals, _locals)
|
429
433
|
|
430
434
|
return Result(
|
@@ -531,7 +535,7 @@ class HookStage(BaseStage):
|
|
531
535
|
if k.removeprefix("_") in args:
|
532
536
|
args[k] = args.pop(k.removeprefix("_"))
|
533
537
|
|
534
|
-
|
538
|
+
logger.info(
|
535
539
|
f"({self.run_id}) [STAGE]: Hook-Execute: {t_func.name}@{t_func.tag}"
|
536
540
|
)
|
537
541
|
rs: DictData = t_func(**param2template(args, params))
|
@@ -583,7 +587,7 @@ class TriggerStage(BaseStage):
|
|
583
587
|
pipe: Pipeline = Pipeline.from_loader(
|
584
588
|
name=_trigger, externals={"run_id": self.run_id}
|
585
589
|
)
|
586
|
-
|
590
|
+
logger.info(f"({self.run_id}) [STAGE]: Trigger-Execute: {_trigger!r}")
|
587
591
|
return pipe.execute(params=param2template(self.params, params))
|
588
592
|
|
589
593
|
|
ddeutil/workflow/utils.py
CHANGED
@@ -14,14 +14,14 @@ from abc import ABC, abstractmethod
|
|
14
14
|
from ast import Call, Constant, Expr, Module, Name, parse
|
15
15
|
from collections.abc import Iterator
|
16
16
|
from datetime import date, datetime
|
17
|
-
from functools import wraps
|
17
|
+
from functools import cached_property, wraps
|
18
18
|
from hashlib import md5
|
19
19
|
from importlib import import_module
|
20
20
|
from inspect import isfunction
|
21
21
|
from itertools import chain, islice, product
|
22
22
|
from pathlib import Path
|
23
23
|
from random import randrange
|
24
|
-
from typing import Any, Callable, Literal, Optional, Protocol, Union
|
24
|
+
from typing import Any, Callable, Literal, Optional, Protocol, TypeVar, Union
|
25
25
|
from zoneinfo import ZoneInfo
|
26
26
|
|
27
27
|
try:
|
@@ -30,16 +30,20 @@ except ImportError:
|
|
30
30
|
from typing_extensions import ParamSpec
|
31
31
|
|
32
32
|
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy, str2bool
|
33
|
-
from ddeutil.io import PathData, search_env_replace
|
33
|
+
from ddeutil.io import PathData, PathSearch, YamlFlResolve, search_env_replace
|
34
34
|
from ddeutil.io.models.lineage import dt_now
|
35
35
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
36
|
+
from pydantic.functional_serializers import field_serializer
|
36
37
|
from pydantic.functional_validators import model_validator
|
37
38
|
from typing_extensions import Self
|
38
39
|
|
39
40
|
from .__types import DictData, Matrix, Re
|
40
41
|
from .exceptions import ParamValueException, UtilException
|
41
42
|
|
43
|
+
logger = logging.getLogger("ddeutil.workflow")
|
42
44
|
P = ParamSpec("P")
|
45
|
+
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
46
|
+
AnyModelType = type[AnyModel]
|
43
47
|
|
44
48
|
|
45
49
|
def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
|
@@ -51,11 +55,13 @@ def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
|
|
51
55
|
)
|
52
56
|
|
53
57
|
|
54
|
-
def delay() -> None:
|
58
|
+
def delay(second: float = 0) -> None:
|
55
59
|
"""Delay time that use time.sleep with random second value between
|
56
60
|
0.00 - 0.99 seconds.
|
61
|
+
|
62
|
+
:param second: A second number that want to adds-on random value.
|
57
63
|
"""
|
58
|
-
time.sleep(randrange(0, 99, step=10) / 100)
|
64
|
+
time.sleep(second + randrange(0, 99, step=10) / 100)
|
59
65
|
|
60
66
|
|
61
67
|
class Engine(BaseModel):
|
@@ -143,6 +149,112 @@ def config() -> ConfParams:
|
|
143
149
|
)
|
144
150
|
|
145
151
|
|
152
|
+
class SimLoad:
|
153
|
+
"""Simple Load Object that will search config data by given some identity
|
154
|
+
value like name of pipeline or on.
|
155
|
+
|
156
|
+
:param name: A name of config data that will read by Yaml Loader object.
|
157
|
+
:param params: A Params model object.
|
158
|
+
:param externals: An external parameters
|
159
|
+
|
160
|
+
Noted:
|
161
|
+
---
|
162
|
+
The config data should have ``type`` key for modeling validation that
|
163
|
+
make this loader know what is config should to do pass to.
|
164
|
+
|
165
|
+
... <identity-key>:
|
166
|
+
... type: <importable-object>
|
167
|
+
... <key-data>: <value-data>
|
168
|
+
... ...
|
169
|
+
|
170
|
+
"""
|
171
|
+
|
172
|
+
def __init__(
|
173
|
+
self,
|
174
|
+
name: str,
|
175
|
+
params: ConfParams,
|
176
|
+
externals: DictData | None = None,
|
177
|
+
) -> None:
|
178
|
+
self.data: DictData = {}
|
179
|
+
for file in PathSearch(params.engine.paths.conf).files:
|
180
|
+
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
181
|
+
data := YamlFlResolve(file).read().get(name, {})
|
182
|
+
):
|
183
|
+
self.data = data
|
184
|
+
|
185
|
+
# VALIDATE: check the data that reading should not empty.
|
186
|
+
if not self.data:
|
187
|
+
raise ValueError(f"Config {name!r} does not found on conf path")
|
188
|
+
|
189
|
+
self.conf_params: ConfParams = params
|
190
|
+
self.externals: DictData = externals or {}
|
191
|
+
self.data.update(self.externals)
|
192
|
+
|
193
|
+
@classmethod
|
194
|
+
def finds(
|
195
|
+
cls,
|
196
|
+
obj: object,
|
197
|
+
params: ConfParams,
|
198
|
+
*,
|
199
|
+
include: list[str] | None = None,
|
200
|
+
exclude: list[str] | None = None,
|
201
|
+
) -> Iterator[tuple[str, DictData]]:
|
202
|
+
"""Find all data that match with object type in config path. This class
|
203
|
+
method can use include and exclude list of identity name for filter and
|
204
|
+
adds-on.
|
205
|
+
"""
|
206
|
+
exclude: list[str] = exclude or []
|
207
|
+
for file in PathSearch(params.engine.paths.conf).files:
|
208
|
+
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
209
|
+
values := YamlFlResolve(file).read()
|
210
|
+
):
|
211
|
+
for key, data in values.items():
|
212
|
+
if key in exclude:
|
213
|
+
continue
|
214
|
+
if issubclass(get_type(data["type"], params), obj) and (
|
215
|
+
include is None or all(i in data for i in include)
|
216
|
+
):
|
217
|
+
yield key, data
|
218
|
+
|
219
|
+
@cached_property
|
220
|
+
def type(self) -> AnyModelType:
|
221
|
+
"""Return object of string type which implement on any registry. The
|
222
|
+
object type.
|
223
|
+
|
224
|
+
:rtype: AnyModelType
|
225
|
+
"""
|
226
|
+
if not (_typ := self.data.get("type")):
|
227
|
+
raise ValueError(
|
228
|
+
f"the 'type' value: {_typ} does not exists in config data."
|
229
|
+
)
|
230
|
+
return get_type(_typ, self.conf_params)
|
231
|
+
|
232
|
+
|
233
|
+
class Loader(SimLoad):
|
234
|
+
"""Loader Object that get the config `yaml` file from current path.
|
235
|
+
|
236
|
+
:param name: A name of config data that will read by Yaml Loader object.
|
237
|
+
:param externals: An external parameters
|
238
|
+
"""
|
239
|
+
|
240
|
+
@classmethod
|
241
|
+
def finds(
|
242
|
+
cls,
|
243
|
+
obj: object,
|
244
|
+
*,
|
245
|
+
include: list[str] | None = None,
|
246
|
+
exclude: list[str] | None = None,
|
247
|
+
**kwargs,
|
248
|
+
) -> DictData:
|
249
|
+
"""Override the find class method from the Simple Loader object."""
|
250
|
+
return super().finds(
|
251
|
+
obj=obj, params=config(), include=include, exclude=exclude
|
252
|
+
)
|
253
|
+
|
254
|
+
def __init__(self, name: str, externals: DictData) -> None:
|
255
|
+
super().__init__(name, config(), externals)
|
256
|
+
|
257
|
+
|
146
258
|
def gen_id(
|
147
259
|
value: Any,
|
148
260
|
*,
|
@@ -176,6 +288,26 @@ def gen_id(
|
|
176
288
|
).hexdigest()
|
177
289
|
|
178
290
|
|
291
|
+
def get_type(t: str, params: ConfParams) -> AnyModelType:
|
292
|
+
"""Return import type from string importable value in the type key.
|
293
|
+
|
294
|
+
:param t: A importable type string.
|
295
|
+
:param params: A config parameters that use registry to search this
|
296
|
+
type.
|
297
|
+
:rtype: AnyModelType
|
298
|
+
"""
|
299
|
+
try:
|
300
|
+
# NOTE: Auto adding module prefix if it does not set
|
301
|
+
return import_string(f"ddeutil.workflow.{t}")
|
302
|
+
except ModuleNotFoundError:
|
303
|
+
for registry in params.engine.registry:
|
304
|
+
try:
|
305
|
+
return import_string(f"{registry}.{t}")
|
306
|
+
except ModuleNotFoundError:
|
307
|
+
continue
|
308
|
+
return import_string(f"{t}")
|
309
|
+
|
310
|
+
|
179
311
|
class TagFunc(Protocol):
|
180
312
|
"""Tag Function Protocol"""
|
181
313
|
|
@@ -260,6 +392,10 @@ class BaseParam(BaseModel, ABC):
|
|
260
392
|
"Receive value and validate typing before return valid value."
|
261
393
|
)
|
262
394
|
|
395
|
+
@field_serializer("type")
|
396
|
+
def __serializer_type(self, value: str) -> str:
|
397
|
+
return value
|
398
|
+
|
263
399
|
|
264
400
|
class DefaultParam(BaseParam):
|
265
401
|
"""Default Parameter that will check default if it required"""
|
@@ -583,7 +719,7 @@ def map_post_filter(
|
|
583
719
|
else:
|
584
720
|
value: Any = f_func(value, *args, **kwargs)
|
585
721
|
except Exception as err:
|
586
|
-
|
722
|
+
logger.warning(str(err))
|
587
723
|
raise UtilException(
|
588
724
|
f"The post-filter function: {func_name} does not fit with "
|
589
725
|
f"{value} (type: {type(value).__name__})."
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.10
|
4
4
|
Summary: Lightweight workflow orchestration with less dependencies
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -21,13 +21,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Requires-Python: >=3.9.13
|
22
22
|
Description-Content-Type: text/markdown
|
23
23
|
License-File: LICENSE
|
24
|
-
Requires-Dist: fmtutil
|
25
24
|
Requires-Dist: ddeutil-io
|
26
25
|
Requires-Dist: python-dotenv ==1.0.1
|
27
|
-
Requires-Dist: typer
|
26
|
+
Requires-Dist: typer <1.0.0,==0.12.5
|
28
27
|
Provides-Extra: api
|
29
|
-
Requires-Dist: fastapi[standard]
|
30
|
-
Requires-Dist: croniter ==3.0.3 ; extra == 'api'
|
28
|
+
Requires-Dist: fastapi[standard] <1.0.0,==0.112.2 ; extra == 'api'
|
31
29
|
Provides-Extra: schedule
|
32
30
|
Requires-Dist: schedule <2.0.0,==1.2.2 ; extra == 'schedule'
|
33
31
|
|
@@ -39,17 +37,6 @@ Requires-Dist: schedule <2.0.0,==1.2.2 ; extra == 'schedule'
|
|
39
37
|
[](https://github.com/ddeutils/ddeutil-workflow/blob/main/LICENSE)
|
40
38
|
[](https://github.com/psf/black)
|
41
39
|
|
42
|
-
**Table of Contents**:
|
43
|
-
|
44
|
-
- [Installation](#installation)
|
45
|
-
- [Getting Started](#getting-started)
|
46
|
-
- [On](#on)
|
47
|
-
- [Pipeline](#pipeline)
|
48
|
-
- [Usage](#usage)
|
49
|
-
- [Configuration](#configuration)
|
50
|
-
- [Future](#future)
|
51
|
-
- [Deployment](#deployment)
|
52
|
-
|
53
40
|
The **Lightweight workflow orchestration** with less dependencies the was created
|
54
41
|
for easy to make a simple metadata driven for data pipeline orchestration.
|
55
42
|
It can to use for data operator by a `.yaml` template.
|
@@ -103,82 +90,6 @@ this package with application add-ons, you should add `app` in installation;
|
|
103
90
|
> | ddeutil-workflow:python3.11 | `3.11` | :x: |
|
104
91
|
> | ddeutil-workflow:python3.12 | `3.12` | :x: |
|
105
92
|
|
106
|
-
## Getting Started
|
107
|
-
|
108
|
-
The main feature of this project is the `Pipeline` object that can call any
|
109
|
-
registries function. The pipeline can handle everything that you want to do, it
|
110
|
-
will passing parameters and catching the output for re-use it to next step.
|
111
|
-
|
112
|
-
### On
|
113
|
-
|
114
|
-
The **On** is schedule object that receive crontab value and able to generate
|
115
|
-
datetime value with next or previous with any start point of an input datetime.
|
116
|
-
|
117
|
-
```yaml
|
118
|
-
# This file should keep under this path: `./root-path/conf-path/*`
|
119
|
-
on_every_5_min:
|
120
|
-
type: on.On
|
121
|
-
cron: "*/5 * * * *"
|
122
|
-
```
|
123
|
-
|
124
|
-
```python
|
125
|
-
from ddeutil.workflow.on import On
|
126
|
-
|
127
|
-
# NOTE: Start load the on data from `.yaml` template file with this key.
|
128
|
-
schedule = On.from_loader(name='on_every_5_min', externals={})
|
129
|
-
|
130
|
-
assert '*/5 * * * *' == str(schedule.cronjob)
|
131
|
-
|
132
|
-
cron_iter = schedule.generate('2022-01-01 00:00:00')
|
133
|
-
|
134
|
-
assert "2022-01-01 00:05:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
|
135
|
-
assert "2022-01-01 00:10:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
|
136
|
-
assert "2022-01-01 00:15:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
|
137
|
-
```
|
138
|
-
|
139
|
-
### Pipeline
|
140
|
-
|
141
|
-
The **Pipeline** object that is the core feature of this project.
|
142
|
-
|
143
|
-
```yaml
|
144
|
-
# This file should keep under this path: `./root-path/conf-path/*`
|
145
|
-
pipeline-name:
|
146
|
-
type: ddeutil.workflow.pipeline.Pipeline
|
147
|
-
on: 'on_every_5_min'
|
148
|
-
params:
|
149
|
-
author-run:
|
150
|
-
type: str
|
151
|
-
run-date:
|
152
|
-
type: datetime
|
153
|
-
jobs:
|
154
|
-
first-job:
|
155
|
-
stages:
|
156
|
-
- name: "Empty stage do logging to console only!!"
|
157
|
-
```
|
158
|
-
|
159
|
-
```python
|
160
|
-
from ddeutil.workflow.pipeline import Pipeline
|
161
|
-
|
162
|
-
pipe = Pipeline.from_loader(name='pipeline-name', externals={})
|
163
|
-
pipe.execute(params={'author-run': 'Local Workflow', 'run-date': '2024-01-01'})
|
164
|
-
```
|
165
|
-
|
166
|
-
> [!NOTE]
|
167
|
-
> The above parameter can use short declarative statement. You can pass a parameter
|
168
|
-
> type to the key of a parameter name but it does not handler default value if you
|
169
|
-
> run this pipeline workflow with schedule.
|
170
|
-
>
|
171
|
-
> ```yaml
|
172
|
-
> ...
|
173
|
-
> params:
|
174
|
-
> author-run: str
|
175
|
-
> run-date: datetime
|
176
|
-
> ...
|
177
|
-
> ```
|
178
|
-
>
|
179
|
-
> And for the type, you can remove `ddeutil.workflow` prefix because we can find
|
180
|
-
> it by looping search from `WORKFLOW_CORE_REGISTRY` value.
|
181
|
-
|
182
93
|
## Usage
|
183
94
|
|
184
95
|
This is examples that use workflow file for running common Data Engineering
|
@@ -209,7 +120,9 @@ run_py_local:
|
|
209
120
|
url: https://open-data/
|
210
121
|
auth: ${API_ACCESS_REFRESH_TOKEN}
|
211
122
|
aws_s3_path: my-data/open-data/
|
212
|
-
|
123
|
+
|
124
|
+
# This Authentication code should implement with your custom hook function.
|
125
|
+
# The template allow you to use environment variable.
|
213
126
|
aws_access_client_id: ${AWS_ACCESS_CLIENT_ID}
|
214
127
|
aws_access_client_secret: ${AWS_ACCESS_CLIENT_SECRET}
|
215
128
|
```
|
@@ -227,28 +140,24 @@ run_py_local:
|
|
227
140
|
| `WORKFLOW_CORE_STAGE_RAISE_ERROR` | Core | true | A flag that all stage raise StageException from stage execution |
|
228
141
|
| `WORKFLOW_CORE_MAX_PIPELINE_POKING` | Core | 4 | |
|
229
142
|
| `WORKFLOW_CORE_MAX_JOB_PARALLEL` | Core | 2 | The maximum job number that able to run parallel in pipeline executor |
|
143
|
+
| `WORKFLOW_LOG_DEBUG_MODE` | Log | true | A flag that enable logging with debug level mode |
|
230
144
|
| `WORKFLOW_LOG_ENABLE_WRITE` | Log | true | A flag that enable logging object saving log to its destination |
|
231
145
|
|
232
146
|
|
233
147
|
**Application**:
|
234
148
|
|
235
|
-
| Environment | Default
|
236
|
-
|
237
|
-
| `WORKFLOW_APP_PROCESS_WORKER` | 2
|
238
|
-
| `
|
149
|
+
| Environment | Default | Description |
|
150
|
+
|-------------------------------------|----------------------------------|-------------------------------------------------------------------------|
|
151
|
+
| `WORKFLOW_APP_PROCESS_WORKER` | 2 | The maximum process worker number that run in scheduler app module |
|
152
|
+
| `WORKFLOW_APP_SCHEDULE_PER_PROCESS` | 100 | A schedule per process that run parallel |
|
153
|
+
| `WORKFLOW_APP_STOP_BOUNDARY_DELTA` | '{"minutes": 5, "seconds": 20}' | A time delta value that use to stop scheduler app in json string format |
|
239
154
|
|
240
155
|
**API server**:
|
241
156
|
|
242
|
-
| Environment
|
243
|
-
|
244
|
-
| `
|
245
|
-
|
246
|
-
## Future
|
247
|
-
|
248
|
-
The current milestone that will develop and necessary features that should to
|
249
|
-
implement on this project.
|
250
|
-
|
251
|
-
- ...
|
157
|
+
| Environment | Default | Description |
|
158
|
+
|--------------------------------------|---------|-----------------------------------------------------------------------------------|
|
159
|
+
| `WORKFLOW_API_ENABLE_ROUTE_WORKFLOW` | true | A flag that enable workflow route to manage execute manually and workflow logging |
|
160
|
+
| `WORKFLOW_API_ENABLE_ROUTE_SCHEDULE` | true | A flag that enable run scheduler |
|
252
161
|
|
253
162
|
## Deployment
|
254
163
|
|
@@ -270,4 +179,4 @@ like crontab job but via Python API.
|
|
270
179
|
|
271
180
|
> [!NOTE]
|
272
181
|
> If this package already deploy, it able to use
|
273
|
-
> `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80`
|
182
|
+
> `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80 --workers 4`
|
@@ -0,0 +1,21 @@
|
|
1
|
+
ddeutil/workflow/__about__.py,sha256=KJfEGSDA5LiPvdupul6ulxozLD2x2GhgUvq8t60EXsI,28
|
2
|
+
ddeutil/workflow/__init__.py,sha256=oGvg_BpKKb_FG76DlMvXTKD7BsYhqF9wB1r4x5Q_lQI,647
|
3
|
+
ddeutil/workflow/__types.py,sha256=SYMoxbENQX8uPsiCZkjtpHAqqHOh8rUrarAFicAJd0E,1773
|
4
|
+
ddeutil/workflow/api.py,sha256=WHgmjvnnkM4djwHt4bsAqsQjjcjAITRSrNrYYO6bgn8,2582
|
5
|
+
ddeutil/workflow/cli.py,sha256=snJCM-LAqvWwhkSB-3KRWwcgbHAkHn4cZ_DtmfOL5gs,3360
|
6
|
+
ddeutil/workflow/cron.py,sha256=uhp3E5pl_tX_H88bsDujcwdhZmOE53csyV-ouPpPdK8,25321
|
7
|
+
ddeutil/workflow/exceptions.py,sha256=UHojJQmnG9OVuRhXBAzDW6KZn-uKxvxV034QhUBUzUI,686
|
8
|
+
ddeutil/workflow/log.py,sha256=a5L5KWEGS5oiY_y6jugeAoRyAcnAhlt1HfeTU77YeI4,6036
|
9
|
+
ddeutil/workflow/on.py,sha256=Sxwnu0vPbIrMR_WWvH3_rOvD0tbiJntcB5378WoV19M,7163
|
10
|
+
ddeutil/workflow/pipeline.py,sha256=lPw9R3gOnBcU2eogClG8b4e4rTvpn5EbACLNZDuuR38,40825
|
11
|
+
ddeutil/workflow/repeat.py,sha256=0O8voTRB8lNMWsk1AbOYcio_b2_CW98yrfiEzNBb6gA,4954
|
12
|
+
ddeutil/workflow/route.py,sha256=pcn_oDzc2nl6txFhu_TWAnntLggEOFV9A3EVdnazcHI,2597
|
13
|
+
ddeutil/workflow/scheduler.py,sha256=Vu9FZbiHDnshQ2O1SnkVX686eSfaZzip-1oQohfuH_Y,20140
|
14
|
+
ddeutil/workflow/stage.py,sha256=XZEPImipk83kNX9UHrwu7wWUBigXZpEkWqagOG0oS70,20656
|
15
|
+
ddeutil/workflow/utils.py,sha256=ehIcT_fIQL8N0wU16VJDKAFN9q4h1FyMxyT5uTeMIA0,28561
|
16
|
+
ddeutil_workflow-0.0.10.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
17
|
+
ddeutil_workflow-0.0.10.dist-info/METADATA,sha256=7jdDYS2WtaZFpwUCo4ur8NAFWyi-omELh8YLB3EL9ok,9433
|
18
|
+
ddeutil_workflow-0.0.10.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
19
|
+
ddeutil_workflow-0.0.10.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
|
20
|
+
ddeutil_workflow-0.0.10.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
21
|
+
ddeutil_workflow-0.0.10.dist-info/RECORD,,
|
ddeutil/workflow/loader.py
DELETED
@@ -1,132 +0,0 @@
|
|
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 collections.abc import Iterator
|
9
|
-
from functools import cached_property
|
10
|
-
from typing import TypeVar
|
11
|
-
|
12
|
-
from ddeutil.core import import_string
|
13
|
-
from ddeutil.io import PathSearch, YamlFlResolve
|
14
|
-
from pydantic import BaseModel
|
15
|
-
|
16
|
-
from .__types import DictData
|
17
|
-
from .utils import ConfParams, config
|
18
|
-
|
19
|
-
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
20
|
-
AnyModelType = type[AnyModel]
|
21
|
-
|
22
|
-
|
23
|
-
class SimLoad:
|
24
|
-
"""Simple Load Object that will search config data by name.
|
25
|
-
|
26
|
-
:param name: A name of config data that will read by Yaml Loader object.
|
27
|
-
:param params: A Params model object.
|
28
|
-
:param externals: An external parameters
|
29
|
-
|
30
|
-
Noted:
|
31
|
-
The config data should have ``type`` key for engine can know what is
|
32
|
-
config should to do next.
|
33
|
-
"""
|
34
|
-
|
35
|
-
def __init__(
|
36
|
-
self,
|
37
|
-
name: str,
|
38
|
-
params: ConfParams,
|
39
|
-
externals: DictData | None = None,
|
40
|
-
) -> None:
|
41
|
-
self.data: DictData = {}
|
42
|
-
for file in PathSearch(params.engine.paths.conf).files:
|
43
|
-
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
44
|
-
data := YamlFlResolve(file).read().get(name, {})
|
45
|
-
):
|
46
|
-
self.data = data
|
47
|
-
if not self.data:
|
48
|
-
raise ValueError(f"Config {name!r} does not found on conf path")
|
49
|
-
|
50
|
-
# TODO: Validate the version of template data that mean if version of
|
51
|
-
# Template were change it should raise to upgrade package version.
|
52
|
-
# ---
|
53
|
-
# <pipeline-name>:
|
54
|
-
# version: 1
|
55
|
-
# type: pipeline.Pipeline
|
56
|
-
#
|
57
|
-
self.conf_params: ConfParams = params
|
58
|
-
self.externals: DictData = externals or {}
|
59
|
-
self.data.update(self.externals)
|
60
|
-
|
61
|
-
@classmethod
|
62
|
-
def find(
|
63
|
-
cls,
|
64
|
-
obj: object,
|
65
|
-
params: ConfParams,
|
66
|
-
*,
|
67
|
-
include: list[str] | None = None,
|
68
|
-
exclude: list[str] | None = None,
|
69
|
-
) -> Iterator[tuple[str, DictData]]:
|
70
|
-
"""Find all object"""
|
71
|
-
exclude: list[str] = exclude or []
|
72
|
-
for file in PathSearch(params.engine.paths.conf).files:
|
73
|
-
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
74
|
-
values := YamlFlResolve(file).read()
|
75
|
-
):
|
76
|
-
for key, data in values.items():
|
77
|
-
if key in exclude:
|
78
|
-
continue
|
79
|
-
if (
|
80
|
-
(t := data.get("type"))
|
81
|
-
and issubclass(cls.get_type(t, params), obj)
|
82
|
-
and all(i in data for i in (include or data.keys()))
|
83
|
-
):
|
84
|
-
yield key, data
|
85
|
-
|
86
|
-
@classmethod
|
87
|
-
def get_type(cls, t: str, params: ConfParams) -> AnyModelType:
|
88
|
-
try:
|
89
|
-
# NOTE: Auto adding module prefix if it does not set
|
90
|
-
return import_string(f"ddeutil.workflow.{t}")
|
91
|
-
except ModuleNotFoundError:
|
92
|
-
for registry in params.engine.registry:
|
93
|
-
try:
|
94
|
-
return import_string(f"{registry}.{t}")
|
95
|
-
except ModuleNotFoundError:
|
96
|
-
continue
|
97
|
-
return import_string(f"{t}")
|
98
|
-
|
99
|
-
@cached_property
|
100
|
-
def type(self) -> AnyModelType:
|
101
|
-
"""Return object of string type which implement on any registry. The
|
102
|
-
object type
|
103
|
-
"""
|
104
|
-
if not (_typ := self.data.get("type")):
|
105
|
-
raise ValueError(
|
106
|
-
f"the 'type' value: {_typ} does not exists in config data."
|
107
|
-
)
|
108
|
-
return self.get_type(_typ, self.conf_params)
|
109
|
-
|
110
|
-
|
111
|
-
class Loader(SimLoad):
|
112
|
-
"""Loader Object that get the config `yaml` file from current path.
|
113
|
-
|
114
|
-
:param name: A name of config data that will read by Yaml Loader object.
|
115
|
-
:param externals: An external parameters
|
116
|
-
"""
|
117
|
-
|
118
|
-
@classmethod
|
119
|
-
def find(
|
120
|
-
cls,
|
121
|
-
obj,
|
122
|
-
*,
|
123
|
-
include: list[str] | None = None,
|
124
|
-
exclude: list[str] | None = None,
|
125
|
-
**kwargs,
|
126
|
-
) -> DictData:
|
127
|
-
return super().find(
|
128
|
-
obj=obj, params=config(), include=include, exclude=exclude
|
129
|
-
)
|
130
|
-
|
131
|
-
def __init__(self, name: str, externals: DictData) -> None:
|
132
|
-
super().__init__(name, config(), externals)
|