ddeutil-workflow 0.0.46__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.
Files changed (68) hide show
  1. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/PKG-INFO +2 -2
  2. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/pyproject.toml +1 -1
  3. ddeutil_workflow-0.0.47/src/ddeutil/workflow/__about__.py +1 -0
  4. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/result.py +10 -15
  5. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/reusables.py +4 -3
  6. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/stages.py +66 -29
  7. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/PKG-INFO +2 -2
  8. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_reusables_call_tag.py +92 -2
  9. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_stage_handler_exec.py +19 -5
  10. ddeutil_workflow-0.0.46/src/ddeutil/workflow/__about__.py +0 -1
  11. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/LICENSE +0 -0
  12. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/README.md +0 -0
  13. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/setup.cfg +0 -0
  14. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__cron.py +0 -0
  15. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__init__.py +0 -0
  16. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__main__.py +0 -0
  17. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/__types.py +0 -0
  18. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/__init__.py +0 -0
  19. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/api.py +0 -0
  20. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/log.py +0 -0
  21. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/repeat.py +0 -0
  22. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  23. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/job.py +0 -0
  24. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  25. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  26. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  27. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/conf.py +0 -0
  28. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/cron.py +0 -0
  29. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/exceptions.py +0 -0
  30. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/job.py +0 -0
  31. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/logs.py +0 -0
  32. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/params.py +0 -0
  33. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/scheduler.py +0 -0
  34. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/utils.py +0 -0
  35. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil/workflow/workflow.py +0 -0
  36. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  37. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  38. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  39. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  40. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test__cron.py +0 -0
  41. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test__regex.py +0 -0
  42. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_conf.py +0 -0
  43. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_cron_on.py +0 -0
  44. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_job.py +0 -0
  45. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_job_exec.py +0 -0
  46. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_job_exec_strategy.py +0 -0
  47. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_job_strategy.py +0 -0
  48. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_logs_audit.py +0 -0
  49. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_logs_trace.py +0 -0
  50. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_params.py +0 -0
  51. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_release.py +0 -0
  52. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_release_queue.py +0 -0
  53. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_result.py +0 -0
  54. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_reusables_template.py +0 -0
  55. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_reusables_template_filter.py +0 -0
  56. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_schedule.py +0 -0
  57. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_schedule_pending.py +0 -0
  58. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_schedule_tasks.py +0 -0
  59. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_schedule_workflow.py +0 -0
  60. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_scheduler_control.py +0 -0
  61. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_stage.py +0 -0
  62. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_utils.py +0 -0
  63. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow.py +0 -0
  64. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec.py +0 -0
  65. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_poke.py +0 -0
  67. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow_exec_release.py +0 -0
  68. {ddeutil_workflow-0.0.46 → ddeutil_workflow-0.0.47}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.46
3
+ Version: 0.0.47
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -9,7 +9,7 @@ Project-URL: Source Code, https://github.com/ddeutils/ddeutil-workflow/
9
9
  Keywords: orchestration,workflow
10
10
  Classifier: Topic :: Utilities
11
11
  Classifier: Natural Language :: English
12
- Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
@@ -12,7 +12,7 @@ keywords = ['orchestration', 'workflow']
12
12
  classifiers = [
13
13
  "Topic :: Utilities",
14
14
  "Natural Language :: English",
15
- "Development Status :: 5 - Production/Stable",
15
+ "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "Operating System :: OS Independent",
18
18
  "Programming Language :: Python",
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.47"
@@ -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, TupleStr
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"`REGISTER-MODULES.{call.path}.registries` not implement "
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-MODULES.{call.path}.registries.{call.func}`"
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,9 +815,8 @@ class CallStage(BaseStage):
814
815
  run_id=gen_id(self.name + (self.id or ""), unique=True)
815
816
  )
816
817
 
817
- print("Extras in CallStage", self.extras)
818
-
819
- t_func: TagFunc = extract_call(
818
+ has_keyword: bool = False
819
+ call_func: TagFunc = extract_call(
820
820
  param2template(self.uses, params, extras=self.extras),
821
821
  registries=self.extras.get("regis_call"),
822
822
  )()
@@ -826,55 +826,92 @@ class CallStage(BaseStage):
826
826
  args: DictData = {"result": result} | param2template(
827
827
  self.args, params, extras=self.extras
828
828
  )
829
- ips = inspect.signature(t_func)
830
- necessary_params: list[str] = [
831
- k
832
- for k in ips.parameters
829
+ ips = inspect.signature(call_func)
830
+ necessary_params: list[str] = []
831
+ for k in ips.parameters:
833
832
  if (
834
- (v := ips.parameters[k]).default == Parameter.empty
835
- and (
836
- v.kind != Parameter.VAR_KEYWORD
837
- or v.kind != Parameter.VAR_POSITIONAL
838
- )
839
- )
840
- ]
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
+
841
842
  if any(
842
843
  (k.removeprefix("_") not in args and k not in args)
843
844
  for k in necessary_params
844
845
  ):
845
846
  raise ValueError(
846
847
  f"Necessary params, ({', '.join(necessary_params)}, ), "
847
- f"does not set to args"
848
+ f"does not set to args, {list(args.keys())}."
848
849
  )
849
850
 
850
- # NOTE: add '_' prefix if it wants to use.
851
- for k in ips.parameters:
852
- if k.removeprefix("_") in args:
853
- args[k] = args.pop(k.removeprefix("_"))
854
-
855
- if "result" not in ips.parameters:
851
+ if "result" not in ips.parameters and not has_keyword:
856
852
  args.pop("result")
857
853
 
858
- result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
859
- if inspect.iscoroutinefunction(t_func): # pragma: no cov
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):
860
860
  loop = asyncio.get_event_loop()
861
861
  rs: DictData = loop.run_until_complete(
862
- t_func(**param2template(args, params, extras=self.extras))
862
+ call_func(**param2template(args, params, extras=self.extras))
863
863
  )
864
864
  else:
865
- rs: DictData = t_func(
865
+ rs: DictData = call_func(
866
866
  **param2template(args, params, extras=self.extras)
867
867
  )
868
868
 
869
869
  # VALIDATE:
870
870
  # Check the result type from call function, it should be dict.
871
- if not isinstance(rs, dict):
871
+ if isinstance(rs, BaseModel):
872
+ rs: DictData = rs.model_dump()
873
+ elif not isinstance(rs, dict):
872
874
  raise TypeError(
873
- f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
874
- 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`."
875
877
  )
876
878
  return result.catch(status=SUCCESS, context=rs)
877
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
+
878
915
 
879
916
  class TriggerStage(BaseStage):
880
917
  """Trigger Workflow execution stage that execute another workflow. This
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.46
3
+ Version: 0.0.47
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -9,7 +9,7 @@ Project-URL: Source Code, https://github.com/ddeutils/ddeutil-workflow/
9
9
  Keywords: orchestration,workflow
10
10
  Classifier: Topic :: Utilities
11
11
  Classifier: Natural Language :: English
12
- Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
@@ -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
- print(rs)
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
- print(rs)
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 +0,0 @@
1
- __version__: str = "0.0.46"