ddeutil-workflow 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__: str = "0.0.1"
1
+ __version__: str = "0.0.3"
@@ -9,3 +9,4 @@ from typing import Any
9
9
 
10
10
  TupleStr = tuple[str, ...]
11
11
  DictData = dict[str, Any]
12
+ DictStr = dict[str, str]
ddeutil/workflow/conn.py CHANGED
@@ -10,7 +10,7 @@ from collections.abc import Iterator
10
10
  from pathlib import Path
11
11
  from typing import Annotated, Any, Literal, Optional, TypeVar
12
12
 
13
- from ddeutil.model.conn import Conn as ConnModel
13
+ from ddeutil.io.models.conn import Conn as ConnModel
14
14
  from pydantic import BaseModel, ConfigDict, Field
15
15
  from pydantic.functional_validators import field_validator
16
16
  from pydantic.types import SecretStr
@@ -43,27 +43,21 @@ class BaseConn(BaseModel):
43
43
  ]
44
44
 
45
45
  @classmethod
46
- def from_loader(
47
- cls,
48
- name: str,
49
- externals: DictData,
50
- ) -> Self:
51
- """Construct Connection with Loader object with specific config name.
46
+ def from_dict(cls, values: DictData) -> Self:
47
+ """Construct Connection Model from dict data. This construct is
48
+ different with ``.model_validate()`` because it will prepare the values
49
+ before using it if the data dose not have 'url'.
52
50
 
