ddeutil-workflow 0.0.7__py3-none-any.whl → 0.0.8__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/app.py +4 -0
- ddeutil/workflow/exceptions.py +1 -4
- ddeutil/workflow/log.py +49 -0
- ddeutil/workflow/pipeline.py +327 -167
- ddeutil/workflow/stage.py +191 -97
- ddeutil/workflow/utils.py +94 -16
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/METADATA +17 -92
- ddeutil_workflow-0.0.8.dist-info/RECORD +20 -0
- ddeutil_workflow-0.0.7.dist-info/RECORD +0 -20
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stage.py
CHANGED
@@ -3,6 +3,18 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
"""Stage Model that use for getting stage data template from Job Model.
|
7
|
+
The stage that handle the minimize task that run in some thread (same thread at
|
8
|
+
its job owner) that mean it is the lowest executor of a pipeline workflow that
|
9
|
+
can tracking logs.
|
10
|
+
|
11
|
+
The output of stage execution only return 0 status because I do not want to
|
12
|
+
handle stage error on this stage model. I think stage model should have a lot of
|
13
|
+
usecase and it does not worry when I want to create a new one.
|
14
|
+
|
15
|
+
Execution --> Ok --> Result with 0
|
16
|
+
--> Error --> Raise StageException
|
17
|
+
"""
|
6
18
|
from __future__ import annotations
|
7
19
|
|
8
20
|
import contextlib
|
@@ -15,6 +27,7 @@ import uuid
|
|
15
27
|
from abc import ABC, abstractmethod
|
16
28
|
from collections.abc import Iterator
|
17
29
|
from dataclasses import dataclass
|
30
|
+
from functools import wraps
|
18
31
|
from inspect import Parameter
|
19
32
|
from pathlib import Path
|
20
33
|
from subprocess import CompletedProcess
|
@@ -22,6 +35,7 @@ from typing import Callable, Optional, Union
|
|
22
35
|
|
23
36
|
from ddeutil.core import str2bool
|
24
37
|
from pydantic import BaseModel, Field
|
38
|
+
from pydantic.functional_validators import model_validator
|
25
39
|
|
26
40
|
from .__types import DictData, DictStr, Re, TupleStr
|
27
41
|
from .exceptions import StageException
|
@@ -36,6 +50,35 @@ from .utils import (
|
|
36
50
|
)
|
37
51
|
|
38
52
|
|
53
|
+
def handler_result(message: str | None = None):
|
54
|
+
"""Decorator function for handler result from the stage execution."""
|
55
|
+
message: str = message or ""
|
56
|
+
|
57
|
+
def decorator(func):
|
58
|
+
|
59
|
+
@wraps(func)
|
60
|
+
def wrapped(self: BaseStage, *args, **kwargs):
|
61
|
+
try:
|
62
|
+
rs: DictData = func(self, *args, **kwargs)
|
63
|
+
return Result(status=0, context=rs)
|
64
|
+
except Exception as err:
|
65
|
+
logging.error(
|
66
|
+
f"({self.run_id}) [STAGE]: {err.__class__.__name__}: {err}"
|
67
|
+
)
|
68
|
+
if isinstance(err, StageException):
|
69
|
+
raise StageException(
|
70
|
+
f"{self.__class__.__name__}: {message}\n---\n\t{err}"
|
71
|
+
) from err
|
72
|
+
raise StageException(
|
73
|
+
f"{self.__class__.__name__}: {message}\n---\n\t"
|
74
|
+
f"{err.__class__.__name__}: {err}"
|
75
|
+
) from None
|
76
|
+
|
77
|
+
return wrapped
|
78
|
+
|
79
|
+
return decorator
|
80
|
+
|
81
|
+
|
39
82
|
class BaseStage(BaseModel, ABC):
|
40
83
|
"""Base Stage Model that keep only id and name fields for the stage
|
41
84
|
metadata. If you want to implement any custom stage, you can use this class
|
@@ -56,6 +99,17 @@ class BaseStage(BaseModel, ABC):
|
|
56
99
|
default=None,
|
57
100
|
alias="if",
|
58
101
|
)
|
102
|
+
run_id: Optional[str] = Field(
|
103
|
+
default=None,
|
104
|
+
description="A running stage ID.",
|
105
|
+
repr=False,
|
106
|
+
)
|
107
|
+
|
108
|
+
@model_validator(mode="after")
|
109
|
+
def __prepare_running_id(self):
|
110
|
+
if self.run_id is None:
|
111
|
+
self.run_id = gen_id(self.name + (self.id or ""), unique=True)
|
112
|
+
return self
|
59
113
|
|
60
114
|
@abstractmethod
|
61
115
|
def execute(self, params: DictData) -> Result:
|
@@ -74,24 +128,40 @@ class BaseStage(BaseModel, ABC):
|
|
74
128
|
:param params: A context data that want to add output result.
|
75
129
|
:rtype: DictData
|
76
130
|
"""
|
77
|
-
if
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
131
|
+
if not (
|
132
|
+
self.id
|
133
|
+
or str2bool(os.getenv("WORKFLOW_CORE_DEFAULT_STAGE_ID", "false"))
|
134
|
+
):
|
135
|
+
logging.debug(
|
136
|
+
f"({self.run_id}) [STAGE]: Output does not set because this "
|
137
|
+
f"stage does not set ID or default stage ID config flag not be "
|
138
|
+
f"True."
|
139
|
+
)
|
82
140
|
return params
|
83
141
|
|
84
142
|
# NOTE: Create stages key to receive an output from the stage execution.
|
85
143
|
if "stages" not in params:
|
86
144
|
params["stages"] = {}
|
87
145
|
|
146
|
+
# TODO: Validate stage id and name should not dynamic with params
|
147
|
+
# template. (allow only matrix)
|
148
|
+
if self.id:
|
149
|
+
_id: str = param2template(self.id, params=params)
|
150
|
+
else:
|
151
|
+
_id: str = gen_id(param2template(self.name, params=params))
|
152
|
+
|
153
|
+
# NOTE: Set the output to that stage generated ID.
|
88
154
|
params["stages"][_id] = {"outputs": output}
|
155
|
+
logging.debug(
|
156
|
+
f"({self.run_id}) [STAGE]: Set output complete with stage ID: {_id}"
|
157
|
+
)
|
89
158
|
return params
|
90
159
|
|
91
|
-
def
|
160
|
+
def is_skipped(self, params: DictData | None = None) -> bool:
|
92
161
|
"""Return true if condition of this stage do not correct.
|
93
162
|
|
94
163
|
:param params: A parameters that want to pass to condition template.
|
164
|
+
:rtype: bool
|
95
165
|
"""
|
96
166
|
params: DictData = params or {}
|
97
167
|
if self.condition is None:
|
@@ -104,7 +174,7 @@ class BaseStage(BaseModel, ABC):
|
|
104
174
|
raise TypeError("Return type of condition does not be boolean")
|
105
175
|
return not rs
|
106
176
|
except Exception as err:
|
107
|
-
logging.error(
|
177
|
+
logging.error(f"({self.run_id}) [STAGE]: {err}")
|
108
178
|
raise StageException(str(err)) from err
|
109
179
|
|
110
180
|
|
@@ -131,8 +201,10 @@ class EmptyStage(BaseStage):
|
|
131
201
|
:param params: A context data that want to add output result. But this
|
132
202
|
stage does not pass any output.
|
133
203
|
"""
|
134
|
-
|
135
|
-
|
204
|
+
logging.info(
|
205
|
+
f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
|
206
|
+
f"( {param2template(self.echo, params=params) or '...'} )"
|
207
|
+
)
|
136
208
|
return Result(status=0, context={})
|
137
209
|
|
138
210
|
|
@@ -183,12 +255,17 @@ class BashStage(BaseStage):
|
|
183
255
|
f.write(bash.replace("\r\n", "\n"))
|
184
256
|
|
185
257
|
make_exec(f"./{f_name}")
|
258
|
+
logging.debug(
|
259
|
+
f"({self.run_id}) [STAGE]: Start create `.sh` file and running a "
|
260
|
+
f"bash statement."
|
261
|
+
)
|
186
262
|
|
187
263
|
yield [f_shebang, f_name]
|
188
264
|
|
189
265
|
Path(f"./{f_name}").unlink()
|
190
266
|
|
191
|
-
|
267
|
+
@handler_result()
|
268
|
+
def execute(self, params: DictData) -> DictData:
|
192
269
|
"""Execute the Bash statement with the Python build-in ``subprocess``
|
193
270
|
package.
|
194
271
|
|
@@ -199,7 +276,7 @@ class BashStage(BaseStage):
|
|
199
276
|
with self.__prepare_bash(
|
200
277
|
bash=bash, env=param2template(self.env, params)
|
201
278
|
) as sh:
|
202
|
-
logging.info(f"[STAGE]: Shell-Execute: {sh}")
|
279
|
+
logging.info(f"({self.run_id}) [STAGE]: Shell-Execute: {sh}")
|
203
280
|
rs: CompletedProcess = subprocess.run(
|
204
281
|
sh,
|
205
282
|
shell=False,
|
@@ -212,21 +289,32 @@ class BashStage(BaseStage):
|
|
212
289
|
if "\\x00" in rs.stderr
|
213
290
|
else rs.stderr
|
214
291
|
)
|
215
|
-
logging.error(
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
"
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
292
|
+
logging.error(
|
293
|
+
f"({self.run_id}) [STAGE]: {err}\n\n```bash\n{bash}```"
|
294
|
+
)
|
295
|
+
raise StageException(
|
296
|
+
f"{err.__class__.__name__}: {err}\nRunning Statement:"
|
297
|
+
f"\n---\n```bash\n{bash}\n```"
|
298
|
+
)
|
299
|
+
return {
|
300
|
+
"return_code": rs.returncode,
|
301
|
+
"stdout": rs.stdout.rstrip("\n"),
|
302
|
+
"stderr": rs.stderr.rstrip("\n"),
|
303
|
+
}
|
225
304
|
|
226
305
|
|
227
306
|
class PyStage(BaseStage):
|
228
307
|
"""Python executor stage that running the Python statement that receive
|
229
308
|
globals nad additional variables.
|
309
|
+
|
310
|
+
Data Validate:
|
311
|
+
>>> stage = {
|
312
|
+
... "name": "Python stage execution",
|
313
|
+
... "run": 'print("Hello {x}")',
|
314
|
+
... "vars": {
|
315
|
+
... "x": "BAR",
|
316
|
+
... },
|
317
|
+
... }
|
230
318
|
"""
|
231
319
|
|
232
320
|
run: str = Field(
|
@@ -259,7 +347,8 @@ class PyStage(BaseStage):
|
|
259
347
|
params.update({k: _globals[k] for k in params if k in _globals})
|
260
348
|
return params
|
261
349
|
|
262
|
-
|
350
|
+
@handler_result()
|
351
|
+
def execute(self, params: DictData) -> DictData:
|
263
352
|
"""Execute the Python statement that pass all globals and input params
|
264
353
|
to globals argument on ``exec`` build-in function.
|
265
354
|
|
@@ -271,18 +360,10 @@ class PyStage(BaseStage):
|
|
271
360
|
globals() | params | param2template(self.vars, params)
|
272
361
|
)
|
273
362
|
_locals: DictData = {}
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
raise StageException(
|
279
|
-
f"{err.__class__.__name__}: {err}\nRunning Statement:\n---\n"
|
280
|
-
f"{self.run}"
|
281
|
-
) from None
|
282
|
-
return Result(
|
283
|
-
status=0,
|
284
|
-
context={"locals": _locals, "globals": _globals},
|
285
|
-
)
|
363
|
+
run: str = param2template(self.run, params)
|
364
|
+
logging.info(f"({self.run_id}) [STAGE]: Py-Execute: {uuid.uuid4()}")
|
365
|
+
exec(run, _globals, _locals)
|
366
|
+
return {"locals": _locals, "globals": _globals}
|
286
367
|
|
287
368
|
|
288
369
|
@dataclass
|
@@ -294,6 +375,34 @@ class HookSearch:
|
|
294
375
|
tag: str
|
295
376
|
|
296
377
|
|
378
|
+
def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
379
|
+
"""Extract Hook string value to hook function.
|
380
|
+
|
381
|
+
:param hook: A hook value that able to match with Task regex.
|
382
|
+
:rtype: Callable[[], TagFunc]
|
383
|
+
"""
|
384
|
+
if not (found := Re.RE_TASK_FMT.search(hook)):
|
385
|
+
raise ValueError("Task does not match with task format regex.")
|
386
|
+
|
387
|
+
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
388
|
+
hook: HookSearch = HookSearch(**found.groupdict())
|
389
|
+
|
390
|
+
# NOTE: Registry object should implement on this package only.
|
391
|
+
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
392
|
+
if hook.func not in rgt:
|
393
|
+
raise NotImplementedError(
|
394
|
+
f"``REGISTER-MODULES.{hook.path}.registries`` does not "
|
395
|
+
f"implement registry: {hook.func!r}."
|
396
|
+
)
|
397
|
+
|
398
|
+
if hook.tag not in rgt[hook.func]:
|
399
|
+
raise NotImplementedError(
|
400
|
+
f"tag: {hook.tag!r} does not found on registry func: "
|
401
|
+
f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
|
402
|
+
)
|
403
|
+
return rgt[hook.func][hook.tag]
|
404
|
+
|
405
|
+
|
297
406
|
class HookStage(BaseStage):
|
298
407
|
"""Hook executor that hook the Python function from registry with tag
|
299
408
|
decorator function in ``utils`` module and run it with input arguments.
|
@@ -314,48 +423,27 @@ class HookStage(BaseStage):
|
|
314
423
|
"""
|
315
424
|
|
316
425
|
uses: str = Field(
|
317
|
-
description="A pointer that want to load function from registry",
|
426
|
+
description="A pointer that want to load function from registry.",
|
427
|
+
)
|
428
|
+
args: DictData = Field(
|
429
|
+
description="An arguments that want to pass to the hook function.",
|
430
|
+
alias="with",
|
318
431
|
)
|
319
|
-
args: DictData = Field(alias="with")
|
320
|
-
|
321
|
-
@staticmethod
|
322
|
-
def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
323
|
-
"""Extract Hook string value to hook function.
|
324
|
-
|
325
|
-
:param hook: A hook value that able to match with Task regex.
|
326
|
-
"""
|
327
|
-
if not (found := Re.RE_TASK_FMT.search(hook)):
|
328
|
-
raise ValueError("Task does not match with task format regex.")
|
329
|
-
|
330
|
-
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
331
|
-
hook: HookSearch = HookSearch(**found.groupdict())
|
332
|
-
|
333
|
-
# NOTE: Registry object should implement on this package only.
|
334
|
-
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
335
|
-
if hook.func not in rgt:
|
336
|
-
raise NotImplementedError(
|
337
|
-
f"``REGISTER-MODULES.{hook.path}.registries`` does not "
|
338
|
-
f"implement registry: {hook.func!r}."
|
339
|
-
)
|
340
|
-
|
341
|
-
if hook.tag not in rgt[hook.func]:
|
342
|
-
raise NotImplementedError(
|
343
|
-
f"tag: {hook.tag!r} does not found on registry func: "
|
344
|
-
f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
|
345
|
-
)
|
346
|
-
return rgt[hook.func][hook.tag]
|
347
432
|
|
348
|
-
|
433
|
+
@handler_result()
|
434
|
+
def execute(self, params: DictData) -> DictData:
|
349
435
|
"""Execute the Hook function that already in the hook registry.
|
350
436
|
|
351
437
|
:param params: A parameter that want to pass before run any statement.
|
352
438
|
:type params: DictData
|
353
439
|
:rtype: Result
|
354
440
|
"""
|
355
|
-
|
441
|
+
t_func_hook: str = param2template(self.uses, params)
|
442
|
+
t_func: TagFunc = extract_hook(t_func_hook)()
|
356
443
|
if not callable(t_func):
|
357
|
-
raise ImportError(
|
358
|
-
|
444
|
+
raise ImportError(
|
445
|
+
f"Hook caller {t_func_hook!r} function does not callable."
|
446
|
+
)
|
359
447
|
# VALIDATE: check input task caller parameters that exists before
|
360
448
|
# calling.
|
361
449
|
args: DictData = param2template(self.args, params)
|
@@ -369,56 +457,62 @@ class HookStage(BaseStage):
|
|
369
457
|
f"Necessary params, ({', '.join(ips.parameters.keys())}), "
|
370
458
|
f"does not set to args"
|
371
459
|
)
|
372
|
-
|
373
460
|
# NOTE: add '_' prefix if it want to use.
|
374
461
|
for k in ips.parameters:
|
375
462
|
if k.removeprefix("_") in args:
|
376
463
|
args[k] = args.pop(k.removeprefix("_"))
|
377
464
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
465
|
+
logging.info(
|
466
|
+
f"({self.run_id}) [STAGE]: Hook-Execute: "
|
467
|
+
f"{t_func.name}@{t_func.tag}"
|
468
|
+
)
|
469
|
+
rs: DictData = t_func(**param2template(args, params))
|
383
470
|
|
384
|
-
# VALIDATE:
|
471
|
+
# VALIDATE:
|
472
|
+
# Check the result type from hook function, it should be dict.
|
385
473
|
if not isinstance(rs, dict):
|
386
|
-
raise
|
387
|
-
f"Return of hook function: {t_func.name}@{t_func.tag} does
|
388
|
-
f"serialize to result model, you should fix it to
|
474
|
+
raise TypeError(
|
475
|
+
f"Return of hook function: {t_func.name}@{t_func.tag} does "
|
476
|
+
f"not serialize to result model, you should fix it to "
|
477
|
+
f"`dict` type."
|
389
478
|
)
|
390
|
-
return
|
479
|
+
return rs
|
391
480
|
|
392
481
|
|
393
482
|
class TriggerStage(BaseStage):
|
394
|
-
"""Trigger Pipeline execution stage that execute another pipeline object.
|
483
|
+
"""Trigger Pipeline execution stage that execute another pipeline object.
|
484
|
+
|
485
|
+
Data Validate:
|
486
|
+
>>> stage = {
|
487
|
+
... "name": "Trigger pipeline stage execution",
|
488
|
+
... "trigger": 'pipeline-name-for-loader',
|
489
|
+
... "params": {
|
490
|
+
... "run-date": "2024-08-01",
|
491
|
+
... "source": "src",
|
492
|
+
... },
|
493
|
+
... }
|
494
|
+
"""
|
395
495
|
|
396
496
|
trigger: str = Field(description="A trigger pipeline name.")
|
397
|
-
params: DictData = Field(
|
497
|
+
params: DictData = Field(
|
498
|
+
default_factory=dict,
|
499
|
+
description="A parameter that want to pass to pipeline execution.",
|
500
|
+
)
|
398
501
|
|
399
|
-
|
400
|
-
|
502
|
+
@handler_result("Raise from trigger pipeline")
|
503
|
+
def execute(self, params: DictData) -> DictData:
|
504
|
+
"""Trigger pipeline execution.
|
401
505
|
|
402
506
|
:param params: A parameter data that want to use in this execution.
|
403
507
|
:rtype: Result
|
404
508
|
"""
|
405
|
-
from .exceptions import PipelineException
|
406
509
|
from .pipeline import Pipeline
|
407
510
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
rs: Result = pipe.execute(
|
414
|
-
params=param2template(self.params, params)
|
415
|
-
)
|
416
|
-
except PipelineException as err:
|
417
|
-
_alias_stage: str = self.id or self.name
|
418
|
-
raise StageException(
|
419
|
-
f"Trigger Stage: {_alias_stage} get trigger pipeline exception."
|
420
|
-
) from err
|
421
|
-
return rs
|
511
|
+
# NOTE: Loading pipeline object from trigger name.
|
512
|
+
_trigger: str = param2template(self.trigger, params=params)
|
513
|
+
pipe: Pipeline = Pipeline.from_loader(name=_trigger, externals={})
|
514
|
+
rs: Result = pipe.execute(params=param2template(self.params, params))
|
515
|
+
return rs.context
|
422
516
|
|
423
517
|
|
424
518
|
# NOTE: Order of parsing stage data
|
ddeutil/workflow/utils.py
CHANGED
@@ -12,17 +12,17 @@ import stat
|
|
12
12
|
from abc import ABC, abstractmethod
|
13
13
|
from ast import Call, Constant, Expr, Module, Name, parse
|
14
14
|
from collections.abc import Iterator
|
15
|
-
from dataclasses import dataclass, field
|
16
15
|
from datetime import date, datetime
|
17
16
|
from functools import wraps
|
18
17
|
from hashlib import md5
|
19
18
|
from importlib import import_module
|
19
|
+
from inspect import isfunction
|
20
20
|
from itertools import product
|
21
21
|
from pathlib import Path
|
22
22
|
from typing import Any, Callable, Literal, Optional, Protocol, Union
|
23
23
|
from zoneinfo import ZoneInfo
|
24
24
|
|
25
|
-
from ddeutil.core import getdot, hasdot, import_string, lazy
|
25
|
+
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy, str2bool
|
26
26
|
from ddeutil.io import PathData, search_env_replace
|
27
27
|
from ddeutil.io.models.lineage import dt_now
|
28
28
|
from pydantic import BaseModel, ConfigDict, Field
|
@@ -47,10 +47,10 @@ class Engine(BaseModel):
|
|
47
47
|
|
48
48
|
paths: PathData = Field(default_factory=PathData)
|
49
49
|
registry: list[str] = Field(
|
50
|
-
default_factory=lambda: ["ddeutil.workflow"],
|
50
|
+
default_factory=lambda: ["ddeutil.workflow"], # pragma: no cover
|
51
51
|
)
|
52
52
|
registry_filter: list[str] = Field(
|
53
|
-
|
53
|
+
default_factory=lambda: ["ddeutil.workflow.utils"], # pragma: no cover
|
54
54
|
)
|
55
55
|
|
56
56
|
@model_validator(mode="before")
|
@@ -89,7 +89,15 @@ class ConfParams(BaseModel):
|
|
89
89
|
|
90
90
|
|
91
91
|
def config() -> ConfParams:
|
92
|
-
"""Load Config data from ``workflows-conf.yaml`` file.
|
92
|
+
"""Load Config data from ``workflows-conf.yaml`` file.
|
93
|
+
|
94
|
+
Configuration Docs:
|
95
|
+
---
|
96
|
+
:var engine.registry:
|
97
|
+
:var engine.registry_filter:
|
98
|
+
:var paths.root:
|
99
|
+
:var paths.conf:
|
100
|
+
"""
|
93
101
|
root_path: str = os.getenv("WORKFLOW_ROOT_PATH", ".")
|
94
102
|
|
95
103
|
regis: list[str] = ["ddeutil.workflow"]
|
@@ -119,19 +127,31 @@ def config() -> ConfParams:
|
|
119
127
|
)
|
120
128
|
|
121
129
|
|
122
|
-
def gen_id(
|
130
|
+
def gen_id(
|
131
|
+
value: Any,
|
132
|
+
*,
|
133
|
+
sensitive: bool = True,
|
134
|
+
unique: bool = False,
|
135
|
+
) -> str:
|
123
136
|
"""Generate running ID for able to tracking. This generate process use `md5`
|
124
|
-
function.
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
:param
|
137
|
+
algorithm function if ``WORKFLOW_CORE_PIPELINE_ID_SIMPLE`` set to false.
|
138
|
+
But it will cut this hashing value length to 10 it the setting value set to
|
139
|
+
true.
|
140
|
+
|
141
|
+
:param value: A value that want to add to prefix before hashing with md5.
|
142
|
+
:param sensitive: A flag that convert the value to lower case before hashing
|
143
|
+
:param unique: A flag that add timestamp at microsecond level to value
|
144
|
+
before hashing.
|
129
145
|
:rtype: str
|
130
146
|
"""
|
131
147
|
if not isinstance(value, str):
|
132
148
|
value: str = str(value)
|
133
149
|
|
134
150
|
tz: ZoneInfo = ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
|
151
|
+
if str2bool(os.getenv("WORKFLOW_CORE_PIPELINE_ID_SIMPLE", "true")):
|
152
|
+
return hash_str(f"{(value if sensitive else value.lower())}", n=10) + (
|
153
|
+
f"{datetime.now(tz=tz):%Y%m%d%H%M%S%f}" if unique else ""
|
154
|
+
)
|
135
155
|
return md5(
|
136
156
|
(
|
137
157
|
f"{(value if sensitive else value.lower())}"
|
@@ -328,9 +348,42 @@ Param = Union[
|
|
328
348
|
]
|
329
349
|
|
330
350
|
|
331
|
-
|
332
|
-
|
333
|
-
|
351
|
+
class Context(BaseModel):
|
352
|
+
"""Context Pydantic Model"""
|
353
|
+
|
354
|
+
params: dict = Field(default_factory=dict)
|
355
|
+
jobs: dict = Field(default_factory=dict)
|
356
|
+
error: dict = Field(default_factory=dict)
|
357
|
+
|
358
|
+
|
359
|
+
class Result(BaseModel):
|
360
|
+
"""Result Pydantic Model for passing parameter and receiving output from
|
361
|
+
the pipeline execution.
|
362
|
+
"""
|
363
|
+
|
364
|
+
# TODO: Add running ID to this result dataclass.
|
365
|
+
# ---
|
366
|
+
# parent_run_id: str
|
367
|
+
# run_id: str
|
368
|
+
#
|
369
|
+
status: int = Field(default=2)
|
370
|
+
context: DictData = Field(default_factory=dict)
|
371
|
+
|
372
|
+
def receive(self, result: Result) -> Result:
|
373
|
+
self.__dict__["status"] = result.status
|
374
|
+
self.__dict__["context"].update(result.context)
|
375
|
+
return self
|
376
|
+
|
377
|
+
def receive_jobs(self, result: Result) -> Result:
|
378
|
+
self.__dict__["status"] = result.status
|
379
|
+
if "jobs" not in self.__dict__["context"]:
|
380
|
+
self.__dict__["context"]["jobs"] = {}
|
381
|
+
self.__dict__["context"]["jobs"].update(result.context)
|
382
|
+
return self
|
383
|
+
|
384
|
+
|
385
|
+
class ReResult(BaseModel):
|
386
|
+
"""Result Pydantic Model for passing parameter and receiving output from
|
334
387
|
the pipeline execution.
|
335
388
|
"""
|
336
389
|
|
@@ -339,8 +392,14 @@ class Result:
|
|
339
392
|
# parent_run_id: str
|
340
393
|
# run_id: str
|
341
394
|
#
|
342
|
-
status: int =
|
343
|
-
context:
|
395
|
+
status: int = Field(default=2)
|
396
|
+
context: Context = Field(default_factory=Context)
|
397
|
+
|
398
|
+
def receive(self, result: ReResult) -> ReResult:
|
399
|
+
self.__dict__["status"] = result.status
|
400
|
+
self.__dict__["context"].__dict__["jobs"].update(result.context.jobs)
|
401
|
+
self.__dict__["context"].__dict__["error"].update(result.context.error)
|
402
|
+
return self
|
344
403
|
|
345
404
|
|
346
405
|
def make_exec(path: str | Path):
|
@@ -580,6 +639,25 @@ def param2template(
|
|
580
639
|
return str2template(value, params, filters=filters)
|
581
640
|
|
582
641
|
|
642
|
+
def filter_func(value: Any):
|
643
|
+
"""Filter own created function out of any value with replace it to its
|
644
|
+
function name. If it is built-in function, it does not have any changing.
|
645
|
+
"""
|
646
|
+
if isinstance(value, dict):
|
647
|
+
return {k: filter_func(value[k]) for k in value}
|
648
|
+
elif isinstance(value, (list, tuple, set)):
|
649
|
+
return type(value)([filter_func(i) for i in value])
|
650
|
+
|
651
|
+
if isfunction(value):
|
652
|
+
# NOTE: If it want to improve to get this function, it able to save to
|
653
|
+
# some global memory storage.
|
654
|
+
# ---
|
655
|
+
# >>> GLOBAL_DICT[value.__name__] = value
|
656
|
+
#
|
657
|
+
return value.__name__
|
658
|
+
return value
|
659
|
+
|
660
|
+
|
583
661
|
def dash2underscore(
|
584
662
|
key: str,
|
585
663
|
values: DictData,
|