etlplus 0.15.0__py3-none-any.whl → 0.16.0__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.
- etlplus/README.md +3 -3
- etlplus/api/README.md +31 -0
- etlplus/api/auth.py +1 -1
- etlplus/api/config.py +5 -10
- etlplus/api/endpoint_client.py +4 -4
- etlplus/api/pagination/config.py +1 -1
- etlplus/api/pagination/paginator.py +6 -7
- etlplus/api/rate_limiting/config.py +4 -4
- etlplus/api/rate_limiting/rate_limiter.py +1 -1
- etlplus/api/retry_manager.py +2 -2
- etlplus/api/transport.py +1 -1
- etlplus/api/types.py +99 -0
- etlplus/api/utils.py +1 -1
- etlplus/cli/commands.py +75 -42
- etlplus/cli/constants.py +1 -1
- etlplus/cli/handlers.py +31 -13
- etlplus/cli/io.py +2 -2
- etlplus/cli/main.py +2 -2
- etlplus/cli/state.py +4 -7
- etlplus/connector/__init__.py +43 -0
- etlplus/connector/api.py +161 -0
- etlplus/connector/connector.py +26 -0
- etlplus/connector/core.py +132 -0
- etlplus/connector/database.py +122 -0
- etlplus/connector/enums.py +52 -0
- etlplus/connector/file.py +120 -0
- etlplus/connector/types.py +40 -0
- etlplus/connector/utils.py +122 -0
- etlplus/database/ddl.py +2 -2
- etlplus/database/engine.py +19 -3
- etlplus/database/orm.py +2 -0
- etlplus/enums.py +1 -33
- etlplus/file/_imports.py +1 -0
- etlplus/file/_io.py +52 -4
- etlplus/file/accdb.py +3 -2
- etlplus/file/arrow.py +3 -2
- etlplus/file/avro.py +3 -2
- etlplus/file/bson.py +3 -2
- etlplus/file/cbor.py +3 -2
- etlplus/file/cfg.py +3 -2
- etlplus/file/conf.py +3 -2
- etlplus/file/core.py +11 -8
- etlplus/file/csv.py +3 -2
- etlplus/file/dat.py +3 -2
- etlplus/file/dta.py +3 -2
- etlplus/file/duckdb.py +3 -2
- etlplus/file/enums.py +1 -1
- etlplus/file/feather.py +3 -2
- etlplus/file/fwf.py +3 -2
- etlplus/file/gz.py +3 -2
- etlplus/file/hbs.py +3 -2
- etlplus/file/hdf5.py +3 -2
- etlplus/file/ini.py +3 -2
- etlplus/file/ion.py +3 -2
- etlplus/file/jinja2.py +3 -2
- etlplus/file/json.py +5 -16
- etlplus/file/log.py +3 -2
- etlplus/file/mat.py +3 -2
- etlplus/file/mdb.py +3 -2
- etlplus/file/msgpack.py +3 -2
- etlplus/file/mustache.py +3 -2
- etlplus/file/nc.py +3 -2
- etlplus/file/ndjson.py +3 -2
- etlplus/file/numbers.py +3 -2
- etlplus/file/ods.py +3 -2
- etlplus/file/orc.py +3 -2
- etlplus/file/parquet.py +3 -2
- etlplus/file/pb.py +3 -2
- etlplus/file/pbf.py +3 -2
- etlplus/file/properties.py +3 -2
- etlplus/file/proto.py +3 -2
- etlplus/file/psv.py +3 -2
- etlplus/file/rda.py +3 -2
- etlplus/file/rds.py +3 -2
- etlplus/file/sas7bdat.py +3 -2
- etlplus/file/sav.py +3 -2
- etlplus/file/sqlite.py +3 -2
- etlplus/file/stub.py +1 -0
- etlplus/file/sylk.py +3 -2
- etlplus/file/tab.py +3 -2
- etlplus/file/toml.py +3 -2
- etlplus/file/tsv.py +3 -2
- etlplus/file/txt.py +4 -3
- etlplus/file/vm.py +3 -2
- etlplus/file/wks.py +3 -2
- etlplus/file/xls.py +3 -2
- etlplus/file/xlsm.py +3 -2
- etlplus/file/xlsx.py +3 -2
- etlplus/file/xml.py +9 -3
- etlplus/file/xpt.py +3 -2
- etlplus/file/yaml.py +5 -16
- etlplus/file/zip.py +3 -2
- etlplus/file/zsav.py +3 -2
- etlplus/ops/extract.py +13 -1
- etlplus/ops/load.py +15 -2
- etlplus/ops/run.py +4 -4
- etlplus/ops/transform.py +2 -2
- etlplus/ops/utils.py +6 -35
- etlplus/ops/validate.py +3 -3
- etlplus/types.py +3 -2
- etlplus/utils.py +163 -29
- etlplus/workflow/__init__.py +0 -11
- etlplus/workflow/jobs.py +84 -27
- etlplus/workflow/pipeline.py +48 -48
- {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/METADATA +4 -4
- etlplus-0.16.0.dist-info/RECORD +141 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/WHEEL +1 -1
- etlplus/config/README.md +0 -50
- etlplus/config/__init__.py +0 -33
- etlplus/config/types.py +0 -140
- etlplus/dag.py +0 -103
- etlplus/workflow/connector.py +0 -373
- etlplus/workflow/types.py +0 -115
- etlplus/workflow/utils.py +0 -120
- etlplus-0.15.0.dist-info/RECORD +0 -139
- {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/entry_points.txt +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/top_level.txt +0 -0
etlplus/ops/load.py
CHANGED
|
@@ -14,7 +14,7 @@ from typing import cast
|
|
|
14
14
|
|
|
15
15
|
from ..api import HttpMethod
|
|
16
16
|
from ..api.utils import resolve_request
|
|
17
|
-
from ..
|
|
17
|
+
from ..connector import DataConnectorType
|
|
18
18
|
from ..file import File
|
|
19
19
|
from ..file import FileFormat
|
|
20
20
|
from ..types import JSONData
|
|
@@ -23,6 +23,19 @@ from ..types import JSONList
|
|
|
23
23
|
from ..types import StrPath
|
|
24
24
|
from ..utils import count_records
|
|
25
25
|
|
|
26
|
+
# SECTION: EXPORTS ========================================================== #
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Functions
|
|
31
|
+
'load',
|
|
32
|
+
'load_data',
|
|
33
|
+
'load_to_api',
|
|
34
|
+
'load_to_database',
|
|
35
|
+
'load_to_file',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
# SECTION: INTERNAL FUNCTIONS ============================================== #
|
|
27
40
|
|
|
28
41
|
|
|
@@ -30,7 +43,7 @@ def _parse_json_string(
|
|
|
30
43
|
raw: str,
|
|
31
44
|
) -> JSONData:
|
|
32
45
|
"""
|
|
33
|
-
Parse JSON data from
|
|
46
|
+
Parse JSON data from *raw* text.
|
|
34
47
|
|
|
35
48
|
Parameters
|
|
36
49
|
----------
|
etlplus/ops/run.py
CHANGED
|
@@ -20,7 +20,7 @@ from ..api import RequestOptions
|
|
|
20
20
|
from ..api import compose_api_request_env
|
|
21
21
|
from ..api import compose_api_target_env
|
|
22
22
|
from ..api import paginate_with_client
|
|
23
|
-
from ..
|
|
23
|
+
from ..connector import DataConnectorType
|
|
24
24
|
from ..file import FileFormat
|
|
25
25
|
from ..types import JSONData
|
|
26
26
|
from ..types import JSONDict
|
|
@@ -94,7 +94,7 @@ def run(
|
|
|
94
94
|
Run a pipeline job defined in a YAML configuration.
|
|
95
95
|
|
|
96
96
|
By default it reads the configuration from ``in/pipeline.yml``, but callers
|
|
97
|
-
can provide an explicit
|
|
97
|
+
can provide an explicit *config_path* to override this.
|
|
98
98
|
|
|
99
99
|
Parameters
|
|
100
100
|
----------
|
|
@@ -328,11 +328,11 @@ def run_pipeline(
|
|
|
328
328
|
Parameters
|
|
329
329
|
----------
|
|
330
330
|
source_type : DataConnectorType | str | None, optional
|
|
331
|
-
Connector type for extraction. When ``None``,
|
|
331
|
+
Connector type for extraction. When ``None``, *source* is assumed
|
|
332
332
|
to be pre-loaded data and extraction is skipped.
|
|
333
333
|
source : StrPath | JSONData | None, optional
|
|
334
334
|
Data source for extraction or the pre-loaded payload when
|
|
335
|
-
|
|
335
|
+
*source_type* is ``None``.
|
|
336
336
|
operations : PipelineConfig | None, optional
|
|
337
337
|
Transform configuration passed to :func:`etlplus.ops.transform`.
|
|
338
338
|
target_type : DataConnectorType | str | None, optional
|
etlplus/ops/transform.py
CHANGED
|
@@ -110,7 +110,7 @@ def _agg_count(
|
|
|
110
110
|
present: int,
|
|
111
111
|
) -> int:
|
|
112
112
|
"""
|
|
113
|
-
Return the provided presence count
|
|
113
|
+
Return the provided presence count *present*.
|
|
114
114
|
|
|
115
115
|
Parameters
|
|
116
116
|
----------
|
|
@@ -120,7 +120,7 @@ def _agg_count(
|
|
|
120
120
|
Returns
|
|
121
121
|
-------
|
|
122
122
|
int
|
|
123
|
-
The provided presence count
|
|
123
|
+
The provided presence count *present*.
|
|
124
124
|
"""
|
|
125
125
|
return present
|
|
126
126
|
|
etlplus/ops/utils.py
CHANGED
|
@@ -7,13 +7,11 @@ The helpers defined here embrace a "high cohesion, low coupling" design by
|
|
|
7
7
|
isolating normalization, configuration, and logging responsibilities. The
|
|
8
8
|
resulting surface keeps ``maybe_validate`` focused on orchestration while
|
|
9
9
|
offloading ancillary concerns to composable helpers.
|
|
10
|
-
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
12
|
from __future__ import annotations
|
|
14
13
|
|
|
15
14
|
from collections.abc import Callable
|
|
16
|
-
from collections.abc import Mapping
|
|
17
15
|
from dataclasses import dataclass
|
|
18
16
|
from types import MappingProxyType
|
|
19
17
|
from typing import Any
|
|
@@ -23,7 +21,7 @@ from typing import TypedDict
|
|
|
23
21
|
from typing import cast
|
|
24
22
|
|
|
25
23
|
from ..types import StrAnyMap
|
|
26
|
-
from ..utils import
|
|
24
|
+
from ..utils import normalize_choice
|
|
27
25
|
|
|
28
26
|
# SECTION: TYPED DICTIONARIES =============================================== #
|
|
29
27
|
|
|
@@ -207,14 +205,14 @@ def maybe_validate(
|
|
|
207
205
|
Returns
|
|
208
206
|
-------
|
|
209
207
|
Any
|
|
210
|
-
|
|
208
|
+
*payload* when validation is skipped or when severity is ``"warn"``
|
|
211
209
|
and the validation fails. Returns the validator ``data`` payload when
|
|
212
210
|
validation succeeds.
|
|
213
211
|
|
|
214
212
|
Raises
|
|
215
213
|
------
|
|
216
214
|
ValueError
|
|
217
|
-
Raised when validation fails and
|
|
215
|
+
Raised when validation fails and *severity* is ``"error"``.
|
|
218
216
|
|
|
219
217
|
Examples
|
|
220
218
|
--------
|
|
@@ -320,7 +318,7 @@ def _normalize_phase(
|
|
|
320
318
|
"""
|
|
321
319
|
return cast(
|
|
322
320
|
ValidationPhase,
|
|
323
|
-
|
|
321
|
+
normalize_choice(
|
|
324
322
|
value,
|
|
325
323
|
mapping=_PHASE_CHOICES,
|
|
326
324
|
default='before_transform',
|
|
@@ -346,7 +344,7 @@ def _normalize_severity(
|
|
|
346
344
|
"""
|
|
347
345
|
return cast(
|
|
348
346
|
ValidationSeverity,
|
|
349
|
-
|
|
347
|
+
normalize_choice(
|
|
350
348
|
value,
|
|
351
349
|
mapping=_SEVERITY_CHOICES,
|
|
352
350
|
default='error',
|
|
@@ -372,7 +370,7 @@ def _normalize_window(
|
|
|
372
370
|
"""
|
|
373
371
|
return cast(
|
|
374
372
|
ValidationWindow,
|
|
375
|
-
|
|
373
|
+
normalize_choice(
|
|
376
374
|
value,
|
|
377
375
|
mapping=_WINDOW_CHOICES,
|
|
378
376
|
default='both',
|
|
@@ -380,33 +378,6 @@ def _normalize_window(
|
|
|
380
378
|
)
|
|
381
379
|
|
|
382
380
|
|
|
383
|
-
def _normalize_choice(
|
|
384
|
-
value: str | None,
|
|
385
|
-
*,
|
|
386
|
-
mapping: Mapping[str, str],
|
|
387
|
-
default: str,
|
|
388
|
-
) -> str:
|
|
389
|
-
"""
|
|
390
|
-
Normalize a text value against a mapping with a default fallback.
|
|
391
|
-
|
|
392
|
-
Parameters
|
|
393
|
-
----------
|
|
394
|
-
value : str | None
|
|
395
|
-
Input text to normalize.
|
|
396
|
-
mapping : Mapping[str, str]
|
|
397
|
-
Mapping of accepted values to normalized outputs.
|
|
398
|
-
default : str
|
|
399
|
-
Default to return when input is missing or unrecognized.
|
|
400
|
-
|
|
401
|
-
Returns
|
|
402
|
-
-------
|
|
403
|
-
str
|
|
404
|
-
Normalized value.
|
|
405
|
-
"""
|
|
406
|
-
normalized = normalized_str(value)
|
|
407
|
-
return mapping.get(normalized, default)
|
|
408
|
-
|
|
409
|
-
|
|
410
381
|
def _rule_name(
|
|
411
382
|
rules: Ruleset,
|
|
412
383
|
) -> str | None:
|
etlplus/ops/validate.py
CHANGED
|
@@ -11,8 +11,8 @@ Highlights
|
|
|
11
11
|
----------
|
|
12
12
|
- Centralized type map and helpers for clarity and reuse.
|
|
13
13
|
- Consistent error wording; field and item paths like ``[2].email``.
|
|
14
|
-
- Small, focused public API with
|
|
15
|
-
|
|
14
|
+
- Small, focused public API with :func:`load_data`, :func:`validate_field`,
|
|
15
|
+
:func:`validate`.
|
|
16
16
|
|
|
17
17
|
Examples
|
|
18
18
|
--------
|
|
@@ -66,7 +66,7 @@ TYPE_MAP: Final[dict[str, type | tuple[type, ...]]] = {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
# SECTION:
|
|
69
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class FieldRules(TypedDict, total=False):
|
etlplus/types.py
CHANGED
|
@@ -11,8 +11,9 @@ Notes
|
|
|
11
11
|
|
|
12
12
|
See Also
|
|
13
13
|
--------
|
|
14
|
-
- :mod:`etlplus.api.types` for HTTP-specific aliases
|
|
15
|
-
- :mod:`etlplus.
|
|
14
|
+
- :mod:`etlplus.api.types` for HTTP-specific aliases and data classes
|
|
15
|
+
- :mod:`etlplus.connector.types` for connector-specific aliases and TypedDict
|
|
16
|
+
surfaces
|
|
16
17
|
|
|
17
18
|
Examples
|
|
18
19
|
--------
|
etlplus/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
+
from collections.abc import Iterable
|
|
11
12
|
from collections.abc import Mapping
|
|
12
13
|
from typing import Any
|
|
13
14
|
from typing import TypeVar
|
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
|
25
26
|
# Mapping utilities
|
|
26
27
|
'cast_str_dict',
|
|
27
28
|
'coerce_dict',
|
|
29
|
+
'deep_substitute',
|
|
28
30
|
'maybe_mapping',
|
|
29
31
|
# Float coercion
|
|
30
32
|
'to_float',
|
|
@@ -39,7 +41,8 @@ __all__ = [
|
|
|
39
41
|
# Generic number coercion
|
|
40
42
|
'to_number',
|
|
41
43
|
# Text processing
|
|
42
|
-
'
|
|
44
|
+
'normalize_choice',
|
|
45
|
+
'normalize_str',
|
|
43
46
|
]
|
|
44
47
|
|
|
45
48
|
|
|
@@ -56,6 +59,52 @@ Num = TypeVar('Num', int, float)
|
|
|
56
59
|
# -- Data Utilities -- #
|
|
57
60
|
|
|
58
61
|
|
|
62
|
+
def deep_substitute(
|
|
63
|
+
value: Any,
|
|
64
|
+
vars_map: StrAnyMap | None,
|
|
65
|
+
env_map: Mapping[str, str] | None,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""
|
|
68
|
+
Recursively substitute ``${VAR}`` tokens in nested structures.
|
|
69
|
+
|
|
70
|
+
Only strings are substituted; other types are returned as-is.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
value : Any
|
|
75
|
+
The value to perform substitutions on.
|
|
76
|
+
vars_map : StrAnyMap | None
|
|
77
|
+
Mapping of variable names to replacement values (lower precedence).
|
|
78
|
+
env_map : Mapping[str, str] | None
|
|
79
|
+
Mapping of environment variables overriding *vars_map* values (higher
|
|
80
|
+
precedence).
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
Any
|
|
85
|
+
New structure with substitutions applied where tokens were found.
|
|
86
|
+
"""
|
|
87
|
+
substitutions = _prepare_substitutions(vars_map, env_map)
|
|
88
|
+
|
|
89
|
+
def _apply(node: Any) -> Any:
|
|
90
|
+
match node:
|
|
91
|
+
case str():
|
|
92
|
+
return _replace_tokens(node, substitutions)
|
|
93
|
+
case Mapping():
|
|
94
|
+
return {k: _apply(v) for k, v in node.items()}
|
|
95
|
+
case list() | tuple() as seq:
|
|
96
|
+
apply = [_apply(item) for item in seq]
|
|
97
|
+
return apply if isinstance(seq, list) else tuple(apply)
|
|
98
|
+
case set():
|
|
99
|
+
return {_apply(item) for item in node}
|
|
100
|
+
case frozenset():
|
|
101
|
+
return frozenset(_apply(item) for item in node)
|
|
102
|
+
case _:
|
|
103
|
+
return node
|
|
104
|
+
|
|
105
|
+
return _apply(value)
|
|
106
|
+
|
|
107
|
+
|
|
59
108
|
def cast_str_dict(
|
|
60
109
|
mapping: StrAnyMap | None,
|
|
61
110
|
) -> dict[str, str]:
|
|
@@ -81,7 +130,7 @@ def coerce_dict(
|
|
|
81
130
|
value: Any,
|
|
82
131
|
) -> dict[str, Any]:
|
|
83
132
|
"""
|
|
84
|
-
Return a ``dict`` copy when
|
|
133
|
+
Return a ``dict`` copy when *value* is mapping-like.
|
|
85
134
|
|
|
86
135
|
Parameters
|
|
87
136
|
----------
|
|
@@ -91,7 +140,7 @@ def coerce_dict(
|
|
|
91
140
|
Returns
|
|
92
141
|
-------
|
|
93
142
|
dict[str, Any]
|
|
94
|
-
Shallow copy of
|
|
143
|
+
Shallow copy of *value* converted to a standard ``dict``.
|
|
95
144
|
"""
|
|
96
145
|
return dict(value) if isinstance(value, Mapping) else {}
|
|
97
146
|
|
|
@@ -121,7 +170,7 @@ def maybe_mapping(
|
|
|
121
170
|
value: Any,
|
|
122
171
|
) -> StrAnyMap | None:
|
|
123
172
|
"""
|
|
124
|
-
Return
|
|
173
|
+
Return *value* when it is mapping-like; otherwise ``None``.
|
|
125
174
|
|
|
126
175
|
Parameters
|
|
127
176
|
----------
|
|
@@ -140,7 +189,7 @@ def print_json(
|
|
|
140
189
|
obj: Any,
|
|
141
190
|
) -> None:
|
|
142
191
|
"""
|
|
143
|
-
Pretty-print
|
|
192
|
+
Pretty-print *obj* as UTF-8 JSON without ASCII escaping.
|
|
144
193
|
|
|
145
194
|
Parameters
|
|
146
195
|
----------
|
|
@@ -165,12 +214,12 @@ def to_float(
|
|
|
165
214
|
maximum: float | None = None,
|
|
166
215
|
) -> float | None:
|
|
167
216
|
"""
|
|
168
|
-
Coerce
|
|
217
|
+
Coerce *value* to a float with optional fallback and bounds.
|
|
169
218
|
|
|
170
219
|
Notes
|
|
171
220
|
-----
|
|
172
221
|
For strings, leading/trailing whitespace is ignored. Returns ``None``
|
|
173
|
-
when coercion fails and no
|
|
222
|
+
when coercion fails and no *default* is provided.
|
|
174
223
|
"""
|
|
175
224
|
return _normalize_number(
|
|
176
225
|
_coerce_float,
|
|
@@ -186,7 +235,7 @@ def to_maximum_float(
|
|
|
186
235
|
default: float,
|
|
187
236
|
) -> float:
|
|
188
237
|
"""
|
|
189
|
-
Return the greater of
|
|
238
|
+
Return the greater of *default* and *value* after float coercion.
|
|
190
239
|
|
|
191
240
|
Parameters
|
|
192
241
|
----------
|
|
@@ -198,7 +247,7 @@ def to_maximum_float(
|
|
|
198
247
|
Returns
|
|
199
248
|
-------
|
|
200
249
|
float
|
|
201
|
-
|
|
250
|
+
*default* if coercion fails; else ``max(coerced, default)``.
|
|
202
251
|
"""
|
|
203
252
|
result = to_float(value, default)
|
|
204
253
|
return max(_value_or_default(result, default), default)
|
|
@@ -209,7 +258,7 @@ def to_minimum_float(
|
|
|
209
258
|
default: float,
|
|
210
259
|
) -> float:
|
|
211
260
|
"""
|
|
212
|
-
Return the lesser of
|
|
261
|
+
Return the lesser of *default* and *value* after float coercion.
|
|
213
262
|
|
|
214
263
|
Parameters
|
|
215
264
|
----------
|
|
@@ -221,7 +270,7 @@ def to_minimum_float(
|
|
|
221
270
|
Returns
|
|
222
271
|
-------
|
|
223
272
|
float
|
|
224
|
-
|
|
273
|
+
*default* if coercion fails; else ``min(coerced, default)``.
|
|
225
274
|
"""
|
|
226
275
|
result = to_float(value, default)
|
|
227
276
|
return min(_value_or_default(result, default), default)
|
|
@@ -257,12 +306,12 @@ def to_int(
|
|
|
257
306
|
maximum: int | None = None,
|
|
258
307
|
) -> int | None:
|
|
259
308
|
"""
|
|
260
|
-
Coerce
|
|
309
|
+
Coerce *value* to an integer with optional fallback and bounds.
|
|
261
310
|
|
|
262
311
|
Notes
|
|
263
312
|
-----
|
|
264
313
|
For strings, leading/trailing whitespace is ignored. Returns ``None``
|
|
265
|
-
when coercion fails and no
|
|
314
|
+
when coercion fails and no *default* is provided.
|
|
266
315
|
"""
|
|
267
316
|
return _normalize_number(
|
|
268
317
|
_coerce_int,
|
|
@@ -278,7 +327,7 @@ def to_maximum_int(
|
|
|
278
327
|
default: int,
|
|
279
328
|
) -> int:
|
|
280
329
|
"""
|
|
281
|
-
Return the greater of
|
|
330
|
+
Return the greater of *default* and *value* after integer coercion.
|
|
282
331
|
|
|
283
332
|
Parameters
|
|
284
333
|
----------
|
|
@@ -290,7 +339,7 @@ def to_maximum_int(
|
|
|
290
339
|
Returns
|
|
291
340
|
-------
|
|
292
341
|
int
|
|
293
|
-
|
|
342
|
+
*default* if coercion fails; else ``max(coerced, default)``.
|
|
294
343
|
"""
|
|
295
344
|
result = to_int(value, default)
|
|
296
345
|
return max(_value_or_default(result, default), default)
|
|
@@ -301,7 +350,7 @@ def to_minimum_int(
|
|
|
301
350
|
default: int,
|
|
302
351
|
) -> int:
|
|
303
352
|
"""
|
|
304
|
-
Return the lesser of
|
|
353
|
+
Return the lesser of *default* and *value* after integer coercion.
|
|
305
354
|
|
|
306
355
|
Parameters
|
|
307
356
|
----------
|
|
@@ -313,7 +362,7 @@ def to_minimum_int(
|
|
|
313
362
|
Returns
|
|
314
363
|
-------
|
|
315
364
|
int
|
|
316
|
-
|
|
365
|
+
*default* if coercion fails; else ``min(coerced, default)``.
|
|
317
366
|
"""
|
|
318
367
|
result = to_int(value, default)
|
|
319
368
|
return min(_value_or_default(result, default), default)
|
|
@@ -326,21 +375,21 @@ def to_positive_int(
|
|
|
326
375
|
minimum: int = 1,
|
|
327
376
|
) -> int:
|
|
328
377
|
"""
|
|
329
|
-
Return a positive integer, falling back to
|
|
378
|
+
Return a positive integer, falling back to *minimum* when needed.
|
|
330
379
|
|
|
331
380
|
Parameters
|
|
332
381
|
----------
|
|
333
382
|
value : Any
|
|
334
383
|
Candidate input coerced with :func:`to_int`.
|
|
335
384
|
default : int
|
|
336
|
-
Fallback value when coercion fails; clamped by
|
|
385
|
+
Fallback value when coercion fails; clamped by *minimum*.
|
|
337
386
|
minimum : int
|
|
338
387
|
Inclusive lower bound for the result. Defaults to ``1``.
|
|
339
388
|
|
|
340
389
|
Returns
|
|
341
390
|
-------
|
|
342
391
|
int
|
|
343
|
-
Positive integer respecting
|
|
392
|
+
Positive integer respecting *minimum*.
|
|
344
393
|
"""
|
|
345
394
|
result = to_int(value, default, minimum=minimum)
|
|
346
395
|
return _value_or_default(result, minimum)
|
|
@@ -353,7 +402,7 @@ def to_number(
|
|
|
353
402
|
value: object,
|
|
354
403
|
) -> float | None:
|
|
355
404
|
"""
|
|
356
|
-
Coerce
|
|
405
|
+
Coerce *value* to a ``float`` using the internal float coercer.
|
|
357
406
|
|
|
358
407
|
Parameters
|
|
359
408
|
----------
|
|
@@ -372,7 +421,7 @@ def to_number(
|
|
|
372
421
|
# -- Text Processing -- #
|
|
373
422
|
|
|
374
423
|
|
|
375
|
-
def
|
|
424
|
+
def normalize_str(
|
|
376
425
|
value: str | None,
|
|
377
426
|
) -> str:
|
|
378
427
|
"""
|
|
@@ -392,6 +441,36 @@ def normalized_str(
|
|
|
392
441
|
return (value or '').strip().lower()
|
|
393
442
|
|
|
394
443
|
|
|
444
|
+
def normalize_choice(
|
|
445
|
+
value: str | None,
|
|
446
|
+
*,
|
|
447
|
+
mapping: Mapping[str, str],
|
|
448
|
+
default: str,
|
|
449
|
+
normalize: Callable[[str | None], str] = normalize_str,
|
|
450
|
+
) -> str:
|
|
451
|
+
"""
|
|
452
|
+
Normalize a string choice using a mapping and fallback.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
value : str | None
|
|
457
|
+
Input value to normalize.
|
|
458
|
+
mapping : Mapping[str, str]
|
|
459
|
+
Mapping of acceptable normalized inputs to output values.
|
|
460
|
+
default : str
|
|
461
|
+
Default return value when input is missing or unrecognized.
|
|
462
|
+
normalize : Callable[[str | None], str], optional
|
|
463
|
+
Normalization function applied to *value*. Defaults to
|
|
464
|
+
:func:`normalize_str`.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
str
|
|
469
|
+
Normalized mapped value or *default*.
|
|
470
|
+
"""
|
|
471
|
+
return mapping.get(normalize(value), default)
|
|
472
|
+
|
|
473
|
+
|
|
395
474
|
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
396
475
|
|
|
397
476
|
|
|
@@ -401,7 +480,7 @@ def _clamp(
|
|
|
401
480
|
maximum: Num | None,
|
|
402
481
|
) -> Num:
|
|
403
482
|
"""
|
|
404
|
-
Return
|
|
483
|
+
Return *value* constrained to the interval ``[minimum, maximum]``.
|
|
405
484
|
|
|
406
485
|
Parameters
|
|
407
486
|
----------
|
|
@@ -425,6 +504,61 @@ def _clamp(
|
|
|
425
504
|
return value
|
|
426
505
|
|
|
427
506
|
|
|
507
|
+
def _prepare_substitutions(
|
|
508
|
+
vars_map: StrAnyMap | None,
|
|
509
|
+
env_map: Mapping[str, Any] | None,
|
|
510
|
+
) -> tuple[tuple[str, Any], ...]:
|
|
511
|
+
"""
|
|
512
|
+
Merge variable and environment maps into an ordered substitutions list.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
vars_map : StrAnyMap | None
|
|
517
|
+
Mapping of variable names to replacement values (lower precedence).
|
|
518
|
+
env_map : Mapping[str, Any] | None
|
|
519
|
+
Environment-backed values that override entries from *vars_map*.
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
tuple[tuple[str, Any], ...]
|
|
524
|
+
Immutable sequence of ``(name, value)`` pairs suitable for token
|
|
525
|
+
replacement.
|
|
526
|
+
"""
|
|
527
|
+
if not vars_map and not env_map:
|
|
528
|
+
return ()
|
|
529
|
+
merged: dict[str, Any] = {**(vars_map or {}), **(env_map or {})}
|
|
530
|
+
return tuple(merged.items())
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _replace_tokens(
|
|
534
|
+
text: str,
|
|
535
|
+
substitutions: Iterable[tuple[str, Any]],
|
|
536
|
+
) -> str:
|
|
537
|
+
"""
|
|
538
|
+
Replace ``${VAR}`` tokens in *text* using *substitutions*.
|
|
539
|
+
|
|
540
|
+
Parameters
|
|
541
|
+
----------
|
|
542
|
+
text : str
|
|
543
|
+
Input string that may contain ``${VAR}`` tokens.
|
|
544
|
+
substitutions : Iterable[tuple[str, Any]]
|
|
545
|
+
Sequence of ``(name, value)`` pairs used for token replacement.
|
|
546
|
+
|
|
547
|
+
Returns
|
|
548
|
+
-------
|
|
549
|
+
str
|
|
550
|
+
Updated text with replacements applied.
|
|
551
|
+
"""
|
|
552
|
+
if not substitutions:
|
|
553
|
+
return text
|
|
554
|
+
out = text
|
|
555
|
+
for name, replacement in substitutions:
|
|
556
|
+
token = f'${{{name}}}'
|
|
557
|
+
if token in out:
|
|
558
|
+
out = out.replace(token, str(replacement))
|
|
559
|
+
return out
|
|
560
|
+
|
|
561
|
+
|
|
428
562
|
def _coerce_float(
|
|
429
563
|
value: object,
|
|
430
564
|
) -> float | None:
|
|
@@ -502,7 +636,7 @@ def _integral_from_float(
|
|
|
502
636
|
candidate: float | None,
|
|
503
637
|
) -> int | None:
|
|
504
638
|
"""
|
|
505
|
-
Return ``int(candidate)`` when
|
|
639
|
+
Return ``int(candidate)`` when *candidate* is integral.
|
|
506
640
|
|
|
507
641
|
Parameters
|
|
508
642
|
----------
|
|
@@ -512,7 +646,7 @@ def _integral_from_float(
|
|
|
512
646
|
Returns
|
|
513
647
|
-------
|
|
514
648
|
int | None
|
|
515
|
-
Integer form of
|
|
649
|
+
Integer form of *candidate*; else ``None`` if not integral.
|
|
516
650
|
"""
|
|
517
651
|
if candidate is None or not candidate.is_integer():
|
|
518
652
|
return None
|
|
@@ -528,7 +662,7 @@ def _normalize_number(
|
|
|
528
662
|
maximum: Num | None = None,
|
|
529
663
|
) -> Num | None:
|
|
530
664
|
"""
|
|
531
|
-
Coerce
|
|
665
|
+
Coerce *value* with *coercer* and optionally clamp it.
|
|
532
666
|
|
|
533
667
|
Parameters
|
|
534
668
|
----------
|
|
@@ -561,7 +695,7 @@ def _validate_bounds(
|
|
|
561
695
|
maximum: Num | None,
|
|
562
696
|
) -> tuple[Num | None, Num | None]:
|
|
563
697
|
"""
|
|
564
|
-
Ensure
|
|
698
|
+
Ensure *minimum* does not exceed *maximum*.
|
|
565
699
|
|
|
566
700
|
Parameters
|
|
567
701
|
----------
|
|
@@ -590,7 +724,7 @@ def _value_or_default(
|
|
|
590
724
|
default: Num,
|
|
591
725
|
) -> Num:
|
|
592
726
|
"""
|
|
593
|
-
Return
|
|
727
|
+
Return *value* if not ``None``; else *default*.
|
|
594
728
|
|
|
595
729
|
Parameters
|
|
596
730
|
----------
|
|
@@ -602,6 +736,6 @@ def _value_or_default(
|
|
|
602
736
|
Returns
|
|
603
737
|
-------
|
|
604
738
|
Num
|
|
605
|
-
|
|
739
|
+
*value* or *default*.
|
|
606
740
|
"""
|
|
607
741
|
return default if value is None else value
|
etlplus/workflow/__init__.py
CHANGED
|
@@ -6,11 +6,6 @@ Job workflow helpers.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from .connector import Connector
|
|
10
|
-
from .connector import ConnectorApi
|
|
11
|
-
from .connector import ConnectorDb
|
|
12
|
-
from .connector import ConnectorFile
|
|
13
|
-
from .connector import parse_connector
|
|
14
9
|
from .dag import topological_sort_jobs
|
|
15
10
|
from .jobs import ExtractRef
|
|
16
11
|
from .jobs import JobConfig
|
|
@@ -25,9 +20,6 @@ from .pipeline import load_pipeline_config
|
|
|
25
20
|
|
|
26
21
|
__all__ = [
|
|
27
22
|
# Data Classes
|
|
28
|
-
'ConnectorApi',
|
|
29
|
-
'ConnectorDb',
|
|
30
|
-
'ConnectorFile',
|
|
31
23
|
'ExtractRef',
|
|
32
24
|
'JobConfig',
|
|
33
25
|
'LoadRef',
|
|
@@ -36,8 +28,5 @@ __all__ = [
|
|
|
36
28
|
'ValidationRef',
|
|
37
29
|
# Functions
|
|
38
30
|
'load_pipeline_config',
|
|
39
|
-
'parse_connector',
|
|
40
31
|
'topological_sort_jobs',
|
|
41
|
-
# Type Aliases
|
|
42
|
-
'Connector',
|
|
43
32
|
]
|