etlplus 0.15.0__py3-none-any.whl → 0.15.2__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/config.py +3 -8
- etlplus/api/types.py +89 -0
- etlplus/cli/commands.py +75 -42
- etlplus/cli/handlers.py +30 -12
- etlplus/cli/main.py +1 -1
- etlplus/cli/state.py +4 -7
- etlplus/database/engine.py +18 -2
- etlplus/database/orm.py +2 -0
- etlplus/file/_io.py +39 -0
- etlplus/file/json.py +2 -14
- etlplus/file/yaml.py +2 -14
- etlplus/ops/utils.py +4 -33
- etlplus/ops/validate.py +3 -3
- etlplus/types.py +3 -2
- etlplus/utils.py +136 -2
- etlplus/workflow/connector.py +41 -28
- etlplus/workflow/jobs.py +84 -27
- etlplus/workflow/pipeline.py +46 -46
- etlplus/workflow/types.py +1 -1
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/METADATA +4 -4
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/RECORD +27 -32
- 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/utils.py +0 -120
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/WHEEL +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/entry_points.txt +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.15.2.dist-info}/top_level.txt +0 -0
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
|
|
|
@@ -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.workflow.types` for workflow-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
|
|
80
|
+
(higher 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]:
|
|
@@ -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
|
|
|
@@ -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:
|
etlplus/workflow/connector.py
CHANGED
|
@@ -22,15 +22,15 @@ Notes
|
|
|
22
22
|
-----
|
|
23
23
|
- TypedDict shapes are editor hints; runtime parsing remains permissive
|
|
24
24
|
(from_obj accepts Mapping[str, Any]).
|
|
25
|
-
- TypedDicts referenced in :mod:`etlplus.
|
|
25
|
+
- TypedDicts referenced in :mod:`etlplus.workflow.types` remain editor hints.
|
|
26
26
|
Runtime parsing stays permissive and tolerant.
|
|
27
27
|
|
|
28
28
|
See Also
|
|
29
29
|
--------
|
|
30
30
|
- TypedDict shapes for editor hints (not enforced at runtime):
|
|
31
|
-
:mod:`etlplus.
|
|
32
|
-
:mod:`etlplus.
|
|
33
|
-
:mod:`etlplus.
|
|
31
|
+
:mod:`etlplus.workflow.types.ConnectorApiConfigMap`,
|
|
32
|
+
:mod:`etlplus.workflow.types.ConnectorDbConfigMap`,
|
|
33
|
+
:mod:`etlplus.workflow.types.ConnectorFileConfigMap`.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
from __future__ import annotations
|
|
@@ -71,6 +71,40 @@ __all__ = [
|
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
# SECTION: INTERNAL FUNCTIONS ============================================== #
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _require_name(
|
|
78
|
+
obj: StrAnyMap,
|
|
79
|
+
*,
|
|
80
|
+
kind: str,
|
|
81
|
+
) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Extract and validate the ``name`` field from connector mappings.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
obj : StrAnyMap
|
|
88
|
+
Connector mapping with a ``name`` entry.
|
|
89
|
+
kind : str
|
|
90
|
+
Connector kind used in the error message.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
str
|
|
95
|
+
Valid connector name.
|
|
96
|
+
|
|
97
|
+
Raises
|
|
98
|
+
------
|
|
99
|
+
TypeError
|
|
100
|
+
If ``name`` is missing or not a string.
|
|
101
|
+
"""
|
|
102
|
+
name = obj.get('name')
|
|
103
|
+
if not isinstance(name, str):
|
|
104
|
+
raise TypeError(f'Connector{kind} requires a "name" (str)')
|
|
105
|
+
return name
|
|
106
|
+
|
|
107
|
+
|
|
74
108
|
# SECTION: DATA CLASSES ===================================================== #
|
|
75
109
|
|
|
76
110
|
|
|
@@ -151,15 +185,8 @@ class ConnectorApi:
|
|
|
151
185
|
-------
|
|
152
186
|
Self
|
|
153
187
|
Parsed connector instance.
|
|
154
|
-
|
|
155
|
-
Raises
|
|
156
|
-
------
|
|
157
|
-
TypeError
|
|
158
|
-
If ``name`` is missing or invalid.
|
|
159
188
|
"""
|
|
160
|
-
name = obj
|
|
161
|
-
if not isinstance(name, str):
|
|
162
|
-
raise TypeError('ConnectorApi requires a "name" (str)')
|
|
189
|
+
name = _require_name(obj, kind='Api')
|
|
163
190
|
headers = cast_str_dict(obj.get('headers'))
|
|
164
191
|
|
|
165
192
|
return cls(
|
|
@@ -233,15 +260,8 @@ class ConnectorDb:
|
|
|
233
260
|
-------
|
|
234
261
|
Self
|
|
235
262
|
Parsed connector instance.
|
|
236
|
-
|
|
237
|
-
Raises
|
|
238
|
-
------
|
|
239
|
-
TypeError
|
|
240
|
-
If ``name`` is missing or invalid.
|
|
241
263
|
"""
|
|
242
|
-
name = obj
|
|
243
|
-
if not isinstance(name, str):
|
|
244
|
-
raise TypeError('ConnectorDb requires a "name" (str)')
|
|
264
|
+
name = _require_name(obj, kind='Db')
|
|
245
265
|
|
|
246
266
|
return cls(
|
|
247
267
|
name=name,
|
|
@@ -307,15 +327,8 @@ class ConnectorFile:
|
|
|
307
327
|
-------
|
|
308
328
|
Self
|
|
309
329
|
Parsed connector instance.
|
|
310
|
-
|
|
311
|
-
Raises
|
|
312
|
-
------
|
|
313
|
-
TypeError
|
|
314
|
-
If ``name`` is missing or invalid.
|
|
315
330
|
"""
|
|
316
|
-
name = obj
|
|
317
|
-
if not isinstance(name, str):
|
|
318
|
-
raise TypeError('ConnectorFile requires a "name" (str)')
|
|
331
|
+
name = _require_name(obj, kind='File')
|
|
319
332
|
|
|
320
333
|
return cls(
|
|
321
334
|
name=name,
|
etlplus/workflow/jobs.py
CHANGED
|
@@ -19,6 +19,7 @@ from dataclasses import field
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
from typing import Self
|
|
21
21
|
|
|
22
|
+
from ..types import StrAnyMap
|
|
22
23
|
from ..utils import coerce_dict
|
|
23
24
|
from ..utils import maybe_mapping
|
|
24
25
|
|
|
@@ -35,6 +36,75 @@ __all__ = [
|
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _coerce_optional_str(value: Any) -> str | None:
|
|
43
|
+
"""
|
|
44
|
+
Normalize optional string values, coercing non-strings when needed.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
value : Any
|
|
49
|
+
Optional value to normalize.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
str | None
|
|
54
|
+
``None`` when ``value`` is ``None``; otherwise a string value.
|
|
55
|
+
"""
|
|
56
|
+
if value is None:
|
|
57
|
+
return None
|
|
58
|
+
return value if isinstance(value, str) else str(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_depends_on(
|
|
62
|
+
value: Any,
|
|
63
|
+
) -> list[str]:
|
|
64
|
+
"""
|
|
65
|
+
Normalize dependency declarations into a string list.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
value : Any
|
|
70
|
+
Input dependency specification (string or list of strings).
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
list[str]
|
|
75
|
+
Normalized dependency list.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(value, str):
|
|
78
|
+
return [value]
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
return [entry for entry in value if isinstance(entry, str)]
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _require_str(
|
|
85
|
+
# data: dict[str, Any],
|
|
86
|
+
data: StrAnyMap,
|
|
87
|
+
key: str,
|
|
88
|
+
) -> str | None:
|
|
89
|
+
"""
|
|
90
|
+
Extract a required string field from a mapping.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
data : StrAnyMap
|
|
95
|
+
Mapping containing the target field.
|
|
96
|
+
key : str
|
|
97
|
+
Field name to extract.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
str | None
|
|
102
|
+
The string value when present and valid; otherwise ``None``.
|
|
103
|
+
"""
|
|
104
|
+
value = data.get(key)
|
|
105
|
+
return value if isinstance(value, str) else None
|
|
106
|
+
|
|
107
|
+
|
|
38
108
|
# SECTION: DATA CLASSES ===================================================== #
|
|
39
109
|
|
|
40
110
|
|
|
@@ -79,8 +149,8 @@ class ExtractRef:
|
|
|
79
149
|
data = maybe_mapping(obj)
|
|
80
150
|
if not data:
|
|
81
151
|
return None
|
|
82
|
-
source = data
|
|
83
|
-
if
|
|
152
|
+
source = _require_str(data, 'source')
|
|
153
|
+
if source is None:
|
|
84
154
|
return None
|
|
85
155
|
return cls(
|
|
86
156
|
source=source,
|
|
@@ -144,22 +214,13 @@ class JobConfig:
|
|
|
144
214
|
data = maybe_mapping(obj)
|
|
145
215
|
if not data:
|
|
146
216
|
return None
|
|
147
|
-
name = data
|
|
148
|
-
if
|
|
217
|
+
name = _require_str(data, 'name')
|
|
218
|
+
if name is None:
|
|
149
219
|
return None
|
|
150
220
|
|
|
151
|
-
description = data.get('description')
|
|
152
|
-
if description is not None and not isinstance(description, str):
|
|
153
|
-
description = str(description)
|
|
221
|
+
description = _coerce_optional_str(data.get('description'))
|
|
154
222
|
|
|
155
|
-
|
|
156
|
-
depends_on: list[str] = []
|
|
157
|
-
if isinstance(depends_raw, str):
|
|
158
|
-
depends_on = [depends_raw]
|
|
159
|
-
elif isinstance(depends_raw, list):
|
|
160
|
-
for entry in depends_raw:
|
|
161
|
-
if isinstance(entry, str):
|
|
162
|
-
depends_on.append(entry)
|
|
223
|
+
depends_on = _parse_depends_on(data.get('depends_on'))
|
|
163
224
|
|
|
164
225
|
return cls(
|
|
165
226
|
name=name,
|
|
@@ -213,8 +274,8 @@ class LoadRef:
|
|
|
213
274
|
data = maybe_mapping(obj)
|
|
214
275
|
if not data:
|
|
215
276
|
return None
|
|
216
|
-
target = data
|
|
217
|
-
if
|
|
277
|
+
target = _require_str(data, 'target')
|
|
278
|
+
if target is None:
|
|
218
279
|
return None
|
|
219
280
|
return cls(
|
|
220
281
|
target=target,
|
|
@@ -260,8 +321,8 @@ class TransformRef:
|
|
|
260
321
|
data = maybe_mapping(obj)
|
|
261
322
|
if not data:
|
|
262
323
|
return None
|
|
263
|
-
pipeline = data
|
|
264
|
-
if
|
|
324
|
+
pipeline = _require_str(data, 'pipeline')
|
|
325
|
+
if pipeline is None:
|
|
265
326
|
return None
|
|
266
327
|
return cls(pipeline=pipeline)
|
|
267
328
|
|
|
@@ -311,15 +372,11 @@ class ValidationRef:
|
|
|
311
372
|
data = maybe_mapping(obj)
|
|
312
373
|
if not data:
|
|
313
374
|
return None
|
|
314
|
-
ruleset = data
|
|
315
|
-
if
|
|
375
|
+
ruleset = _require_str(data, 'ruleset')
|
|
376
|
+
if ruleset is None:
|
|
316
377
|
return None
|
|
317
|
-
severity = data.get('severity')
|
|
318
|
-
|
|
319
|
-
severity = str(severity)
|
|
320
|
-
phase = data.get('phase')
|
|
321
|
-
if phase is not None and not isinstance(phase, str):
|
|
322
|
-
phase = str(phase)
|
|
378
|
+
severity = _coerce_optional_str(data.get('severity'))
|
|
379
|
+
phase = _coerce_optional_str(data.get('phase'))
|
|
323
380
|
return cls(
|
|
324
381
|
ruleset=ruleset,
|
|
325
382
|
severity=severity,
|