53
- :param name:
54
- :param externals:
51
+ :param values: A dict data that use to construct this model.
55
52
  """
56
- loader: Loader = Loader(name, externals=externals)
57
- # NOTE: Validate the config type match with current connection model
58
- if loader.type != cls:
59
- raise ValueError(f"Type {loader.type} does not match with {cls}")
53
+ # NOTE: filter out the fields of this model.
60
54
  filter_data: DictData = {
61
- k: loader.data.pop(k)
62
- for k in loader.data.copy()
55
+ k: values.pop(k)
56
+ for k in values.copy()
63
57
  if k not in cls.model_fields and k not in EXCLUDED_EXTRAS
64
58
  }
65
- if "url" in loader.data:
66
- url: ConnModel = ConnModel.from_url(loader.data.pop("url"))
59
+ if "url" in values:
60
+ url: ConnModel = ConnModel.from_url(values.pop("url"))
67
61
  return cls(
68
62
  dialect=url.dialect,
69
63
  host=url.host,
@@ -73,27 +67,38 @@ class BaseConn(BaseModel):
73
67
  # NOTE:
74
68
  # I will replace None endpoint with memory value for SQLite
75
69
  # connection string.
76
- endpoint=cls.__prepare_slash_from_url(url.endpoint or "memory"),
70
+ endpoint=(url.endpoint or "memory"),
77
71
  # NOTE: This order will show that externals this the top level.
78
- extras=(url.options | filter_data | externals),
72
+ extras=(url.options | filter_data),
79
73
  )
80
74
  return cls.model_validate(
81
75
  obj={
82
- "extras": (
83
- loader.data.pop("extras", {}) | filter_data | externals
84
- ),
85
- **loader.data,
76
+ "extras": (values.pop("extras", {}) | filter_data),
77
+ **values,
86
78
  }
87
79
  )
88
80
 
89
81
  @classmethod
90
- def __prepare_slash_from_url(cls, value: str) -> str:
91
- if value.startswith("/"):
92
- return value[1:]
93
- return value
82
+ def from_loader(cls, name: str, externals: DictData) -> Self:
83
+ """Construct Connection with Loader object with specific config name.
84
+
85
+ :param name: A config name.
86
+ :param externals: A external data that want to adding to extras.
87
+ """
88
+ loader: Loader = Loader(name, externals=externals)
89
+ # NOTE: Validate the config type match with current connection model
90
+ if loader.type != cls:
91
+ raise ValueError(f"Type {loader.type} does not match with {cls}")
92
+ return cls.from_dict(
93
+ {
94
+ "extras": (loader.data.pop("extras", {}) | externals),
95
+ **loader.data,
96
+ }
97
+ )
94
98
 
95
99
  @field_validator("endpoint")
96
100
  def __prepare_slash(cls, value: str) -> str:
101
+ """Prepare slash character that map double form URL model loading."""
97
102
  if value.startswith("//"):
98
103
  return value[1:]
99
104
  return value
@@ -146,7 +151,7 @@ class SFTP(Conn):
146
151
  dialect: Literal["sftp"] = "sftp"
147
152
 
148
153
  def __client(self):
149
- from .vendors.sftp_wrapped import WrapSFTP
154
+ from .vendors.sftp import WrapSFTP
150
155
 
151
156
  return WrapSFTP(
152
157
  host=self.host,
@@ -8,75 +8,5 @@ Define Errors Object for Node package
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Union
12
-
13
-
14
- class BaseError(Exception):
15
- """Base Error Object that use for catch any errors statement of
16
- all step in this src
17
- """
18
-
19
-
20
- class WorkflowBaseError(BaseError):
21
- """Core Base Error object"""
22
-
23
-
24
- class ConfigNotFound(WorkflowBaseError):
25
- """Error raise for a method not found the config file or data."""
26
-
27
-
28
- class ConfigArgumentError(WorkflowBaseError):
29
- """Error raise for a wrong configuration argument."""
30
-
31
- def __init__(self, argument: Union[str, tuple], message: str):
32
- """Main Initialization that merge the argument and message input values
33
- with specific content message together like
34
-
35
- `__class__` with `argument`, `message`
36
-
37
- :param argument: Union[str, tuple]
38
- :param message: str
39
- """
40
- if isinstance(argument, tuple):
41
- _last_arg: str = str(argument[-1])
42
- _argument: str = (
43
- (
44
- ", ".join([f"{_!r}" for _ in argument[:-1]])
45
- + f", and {_last_arg!r}"
46
- )
47
- if len(argument) > 1
48
- else f"{_last_arg!r}"
49
- )
50
- else:
51
- _argument: str = f"{argument!r}"
52
- _message: str = f"with {_argument}, {message}"
53
- super().__init__(_message)
54
-
55
-
56
- class ConnArgumentError(ConfigArgumentError):
57
- """Error raise for wrong connection argument when loading or parsing"""
58
-
59
-
60
- class DsArgumentError(ConfigArgumentError):
61
- """Error raise for wrong catalog argument when loading or parsing"""
62
-
63
-
64
- class NodeArgumentError(ConfigArgumentError):
65
- """Error raise for wrong node argument when loading or parsing"""
66
-
67
-
68
- class ScdlArgumentError(ConfigArgumentError):
69
- """Error raise for wrong schedule argument when loading or parsing"""
70
-
71
-
72
- class PipeArgumentError(ConfigArgumentError):
73
- """Error raise for wrong pipeline argument when loading or parsing"""
74
-
75
-
76
- class PyException(Exception): ...
77
-
78
-
79
- class ShellException(Exception): ...
80
-
81
11
 
82
12
  class TaskException(Exception): ...
@@ -5,187 +5,47 @@
5
5
  # ------------------------------------------------------------------------------
6
6
  from __future__ import annotations
7
7
 
8
- import copy
9
- import logging
10
- import urllib.parse
11
8
  from functools import cached_property
12
- from typing import Any, Callable, TypeVar
9
+ from typing import Any, ClassVar, TypeVar
13
10
 
14
11
  from ddeutil.core import (
15
- clear_cache,
16
12
  getdot,
17
13
  hasdot,
18
14
  import_string,
19
- setdot,
20
15
  )
21
16
  from ddeutil.io import (
22
- ConfigNotFound,
23
- Params,
17
+ PathData,
24
18
  PathSearch,
25
- Register,
26
19
  YamlEnvFl,
27
- map_func,
28
20
  )
29
- from ddeutil.io.__conf import UPDATE_KEY, VERSION_KEY
30
- from fmtutil import Datetime
31
- from pydantic import BaseModel
32
- from typing_extensions import Self
21
+ from pydantic import BaseModel, Field
22
+ from pydantic.functional_validators import model_validator
33
23
 
34
24
  from .__regex import RegexConf
35
- from .__types import DictData, TupleStr
36
- from .exceptions import ConfigArgumentError
25
+ from .__types import DictData
37
26
 
27
+ T = TypeVar("T")
28
+ BaseModelType = type[BaseModel]
38
29
  AnyModel = TypeVar("AnyModel", bound=BaseModel)
39
30
 
40
31
 
41
- class YamlEnvQuote(YamlEnvFl):
32
+ class Engine(BaseModel):
33
+ """Engine Model"""
42
34
 
43
- @staticmethod
44
- def prepare(x: str) -> str:
45
- return urllib.parse.quote_plus(str(x))
35
+ paths: PathData = Field(default_factory=PathData)
36
+ registry: list[str] = Field(default_factory=lambda: ["ddeutil.workflow"])
46
37
 
38
+ @model_validator(mode="before")
39
+ def __prepare_registry(cls, values: DictData) -> DictData:
40
+ if (_regis := values.get("registry")) and isinstance(_regis, str):
41
+ values["registry"] = [_regis]
42
+ return values
47
43
 
48
- class BaseLoad:
49
- """Base configuration data loading object for load config data from
50
- `cls.load_stage` stage. The base loading object contain necessary
51
- properties and method for type object.
52
44
 
