ddeutil-workflow 0.0.45__tar.gz → 0.0.47__tar.gz
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-0.0.45 → ddeutil_workflow-0.0.47}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/pyproject.toml +0 -1
- ddeutil_workflow-0.0.47/src/ddeutil/workflow/__about__.py +1 -0
- ddeutil_workflow-0.0.47/src/ddeutil/workflow/__main__.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/conf.py +6 -24
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/job.py +3 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/result.py +10 -15
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/reusables.py +4 -3
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/stages.py +80 -27
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/workflow.py +2 -2
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/SOURCES.txt +1 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_reusables_call_tag.py +92 -2
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_stage_handler_exec.py +19 -5
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec.py +52 -1
- ddeutil_workflow-0.0.45/src/ddeutil/workflow/__about__.py +0 -1
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/LICENSE +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/README.md +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__init__.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/api.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/log.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/repeat.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/logs.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/cron.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/exceptions.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/logs.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/scheduler.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/utils.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_cron_on.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_job.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_job_exec.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_job_strategy.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_logs_audit.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_logs_trace.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_release.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_release_queue.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_result.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_schedule.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_schedule_pending.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_schedule_tasks.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_schedule_workflow.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_scheduler_control.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_stage.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_poke.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_release.py +0 -0
- {ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/tests/test_workflow_task.py +0 -0
@@ -13,7 +13,6 @@ classifiers = [
|
|
13
13
|
"Topic :: Utilities",
|
14
14
|
"Natural Language :: English",
|
15
15
|
"Development Status :: 4 - Beta",
|
16
|
-
# "Development Status :: 5 - Production/Stable",
|
17
16
|
"Intended Audience :: Developers",
|
18
17
|
"Operating System :: OS Independent",
|
19
18
|
"Programming Language :: Python",
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.47"
|
File without changes
|
@@ -26,7 +26,10 @@ PREFIX: str = "WORKFLOW"
|
|
26
26
|
|
27
27
|
|
28
28
|
def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
29
|
-
"""Get environment variable with uppercase and adding prefix string.
|
29
|
+
"""Get environment variable with uppercase and adding prefix string.
|
30
|
+
|
31
|
+
:rtype: str | None
|
32
|
+
"""
|
30
33
|
return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
|
31
34
|
|
32
35
|
|
@@ -42,29 +45,7 @@ __all__: TupleStr = (
|
|
42
45
|
)
|
43
46
|
|
44
47
|
|
45
|
-
class
|
46
|
-
"""BaseConfig object inheritable."""
|
47
|
-
|
48
|
-
__slots__ = ()
|
49
|
-
|
50
|
-
@property
|
51
|
-
def root_path(self) -> Path:
|
52
|
-
"""Root path or the project path.
|
53
|
-
|
54
|
-
:rtype: Path
|
55
|
-
"""
|
56
|
-
return Path(os.getenv("ROOT_PATH", "."))
|
57
|
-
|
58
|
-
@property
|
59
|
-
def conf_path(self) -> Path:
|
60
|
-
"""Config path that use root_path class argument for this construction.
|
61
|
-
|
62
|
-
:rtype: Path
|
63
|
-
"""
|
64
|
-
return self.root_path / os.getenv("CONF_PATH", "conf")
|
65
|
-
|
66
|
-
|
67
|
-
class Config(BaseConfig): # pragma: no cov
|
48
|
+
class Config: # pragma: no cov
|
68
49
|
"""Config object for keeping core configurations on the current session
|
69
50
|
without changing when if the application still running.
|
70
51
|
|
@@ -217,6 +198,7 @@ class Config(BaseConfig): # pragma: no cov
|
|
217
198
|
|
218
199
|
|
219
200
|
class APIConfig:
|
201
|
+
"""API Config object."""
|
220
202
|
|
221
203
|
@property
|
222
204
|
def prefix_path(self) -> str:
|
@@ -641,6 +641,9 @@ def local_execute_strategy(
|
|
641
641
|
|
642
642
|
for stage in job.stages:
|
643
643
|
|
644
|
+
if job.extras:
|
645
|
+
stage.extras = job.extras
|
646
|
+
|
644
647
|
if stage.is_skipped(params=context):
|
645
648
|
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
646
649
|
stage.set_outputs(output={"skipped": True}, to=context)
|
@@ -18,19 +18,10 @@ from pydantic.dataclasses import dataclass
|
|
18
18
|
from pydantic.functional_validators import model_validator
|
19
19
|
from typing_extensions import Self
|
20
20
|
|
21
|
-
from .__types import DictData
|
21
|
+
from .__types import DictData
|
22
22
|
from .logs import TraceLog, get_dt_tznow, get_trace
|
23
23
|
from .utils import default_gen_id, gen_id
|
24
24
|
|
25
|
-
__all__: TupleStr = (
|
26
|
-
"SUCCESS",
|
27
|
-
"FAILED",
|
28
|
-
"WAIT",
|
29
|
-
"SKIP",
|
30
|
-
"Result",
|
31
|
-
"Status",
|
32
|
-
)
|
33
|
-
|
34
25
|
|
35
26
|
class Status(IntEnum):
|
36
27
|
"""Status Int Enum object that use for tracking execution status to the
|
@@ -62,6 +53,10 @@ class Result:
|
|
62
53
|
|
63
54
|
For comparison property, this result will use ``status``, ``context``,
|
64
55
|
and ``_run_id`` fields to comparing with other result instance.
|
56
|
+
|
57
|
+
Warning:
|
58
|
+
I use dataclass object instead of Pydantic model object because context
|
59
|
+
field that keep dict value change its ID when update new value to it.
|
65
60
|
"""
|
66
61
|
|
67
62
|
status: Status = field(default=WAIT)
|
@@ -87,11 +82,11 @@ class Result:
|
|
87
82
|
"""Create the Result object or set parent running id if passing Result
|
88
83
|
object.
|
89
84
|
|
90
|
-
:param result:
|
91
|
-
:param run_id:
|
92
|
-
:param parent_run_id:
|
93
|
-
:param id_logic:
|
94
|
-
:param extras:
|
85
|
+
:param result: A Result instance.
|
86
|
+
:param run_id: A running ID.
|
87
|
+
:param parent_run_id: A parent running ID.
|
88
|
+
:param id_logic: A logic function that use to generate a running ID.
|
89
|
+
:param extras: An extra parameter that want to override the core config.
|
95
90
|
|
96
91
|
:rtype: Self
|
97
92
|
"""
|
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|
10
10
|
import inspect
|
11
11
|
import logging
|
12
12
|
from ast import Call, Constant, Expr, Module, Name, parse
|
13
|
-
from dataclasses import dataclass
|
14
13
|
from datetime import datetime
|
15
14
|
from functools import wraps
|
16
15
|
from importlib import import_module
|
@@ -23,6 +22,7 @@ except ImportError:
|
|
23
22
|
|
24
23
|
from ddeutil.core import getdot, import_string, lazy
|
25
24
|
from ddeutil.io import search_env_replace
|
25
|
+
from pydantic.dataclasses import dataclass
|
26
26
|
|
27
27
|
from .__types import DictData, Re
|
28
28
|
from .conf import dynamic
|
@@ -437,6 +437,7 @@ Registry = dict[str, Callable[[], TagFunc]]
|
|
437
437
|
|
438
438
|
def make_registry(
|
439
439
|
submodule: str,
|
440
|
+
*,
|
440
441
|
registries: Optional[list[str]] = None,
|
441
442
|
) -> dict[str, Registry]:
|
442
443
|
"""Return registries of all functions that able to called with task.
|
@@ -539,13 +540,13 @@ def extract_call(
|
|
539
540
|
|
540
541
|
if call.func not in rgt:
|
541
542
|
raise NotImplementedError(
|
542
|
-
f"`
|
543
|
+
f"`REGISTERS.{call.path}.registries` not implement "
|
543
544
|
f"registry: {call.func!r}."
|
544
545
|
)
|
545
546
|
|
546
547
|
if call.tag not in rgt[call.func]:
|
547
548
|
raise NotImplementedError(
|
548
549
|
f"tag: {call.tag!r} not found on registry func: "
|
549
|
-
f"`REGISTER
|
550
|
+
f"`REGISTER.{call.path}.registries.{call.func}`"
|
550
551
|
)
|
551
552
|
return rgt[call.func][call.tag]
|
@@ -38,12 +38,13 @@ from concurrent.futures import (
|
|
38
38
|
ThreadPoolExecutor,
|
39
39
|
as_completed,
|
40
40
|
)
|
41
|
+
from dataclasses import is_dataclass
|
41
42
|
from inspect import Parameter
|
42
43
|
from pathlib import Path
|
43
44
|
from subprocess import CompletedProcess
|
44
45
|
from textwrap import dedent
|
45
46
|
from threading import Event
|
46
|
-
from typing import Annotated, Optional, Union
|
47
|
+
from typing import Annotated, Any, Optional, Union, get_type_hints
|
47
48
|
|
48
49
|
from pydantic import BaseModel, Field
|
49
50
|
from pydantic.functional_validators import model_validator
|
@@ -814,7 +815,8 @@ class CallStage(BaseStage):
|
|
814
815
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
815
816
|
)
|
816
817
|
|
817
|
-
|
818
|
+
has_keyword: bool = False
|
819
|
+
call_func: TagFunc = extract_call(
|
818
820
|
param2template(self.uses, params, extras=self.extras),
|
819
821
|
registries=self.extras.get("regis_call"),
|
820
822
|
)()
|
@@ -824,55 +826,92 @@ class CallStage(BaseStage):
|
|
824
826
|
args: DictData = {"result": result} | param2template(
|
825
827
|
self.args, params, extras=self.extras
|
826
828
|
)
|
827
|
-
ips = inspect.signature(
|
828
|
-
necessary_params: list[str] = [
|
829
|
-
|
830
|
-
for k in ips.parameters
|
829
|
+
ips = inspect.signature(call_func)
|
830
|
+
necessary_params: list[str] = []
|
831
|
+
for k in ips.parameters:
|
831
832
|
if (
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
833
|
+
v := ips.parameters[k]
|
834
|
+
).default == Parameter.empty and v.kind not in (
|
835
|
+
Parameter.VAR_KEYWORD,
|
836
|
+
Parameter.VAR_POSITIONAL,
|
837
|
+
):
|
838
|
+
necessary_params.append(k)
|
839
|
+
elif v.kind == Parameter.VAR_KEYWORD:
|
840
|
+
has_keyword = True
|
841
|
+
|
839
842
|
if any(
|
840
843
|
(k.removeprefix("_") not in args and k not in args)
|
841
844
|
for k in necessary_params
|
842
845
|
):
|
843
846
|
raise ValueError(
|
844
847
|
f"Necessary params, ({', '.join(necessary_params)}, ), "
|
845
|
-
f"does not set to args"
|
848
|
+
f"does not set to args, {list(args.keys())}."
|
846
849
|
)
|
847
850
|
|
848
|
-
|
849
|
-
for k in ips.parameters:
|
850
|
-
if k.removeprefix("_") in args:
|
851
|
-
args[k] = args.pop(k.removeprefix("_"))
|
852
|
-
|
853
|
-
if "result" not in ips.parameters:
|
851
|
+
if "result" not in ips.parameters and not has_keyword:
|
854
852
|
args.pop("result")
|
855
853
|
|
856
|
-
result.trace.info(
|
857
|
-
|
854
|
+
result.trace.info(
|
855
|
+
f"[STAGE]: Call-Execute: {call_func.name}@{call_func.tag}"
|
856
|
+
)
|
857
|
+
|
858
|
+
args = self.parse_model_args(call_func, args, result)
|
859
|
+
if inspect.iscoroutinefunction(call_func):
|
858
860
|
loop = asyncio.get_event_loop()
|
859
861
|
rs: DictData = loop.run_until_complete(
|
860
|
-
|
862
|
+
call_func(**param2template(args, params, extras=self.extras))
|
861
863
|
)
|
862
864
|
else:
|
863
|
-
rs: DictData =
|
865
|
+
rs: DictData = call_func(
|
864
866
|
**param2template(args, params, extras=self.extras)
|
865
867
|
)
|
866
868
|
|
867
869
|
# VALIDATE:
|
868
870
|
# Check the result type from call function, it should be dict.
|
869
|
-
if
|
871
|
+
if isinstance(rs, BaseModel):
|
872
|
+
rs: DictData = rs.model_dump()
|
873
|
+
elif not isinstance(rs, dict):
|
870
874
|
raise TypeError(
|
871
|
-
f"Return type: '{
|
872
|
-
f"to result model, you change return type to `dict`."
|
875
|
+
f"Return type: '{call_func.name}@{call_func.tag}' does not "
|
876
|
+
f"serialize to result model, you change return type to `dict`."
|
873
877
|
)
|
874
878
|
return result.catch(status=SUCCESS, context=rs)
|
875
879
|
|
880
|
+
@staticmethod
|
881
|
+
def parse_model_args(
|
882
|
+
func: TagFunc,
|
883
|
+
args: DictData,
|
884
|
+
result: Result,
|
885
|
+
) -> DictData:
|
886
|
+
"""Parse Pydantic model from any dict data before parsing to target
|
887
|
+
caller function.
|
888
|
+
"""
|
889
|
+
try:
|
890
|
+
type_hints: dict[str, Any] = get_type_hints(func)
|
891
|
+
except TypeError as e:
|
892
|
+
result.trace.warning(
|
893
|
+
f"[STAGE]: Get type hint raise TypeError: {e}, so, it skip "
|
894
|
+
f"parsing model args process."
|
895
|
+
)
|
896
|
+
return args
|
897
|
+
|
898
|
+
for arg in type_hints:
|
899
|
+
|
900
|
+
if arg == "return":
|
901
|
+
continue
|
902
|
+
|
903
|
+
if arg.removeprefix("_") in args:
|
904
|
+
args[arg] = args.pop(arg.removeprefix("_"))
|
905
|
+
|
906
|
+
t: Any = type_hints[arg]
|
907
|
+
if is_dataclass(t) and t.__name__ == "Result" and arg not in args:
|
908
|
+
args[arg] = result
|
909
|
+
|
910
|
+
if issubclass(t, BaseModel) and arg in args:
|
911
|
+
args[arg] = t.model_validate(obj=args[arg])
|
912
|
+
|
913
|
+
return args
|
914
|
+
|
876
915
|
|
877
916
|
class TriggerStage(BaseStage):
|
878
917
|
"""Trigger Workflow execution stage that execute another workflow. This
|
@@ -976,6 +1015,8 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
976
1015
|
params: DictData,
|
977
1016
|
result: Result,
|
978
1017
|
stages: list[Stage],
|
1018
|
+
*,
|
1019
|
+
extras: DictData | None = None,
|
979
1020
|
) -> DictData:
|
980
1021
|
"""Task execution method for passing a branch to each thread.
|
981
1022
|
|
@@ -984,12 +1025,16 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
984
1025
|
:param result: (Result) A result object for keeping context and status
|
985
1026
|
data.
|
986
1027
|
:param stages:
|
1028
|
+
:param extras
|
987
1029
|
|
988
1030
|
:rtype: DictData
|
989
1031
|
"""
|
990
1032
|
context = {"branch": branch, "stages": {}}
|
991
1033
|
result.trace.debug(f"[STAGE]: Execute parallel branch: {branch!r}")
|
992
1034
|
for stage in stages:
|
1035
|
+
if extras:
|
1036
|
+
stage.extras = extras
|
1037
|
+
|
993
1038
|
try:
|
994
1039
|
stage.set_outputs(
|
995
1040
|
stage.handler_execute(
|
@@ -1048,6 +1093,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1048
1093
|
params=params,
|
1049
1094
|
result=result,
|
1050
1095
|
stages=self.parallel[branch],
|
1096
|
+
extras=self.extras,
|
1051
1097
|
)
|
1052
1098
|
)
|
1053
1099
|
|
@@ -1144,6 +1190,10 @@ class ForEachStage(BaseStage):
|
|
1144
1190
|
context = {"stages": {}}
|
1145
1191
|
|
1146
1192
|
for stage in self.stages:
|
1193
|
+
|
1194
|
+
if self.extras:
|
1195
|
+
stage.extras = self.extras
|
1196
|
+
|
1147
1197
|
try:
|
1148
1198
|
stage.set_outputs(
|
1149
1199
|
stage.handler_execute(
|
@@ -1284,6 +1334,9 @@ class CaseStage(BaseStage): # pragma: no cov
|
|
1284
1334
|
|
1285
1335
|
if match == _condition:
|
1286
1336
|
stage: Stage = match.stage
|
1337
|
+
if self.extras:
|
1338
|
+
stage.extras = self.extras
|
1339
|
+
|
1287
1340
|
try:
|
1288
1341
|
stage.set_outputs(
|
1289
1342
|
stage.handler_execute(
|
@@ -1139,7 +1139,7 @@ class Workflow(BaseModel):
|
|
1139
1139
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
1140
1140
|
):
|
1141
1141
|
job_id: str = job_queue.get()
|
1142
|
-
job: Job = self.
|
1142
|
+
job: Job = self.job(name=job_id)
|
1143
1143
|
|
1144
1144
|
if (check := job.check_needs(context["jobs"])) == WAIT:
|
1145
1145
|
job_queue.task_done()
|
@@ -1231,7 +1231,7 @@ class Workflow(BaseModel):
|
|
1231
1231
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
1232
1232
|
):
|
1233
1233
|
job_id: str = job_queue.get()
|
1234
|
-
job: Job = self.
|
1234
|
+
job: Job = self.job(name=job_id)
|
1235
1235
|
|
1236
1236
|
if (check := job.check_needs(context["jobs"])) == WAIT:
|
1237
1237
|
job_queue.task_done()
|
@@ -3,11 +3,14 @@ from __future__ import annotations
|
|
3
3
|
import asyncio
|
4
4
|
import inspect
|
5
5
|
import shutil
|
6
|
+
import typing
|
7
|
+
from dataclasses import is_dataclass
|
6
8
|
from pathlib import Path
|
7
9
|
from textwrap import dedent
|
8
10
|
|
9
11
|
import pytest
|
10
|
-
from ddeutil.workflow.reusables import Registry, make_registry
|
12
|
+
from ddeutil.workflow.reusables import Registry, extract_call, make_registry
|
13
|
+
from pydantic import BaseModel
|
11
14
|
|
12
15
|
|
13
16
|
@pytest.fixture(scope="module")
|
@@ -94,10 +97,10 @@ def test_make_registry(call_function):
|
|
94
97
|
|
95
98
|
def test_make_registry_from_env():
|
96
99
|
rs: dict[str, Registry] = make_registry("tasks")
|
97
|
-
print(rs)
|
98
100
|
assert set(rs.keys()) == {
|
99
101
|
"async-el-csv-to-parquet",
|
100
102
|
"get-items",
|
103
|
+
"gen-type",
|
101
104
|
"mssql-proc",
|
102
105
|
"el-csv-to-parquet",
|
103
106
|
"return-type-not-valid",
|
@@ -116,12 +119,36 @@ def test_make_registry_raise(call_function_dup):
|
|
116
119
|
make_registry("new_tasks_dup")
|
117
120
|
|
118
121
|
|
122
|
+
def test_extract_call():
|
123
|
+
func = extract_call("tasks/el-csv-to-parquet@polars-dir")
|
124
|
+
call_func = func()
|
125
|
+
assert call_func.name == "el-csv-to-parquet"
|
126
|
+
assert call_func.tag == "polars-dir"
|
127
|
+
|
128
|
+
|
129
|
+
def test_extract_call_args_type():
|
130
|
+
func = extract_call("tasks/gen-type@demo")
|
131
|
+
call_func = func()
|
132
|
+
|
133
|
+
get_types = typing.get_type_hints(call_func)
|
134
|
+
for p in get_types:
|
135
|
+
if is_dataclass(get_types[p]) and get_types[p].__name__ == "Result":
|
136
|
+
print("found result", p, get_types[p])
|
137
|
+
if issubclass(get_types[p], BaseModel):
|
138
|
+
print(p, "with type:", get_types[p])
|
139
|
+
|
140
|
+
|
119
141
|
@pytest.mark.skip("Skip because it uses for local test only.")
|
120
142
|
def test_inspec_func():
|
121
143
|
|
122
144
|
def demo_func(
|
123
145
|
args_1: str, args_2: Path, *args, kwargs_1: str | None = None, **kwargs
|
124
146
|
): # pragma: no cov
|
147
|
+
_ = args_1
|
148
|
+
_ = args_2
|
149
|
+
_ = args
|
150
|
+
_ = kwargs_1
|
151
|
+
_ = kwargs
|
125
152
|
pass
|
126
153
|
|
127
154
|
assert inspect.isfunction(demo_func)
|
@@ -141,6 +168,11 @@ def test_inspec_func():
|
|
141
168
|
args_1: str, args_2: Path, *args, kwargs_1: str | None = None, **kwargs
|
142
169
|
): # pragma: no cov
|
143
170
|
await asyncio.sleep(0.1)
|
171
|
+
_ = args_1
|
172
|
+
_ = args_2
|
173
|
+
_ = args
|
174
|
+
_ = kwargs_1
|
175
|
+
_ = kwargs
|
144
176
|
pass
|
145
177
|
|
146
178
|
print(inspect.isfunction(ademo_func))
|
@@ -158,3 +190,61 @@ def test_inspec_func():
|
|
158
190
|
# print(v.default)
|
159
191
|
# print(v.kind, " (", type(v.kind), ")")
|
160
192
|
# print("-----")
|
193
|
+
|
194
|
+
|
195
|
+
class MockModel(BaseModel): # pragma: no cov
|
196
|
+
name: str
|
197
|
+
|
198
|
+
|
199
|
+
def outside_func(args: MockModel) -> MockModel: # pragma: no cov
|
200
|
+
_ = args
|
201
|
+
pass
|
202
|
+
|
203
|
+
|
204
|
+
@pytest.mark.skip("Skip because it uses for local test only.")
|
205
|
+
def test_inspec_with_pydantic_model_args():
|
206
|
+
from pydantic import BaseModel
|
207
|
+
|
208
|
+
class MockModelLocal(BaseModel):
|
209
|
+
name: str
|
210
|
+
|
211
|
+
def demo_func(
|
212
|
+
args_1: MockModel,
|
213
|
+
args_2: MockModelLocal,
|
214
|
+
*args,
|
215
|
+
kwargs_1: str = None,
|
216
|
+
**kwargs,
|
217
|
+
) -> MockModel: # pragma: no cov
|
218
|
+
_ = args_1
|
219
|
+
_ = args_2
|
220
|
+
_ = args
|
221
|
+
_ = kwargs_1
|
222
|
+
_ = kwargs
|
223
|
+
pass
|
224
|
+
|
225
|
+
# ips = inspect.signature(demo_func)
|
226
|
+
# for k, v in ips.parameters.items():
|
227
|
+
# print(k)
|
228
|
+
# print(ips.parameters[k].default)
|
229
|
+
# print(v)
|
230
|
+
# print(v.name)
|
231
|
+
# print(v.annotation, "type:", type(v.annotation))
|
232
|
+
# print(v.default)
|
233
|
+
# print(v.kind, " (", type(v.kind), ")")
|
234
|
+
# print("-----")
|
235
|
+
#
|
236
|
+
# print(ips.return_annotation, "type:", type(ips.return_annotation))
|
237
|
+
# print(ips.return_annotation is MockModel)
|
238
|
+
# print(ips.return_annotation.__parameter__)
|
239
|
+
|
240
|
+
import typing
|
241
|
+
|
242
|
+
rs = typing.get_type_hints(demo_func, localns=locals(), globalns=globals())
|
243
|
+
print(rs)
|
244
|
+
|
245
|
+
print(demo_func.__annotations__)
|
246
|
+
print(globals()["MockModel"])
|
247
|
+
print(locals()["MockModelLocal"])
|
248
|
+
|
249
|
+
rs = typing.get_type_hints(outside_func)
|
250
|
+
print(rs)
|
@@ -97,20 +97,29 @@ def test_stage_exec_call(test_path):
|
|
97
97
|
with:
|
98
98
|
source: src
|
99
99
|
sink: sink
|
100
|
+
- name: "Return with Pydantic Model"
|
101
|
+
id: return-model
|
102
|
+
uses: tasks/gen-type@demo
|
103
|
+
with:
|
104
|
+
args1: foo
|
105
|
+
args2: conf/path
|
106
|
+
args3:
|
107
|
+
name: test
|
108
|
+
data:
|
109
|
+
input: hello
|
100
110
|
""",
|
101
111
|
):
|
102
112
|
workflow = Workflow.from_conf(name="tmp-wf-call-return-type")
|
103
113
|
|
104
114
|
stage: Stage = workflow.job("second-job").stage("extract-load")
|
105
115
|
rs: Result = stage.handler_execute({})
|
106
|
-
|
116
|
+
assert 0 == rs.status
|
117
|
+
assert {"records": 1} == rs.context
|
107
118
|
|
108
119
|
stage: Stage = workflow.job("second-job").stage("async-extract-load")
|
109
120
|
rs: Result = stage.handler_execute({})
|
110
|
-
|
111
|
-
|
112
|
-
assert 0 == rs.status
|
113
|
-
assert {"records": 1} == rs.context
|
121
|
+
assert rs.status == 0
|
122
|
+
assert rs.context == {"records": 1}
|
114
123
|
|
115
124
|
# NOTE: Raise because invalid return type.
|
116
125
|
with pytest.raises(StageException):
|
@@ -132,6 +141,11 @@ def test_stage_exec_call(test_path):
|
|
132
141
|
stage: Stage = workflow.job("first-job").stage("call-not-register")
|
133
142
|
stage.handler_execute({})
|
134
143
|
|
144
|
+
stage: Stage = workflow.job("second-job").stage("return-model")
|
145
|
+
rs: Result = stage.handler_execute({})
|
146
|
+
assert rs.status == 0
|
147
|
+
assert rs.context == {"name": "foo", "data": {"key": "value"}}
|
148
|
+
|
135
149
|
|
136
150
|
@mock.patch.object(Config, "stage_raise_error", True)
|
137
151
|
def test_stage_exec_py_raise():
|
@@ -1,8 +1,10 @@
|
|
1
|
+
import shutil
|
1
2
|
from datetime import datetime
|
3
|
+
from textwrap import dedent
|
2
4
|
from unittest import mock
|
3
5
|
|
4
6
|
from ddeutil.core import getdot
|
5
|
-
from ddeutil.workflow import SUCCESS, Workflow
|
7
|
+
from ddeutil.workflow import SUCCESS, Workflow, extract_call
|
6
8
|
from ddeutil.workflow.conf import Config
|
7
9
|
from ddeutil.workflow.job import Job
|
8
10
|
from ddeutil.workflow.result import FAILED, Result
|
@@ -488,6 +490,55 @@ def test_workflow_exec_call(test_path):
|
|
488
490
|
} == rs.context
|
489
491
|
|
490
492
|
|
493
|
+
def test_workflow_exec_call_override_registry(test_path):
|
494
|
+
task_path = test_path.parent / "mock_tests"
|
495
|
+
task_path.mkdir(exist_ok=True)
|
496
|
+
(task_path / "__init__.py").open(mode="w")
|
497
|
+
(task_path / "mock_tasks").mkdir(exist_ok=True)
|
498
|
+
|
499
|
+
with (task_path / "mock_tasks/__init__.py").open(mode="w") as f:
|
500
|
+
f.write(
|
501
|
+
dedent(
|
502
|
+
"""
|
503
|
+
from ddeutil.workflow import tag, Result
|
504
|
+
|
505
|
+
@tag("v1", alias="get-info")
|
506
|
+
def get_info(result: Result):
|
507
|
+
result.trace.info("... [CALLER]: Info from mock tasks")
|
508
|
+
return {"get-info": "success"}
|
509
|
+
|
510
|
+
""".strip(
|
511
|
+
"\n"
|
512
|
+
)
|
513
|
+
)
|
514
|
+
)
|
515
|
+
|
516
|
+
with dump_yaml_context(
|
517
|
+
test_path / "conf/demo/01_99_wf_test_wf_exec_call_override.yml",
|
518
|
+
data="""
|
519
|
+
tmp-wf-exec-call-override:
|
520
|
+
type: Workflow
|
521
|
+
jobs:
|
522
|
+
first-job:
|
523
|
+
stages:
|
524
|
+
- name: "Call from mock tasks"
|
525
|
+
uses: mock_tasks/get-info@v1
|
526
|
+
""",
|
527
|
+
):
|
528
|
+
func = extract_call("mock_tasks/get-info@v1", registries=["mock_tests"])
|
529
|
+
assert func().name == "get-info"
|
530
|
+
|
531
|
+
workflow = Workflow.from_conf(
|
532
|
+
name="tmp-wf-exec-call-override",
|
533
|
+
extras={"regis_call": ["mock_tests"]},
|
534
|
+
)
|
535
|
+
rs = workflow.execute(params={})
|
536
|
+
assert rs.status == SUCCESS
|
537
|
+
print(rs.context)
|
538
|
+
|
539
|
+
shutil.rmtree(task_path)
|
540
|
+
|
541
|
+
|
491
542
|
def test_workflow_exec_call_with_prefix(test_path):
|
492
543
|
with dump_yaml_context(
|
493
544
|
test_path / "conf/demo/01_99_wf_test_wf_call_mssql_proc.yml",
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__: str = "0.0.45"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/schedules.py
RENAMED
File without changes
|
{ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/workflows.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/requires.txt
RENAMED
File without changes
|
{ddeutil_workflow-0.0.45 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|