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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__types.py +1 -0
- ddeutil/workflow/conn.py +33 -28
- ddeutil/workflow/exceptions.py +0 -70
- ddeutil/workflow/loader.py +55 -191
- ddeutil/workflow/pipeline.py +264 -110
- ddeutil/workflow/schedule.py +10 -15
- ddeutil/workflow/tasks/__init__.py +6 -10
- ddeutil/workflow/tasks/_pandas.py +54 -0
- ddeutil/workflow/tasks/_polars.py +55 -4
- ddeutil/workflow/utils.py +180 -0
- ddeutil/workflow/vendors/__dataset.py +127 -0
- ddeutil/workflow/vendors/pd.py +13 -0
- ddeutil/workflow/vendors/pg.py +11 -0
- ddeutil/workflow/{dataset.py → vendors/pl.py} +4 -138
- {ddeutil_workflow-0.0.1.dist-info → ddeutil_workflow-0.0.3.dist-info}/METADATA +35 -20
- ddeutil_workflow-0.0.3.dist-info/RECORD +29 -0
- ddeutil/workflow/hooks/__init__.py +0 -9
- ddeutil/workflow/hooks/_postgres.py +0 -2
- ddeutil/workflow/utils/receive.py +0 -33
- ddeutil/workflow/utils/selection.py +0 -2
- ddeutil_workflow-0.0.1.dist-info/RECORD +0 -28
- /ddeutil/workflow/vendors/{aws_warpped.py → aws.py} +0 -0
- /ddeutil/workflow/{utils/__init__.py → vendors/az.py} +0 -0
- /ddeutil/workflow/vendors/{minio_warpped.py → minio.py} +0 -0
- /ddeutil/workflow/vendors/{sftp_wrapped.py → sftp.py} +0 -0
- {ddeutil_workflow-0.0.1.dist-info → ddeutil_workflow-0.0.3.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.1.dist-info → ddeutil_workflow-0.0.3.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.1.dist-info → ddeutil_workflow-0.0.3.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.3"
|
ddeutil/workflow/__types.py
CHANGED
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.
|
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
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
54
|
-
:param externals:
|
51
|
+
:param values: A dict data that use to construct this model.
|
55
52
|
"""
|
56
|
-
|
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:
|
62
|
-
for k in
|
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
|
66
|
-
url: ConnModel = ConnModel.from_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=
|
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
|
72
|
+
extras=(url.options | filter_data),
|
79
73
|
)
|
80
74
|
return cls.model_validate(
|
81
75
|
obj={
|
82
|
-
"extras": (
|
83
|
-
|
84
|
-
),
|
85
|
-
**loader.data,
|
76
|
+
"extras": (values.pop("extras", {}) | filter_data),
|
77
|
+
**values,
|
86
78
|
}
|
87
79
|
)
|
88
80
|
|
89
81
|
@classmethod
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
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.
|
154
|
+
from .vendors.sftp import WrapSFTP
|
150
155
|
|
151
156
|
return WrapSFTP(
|
152
157
|
host=self.host,
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -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): ...
|
ddeutil/workflow/loader.py
CHANGED
@@ -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,
|
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
|
-
|
23
|
-
Params,
|
17
|
+
PathData,
|
24
18
|
PathSearch,
|
25
|
-
Register,
|
26
19
|
YamlEnvFl,
|
27
|
-
map_func,
|
28
20
|
)
|
29
|
-
from
|
30
|
-
from
|
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
|
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
|
32
|
+
class Engine(BaseModel):
|
33
|
+
"""Engine Model"""
|
42
34
|
|
43
|
-
|
44
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
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
|
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) ->
|
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
|
240
|
-
|
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
|
-
|
251
|
-
|
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
|
-
|
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 "./
|
133
|
+
YamlEnvFl(path or f"./{cls.conf_name}.yaml").read()
|
283
134
|
)
|
284
135
|
|
285
136
|
|
286
|
-
def
|
287
|
-
"""Map caller value that found from ``RE_CALLER``
|
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
|
-
:
|
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)):
|