53
- :param data: dict : A configuration data content with fix keys, `name`,
54
- `fullname`, and `data`.
55
- :param params: Optional[dict] : A parameters mapping for some
56
- subclass of loading use.
57
- """
58
-
59
- # NOTE: Set loading config for inherit
60
- load_prefixes: TupleStr = ("conn",)
61
- load_datetime_name: str = "audit_date"
62
- load_datetime_fmt: str = "%Y-%m-%d %H:%M:%S"
63
-
64
- # NOTE: Set preparing config for inherit
65
- data_excluded: TupleStr = (UPDATE_KEY, VERSION_KEY)
66
- option_key: TupleStr = ("parameters",)
67
- datetime_key: TupleStr = ("endpoint",)
68
-
69
- @classmethod
70
- def from_register(
71
- cls,
72
- name: str,
73
- params: Params,
74
- externals: DictData | None = None,
75
- ) -> Self:
76
- """Loading config data from register object.
77
-
78
- :param name: A name of config data catalog that can register.
79
- :type name: str
80
- :param params: A params object.
81
- :type params: Params
82
- :param externals: A external parameters
83
- :type externals: DictData | None(=None)
84
- """
85
- try:
86
- rs: Register = Register(
87
- name=name,
88
- stage=params.stage_final,
89
- params=params,
90
- loader=YamlEnvQuote,
91
- )
92
- except ConfigNotFound:
93
- rs: Register = Register(
94
- name=name,
95
- params=params,
96
- loader=YamlEnvQuote,
97
- ).deploy(stop=params.stage_final)
98
- return cls(
99
- name=rs.name,
100
- data=rs.data().copy(),
101
- params=params,
102
- externals=externals,
103
- )
104
-
105
- def __init__(
106
- self,
107
- name: str,
108
- data: DictData,
109
- params: Params,
110
- externals: DictData | None = None,
111
- ) -> None:
112
- """Main initialize base config object which get a name of configuration
113
- and load data by the register object.
114
- """
115
- self.name: str = name
116
- self.__data: DictData = data
117
- self.params: Params = params
118
- self.externals: DictData = externals or {}
119
-
120
- # NOTE: Validate step of base loading object.
121
- if not any(
122
- self.name.startswith(prefix) for prefix in self.load_prefixes
123
- ):
124
- raise ConfigArgumentError(
125
- "prefix",
126
- (
127
- f"{self.name!r} does not starts with the "
128
- f"{self.__class__.__name__} prefixes: "
129
- f"{self.load_prefixes!r}."
130
- ),
131
- )
132
-
133
- @property
134
- def updt(self):
135
- return self.data.get(UPDATE_KEY)
136
-
137
- @cached_property
138
- def _map_data(self) -> DictData:
139
- """Return configuration data without key in the excluded key set."""
140
- data: DictData = self.__data.copy()
141
- rs: DictData = {k: data[k] for k in data if k not in self.data_excluded}
142
-
143
- # Mapping datetime format to string value.
144
- for _ in self.datetime_key:
145
- if hasdot(_, rs):
146
- # Fill format datetime object to any type value.
147
- rs: DictData = setdot(
148
- _,
149
- rs,
150
- map_func(
151
- getdot(_, rs),
152
- Datetime.parse(
153
- value=self.externals[self.load_datetime_name],
154
- fmt=self.load_datetime_fmt,
155
- ).format,
156
- ),
157
- )
158
- return rs
45
+ class Params(BaseModel):
46
+ """Params Model"""
159
47
 
160
- @property
161
- def data(self) -> DictData:
162
- """Return deep copy of the input data.
163
-
164
- :rtype: DictData
165
- """
166
- return copy.deepcopy(self._map_data)
167
-
168
- @clear_cache(attrs=("type", "_map_data"))
169
- def refresh(self) -> Self:
170
- """Refresh configuration data. This process will use `deploy` method
171
- of the register object.
172
-
173
- :rtype: Self
174
- """
175
- return self.from_register(
176
- name=self.name,
177
- params=self.params,
178
- externals=self.externals,
179
- )
180
-
181
- @cached_property
182
- def type(self) -> Any:
183
- """Return object type which implement in `config_object` key."""
184
- if not (_typ := self.data.get("type")):
185
- raise ValueError(
186
- f"the 'type' value: {_typ} does not exists in config data."
187
- )
188
- return import_string(f"ddeutil.pipe.{_typ}")
48
+ engine: Engine = Field(default_factory=Engine)
189
49
 
190
50
 
191
51
  class SimLoad:
@@ -195,13 +55,11 @@ class SimLoad:
195
55
  :param params: A Params model object.
196
56
  :param externals: An external parameters
197
57
 
198
- Note:
58
+ Noted:
199
59
  The config data should have ``type`` key for engine can know what is
200
60
  config should to do next.
201
61
  """
202
62
 
203
- import_prefix: str = "ddeutil.workflow"
204
-
205
63
  def __init__(
206
64
  self,
207
65
  name: str,
@@ -215,7 +73,7 @@ class SimLoad:
215
73
  ):
216
74
  self.data = data
217
75
  if not self.data:
218
- raise ConfigNotFound(f"Config {name!r} does not found on conf path")
76
+ raise ValueError(f"Config {name!r} does not found on conf path")
219
77
  self.__conf_params: Params = params
220
78
  self.externals: DictData = externals
221
79
 
@@ -224,7 +82,7 @@ class SimLoad:
224
82
  return self.__conf_params
225
83
 
226
84
  @cached_property
227
- def type(self) -> AnyModel:
85
+ def type(self) -> BaseModelType:
228
86
  """Return object type which implement in `config_object` key."""
229
87
  if not (_typ := self.data.get("type")):
230
88
  raise ValueError(
@@ -234,33 +92,25 @@ class SimLoad:
234
92
  # NOTE: Auto adding module prefix if it does not set
235
93
  return import_string(f"ddeutil.workflow.{_typ}")
236
94
  except ModuleNotFoundError:
95
+ for registry in self.conf_params.engine.registry:
96
+ try:
97
+ return import_string(f"{registry}.{_typ}")
98
+ except ModuleNotFoundError:
99
+ continue
237
100
  return import_string(f"{_typ}")
238
101
 
239
- def params(self) -> dict[str, Callable[[Any], Any]]:
240
- """Return a mapping of key from params and imported value on params."""
241
- if not (p := self.data.get("params", {})):
242
- return p
102
+ def load(self) -> AnyModel:
103
+ return self.type.model_validate(self.data)
243
104
 
244
- try:
245
- return {i: import_string(f"{self.import_prefix}.{p[i]}") for i in p}
246
- except ModuleNotFoundError as err:
247
- logging.error(err)
248
- raise err
249
105
 
250
- def validate_params(self, param: dict[str, Any]) -> dict[str, Any]:
251
- """Return parameter that want to catch before workflow running."""
252
- try:
253
- return {i: caller(param[i]) for i, caller in self.params().items()}
254
- except KeyError as err:
255
- logging.error(f"Parameter: {err} does not exists from passing")
256
- raise err
257
- except ValueError as err:
258
- logging.error("Value that passing to params does not valid")
259
- raise err
106
+ class Loader(SimLoad):
107
+ """Main Loader Object that get the config `yaml` file from current path.
260
108
 
109
+ :param name: A name of config data that will read by Yaml Loader object.
110
+ :param externals: An external parameters
111
+ """
261
112
 
262
- class Loader(SimLoad):
263
- """Main Loader Object."""
113
+ conf_name: ClassVar[str] = "workflows-conf"
264
114
 
265
115
  def __init__(
266
116
  self,
@@ -278,23 +128,37 @@ class Loader(SimLoad):
278
128
 
279
129
  @classmethod
280
130
  def config(cls, path: str | None = None) -> Params:
131
+ """Load Config data from ``workflows-conf.yaml`` file."""
281
132
  return Params.model_validate(
282
- YamlEnvFl(path or "./workflows-conf.yaml").read()
133
+ YamlEnvFl(path or f"./{cls.conf_name}.yaml").read()
283
134
  )
284
135
 
285
136
 
286
- def map_caller(value: str, params: dict[str, Any]) -> Any:
287
- """Map caller value that found from ``RE_CALLER`` regex.
137
+ def map_params(value: Any, params: dict[str, Any]) -> Any:
138
+ """Map caller value that found from ``RE_CALLER`` regular expression.
139
+
140
+ :param value: A value that want to mapped with an params
141
+ :param params: A parameter value that getting with matched regular
142
+ expression.
288
143
 
289
- :returns: Any value that getter of caller receive from the params.
144
+ :rtype: Any
145
+ :returns: An any getter value from the params input.
290
146
  """
147
+ if isinstance(value, dict):
148
+ return {k: map_params(value[k], params) for k in value}
149
+ elif isinstance(value, (list, tuple, set)):
150
+ return type(value)([map_params(i, params) for i in value])
151
+ elif not isinstance(value, str):
152
+ return value
153
+
291
154
  if not (found := RegexConf.RE_CALLER.search(value)):
292
155
  return value
156
+
293
157
  # NOTE: get caller value that setting inside; ``${{ <caller-value> }}``
294
- caller = found.group("caller")
158
+ caller: str = found.group("caller")
295
159
  if not hasdot(caller, params):
296
160
  raise ValueError(f"params does not set caller: {caller!r}")
297
- getter = getdot(caller, params)
161
+ getter: Any = getdot(caller, params)
298
162
 
299
163
  # NOTE: check type of vars
300
164
  if isinstance(getter, (str, int)):