etlplus 0.5.4__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/__init__.py +43 -0
- etlplus/__main__.py +22 -0
- etlplus/__version__.py +14 -0
- etlplus/api/README.md +237 -0
- etlplus/api/__init__.py +136 -0
- etlplus/api/auth.py +432 -0
- etlplus/api/config.py +633 -0
- etlplus/api/endpoint_client.py +885 -0
- etlplus/api/errors.py +170 -0
- etlplus/api/pagination/__init__.py +47 -0
- etlplus/api/pagination/client.py +188 -0
- etlplus/api/pagination/config.py +440 -0
- etlplus/api/pagination/paginator.py +775 -0
- etlplus/api/rate_limiting/__init__.py +38 -0
- etlplus/api/rate_limiting/config.py +343 -0
- etlplus/api/rate_limiting/rate_limiter.py +266 -0
- etlplus/api/request_manager.py +589 -0
- etlplus/api/retry_manager.py +430 -0
- etlplus/api/transport.py +325 -0
- etlplus/api/types.py +172 -0
- etlplus/cli/__init__.py +15 -0
- etlplus/cli/app.py +1367 -0
- etlplus/cli/handlers.py +775 -0
- etlplus/cli/main.py +616 -0
- etlplus/config/__init__.py +56 -0
- etlplus/config/connector.py +372 -0
- etlplus/config/jobs.py +311 -0
- etlplus/config/pipeline.py +339 -0
- etlplus/config/profile.py +78 -0
- etlplus/config/types.py +204 -0
- etlplus/config/utils.py +120 -0
- etlplus/ddl.py +197 -0
- etlplus/enums.py +414 -0
- etlplus/extract.py +218 -0
- etlplus/file.py +657 -0
- etlplus/load.py +336 -0
- etlplus/mixins.py +62 -0
- etlplus/py.typed +0 -0
- etlplus/run.py +368 -0
- etlplus/run_helpers.py +843 -0
- etlplus/templates/__init__.py +5 -0
- etlplus/templates/ddl.sql.j2 +128 -0
- etlplus/templates/view.sql.j2 +69 -0
- etlplus/transform.py +1049 -0
- etlplus/types.py +227 -0
- etlplus/utils.py +638 -0
- etlplus/validate.py +493 -0
- etlplus/validation/__init__.py +44 -0
- etlplus/validation/utils.py +389 -0
- etlplus-0.5.4.dist-info/METADATA +616 -0
- etlplus-0.5.4.dist-info/RECORD +55 -0
- etlplus-0.5.4.dist-info/WHEEL +5 -0
- etlplus-0.5.4.dist-info/entry_points.txt +2 -0
- etlplus-0.5.4.dist-info/licenses/LICENSE +21 -0
- etlplus-0.5.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.validation.utils` module.
|
|
3
|
+
|
|
4
|
+
Utility helpers for conditional validation orchestration.
|
|
5
|
+
|
|
6
|
+
The helpers defined here embrace a "high cohesion, low coupling" design by
|
|
7
|
+
isolating normalization, configuration, and logging responsibilities. The
|
|
8
|
+
resulting surface keeps ``maybe_validate`` focused on orchestration while
|
|
9
|
+
offloading ancillary concerns to composable helpers.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
from typing import Literal
|
|
19
|
+
from typing import Self
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
from ..types import StrAnyMap
|
|
23
|
+
from ..utils import normalized_str
|
|
24
|
+
|
|
25
|
+
# SECTION: TYPED DICTIONARIES =============================================== #
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationResult(TypedDict, total=False):
|
|
29
|
+
"""Shape returned by ``validate_fn`` callables."""
|
|
30
|
+
|
|
31
|
+
valid: bool
|
|
32
|
+
data: Any
|
|
33
|
+
errors: Any
|
|
34
|
+
field_errors: Any
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# SECTION: TYPE ALIASES ===================================================== #
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
type Ruleset = StrAnyMap
|
|
41
|
+
|
|
42
|
+
type ValidationPhase = Literal['before_transform', 'after_transform']
|
|
43
|
+
type ValidationWindow = Literal['before_transform', 'after_transform', 'both']
|
|
44
|
+
type ValidationSeverity = Literal['warn', 'error']
|
|
45
|
+
|
|
46
|
+
type ValidateFn = Callable[[Any, Ruleset], ValidationResult]
|
|
47
|
+
type PrintFn = Callable[[Any], None]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# SECTION: DATA CLASSES ===================================================== #
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True, frozen=True)
|
|
54
|
+
class ValidationSettings:
|
|
55
|
+
"""
|
|
56
|
+
Normalized validation configuration.
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
enabled : bool
|
|
61
|
+
Global flag to toggle validation.
|
|
62
|
+
rules : Ruleset | None
|
|
63
|
+
Validation rules to apply. ``None`` or empty mappings short-circuit.
|
|
64
|
+
phase : ValidationPhase
|
|
65
|
+
Current pipeline phase requesting validation. Accepts
|
|
66
|
+
``"before_transform"`` or ``"after_transform"``.
|
|
67
|
+
window : ValidationWindow
|
|
68
|
+
Configured validation window. Accepts ``"before_transform"``,
|
|
69
|
+
``"after_transform"``, or ``"both"``.
|
|
70
|
+
severity : ValidationSeverity
|
|
71
|
+
Failure severity (``"warn"`` or ``"error"``).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# -- Attributes -- #
|
|
75
|
+
|
|
76
|
+
enabled: bool
|
|
77
|
+
rules: Ruleset | None
|
|
78
|
+
phase: ValidationPhase
|
|
79
|
+
window: ValidationWindow
|
|
80
|
+
severity: ValidationSeverity
|
|
81
|
+
|
|
82
|
+
# -- Class Methods -- #
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_raw(
|
|
86
|
+
cls,
|
|
87
|
+
*,
|
|
88
|
+
enabled: bool,
|
|
89
|
+
rules: Ruleset | None,
|
|
90
|
+
phase: str | None,
|
|
91
|
+
window: str | None,
|
|
92
|
+
severity: str | None,
|
|
93
|
+
) -> Self:
|
|
94
|
+
"""
|
|
95
|
+
Construct a settings object from untrusted user configuration.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
enabled : bool
|
|
100
|
+
Global flag to toggle validation.
|
|
101
|
+
rules : Ruleset | None
|
|
102
|
+
Validation rules to apply. ``None`` or empty mappings
|
|
103
|
+
short-circuit.
|
|
104
|
+
phase : str | None
|
|
105
|
+
Current pipeline phase requesting validation. Accepts
|
|
106
|
+
``"before_transform"`` or ``"after_transform"``.
|
|
107
|
+
window : str | None
|
|
108
|
+
Configured validation window. Accepts ``"before_transform"``,
|
|
109
|
+
``"after_transform"``, or ``"both"``.
|
|
110
|
+
severity : str | None
|
|
111
|
+
Failure severity (``"warn"`` or ``"error"``).
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
Self
|
|
116
|
+
Normalized settings object.
|
|
117
|
+
"""
|
|
118
|
+
return cls(
|
|
119
|
+
enabled=bool(enabled),
|
|
120
|
+
rules=rules if rules else None,
|
|
121
|
+
phase=_normalize_phase(phase),
|
|
122
|
+
window=_normalize_window(window),
|
|
123
|
+
severity=_normalize_severity(severity),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# -- Instance Methods -- #
|
|
127
|
+
|
|
128
|
+
def should_run(self) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Return ``True`` when validation should execute for the phase.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
bool
|
|
135
|
+
``True`` when validation should execute for the phase.
|
|
136
|
+
"""
|
|
137
|
+
if not self.enabled or not self.rules:
|
|
138
|
+
return False
|
|
139
|
+
return _should_validate(self.window, self.phase)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# SECTION: FUNCTIONS ======================================================== #
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def maybe_validate(
|
|
146
|
+
payload: Any,
|
|
147
|
+
when: str,
|
|
148
|
+
*,
|
|
149
|
+
enabled: bool,
|
|
150
|
+
rules: Ruleset | None,
|
|
151
|
+
phase: str,
|
|
152
|
+
severity: str,
|
|
153
|
+
validate_fn: ValidateFn,
|
|
154
|
+
print_json_fn: PrintFn,
|
|
155
|
+
) -> Any:
|
|
156
|
+
"""
|
|
157
|
+
Run validation based on declarative configuration.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
payload : Any
|
|
162
|
+
Arbitrary payload supplied by the pipeline stage.
|
|
163
|
+
when : str
|
|
164
|
+
Desired validation window. Accepts ``"before_transform"``,
|
|
165
|
+
``"after_transform"``, or ``"both"``.
|
|
166
|
+
enabled : bool
|
|
167
|
+
Global flag to toggle validation.
|
|
168
|
+
rules : Ruleset | None
|
|
169
|
+
Validation rules to apply. ``None`` or empty mappings short-circuit.
|
|
170
|
+
phase : str
|
|
171
|
+
Current pipeline phase requesting validation.
|
|
172
|
+
severity : str
|
|
173
|
+
Failure severity (``"warn"`` or ``"error"``).
|
|
174
|
+
validate_fn : ValidateFn
|
|
175
|
+
Engine that performs validation and returns a
|
|
176
|
+
:class:`ValidationResult` instance.
|
|
177
|
+
print_json_fn : PrintFn
|
|
178
|
+
Structured logger invoked when validation fails.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
Any
|
|
183
|
+
``payload`` when validation is skipped or when severity is ``"warn"``
|
|
184
|
+
and the validation fails. Returns the validator ``data`` payload when
|
|
185
|
+
validation succeeds.
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
------
|
|
189
|
+
ValueError
|
|
190
|
+
Raised when validation fails and ``severity`` is ``"error"``.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
--------
|
|
194
|
+
>>> maybe_validate(
|
|
195
|
+
... {'valid': True},
|
|
196
|
+
... when='both',
|
|
197
|
+
... enabled=True,
|
|
198
|
+
... rules={'required': ['valid']},
|
|
199
|
+
... phase='before_transform',
|
|
200
|
+
... severity='warn',
|
|
201
|
+
... validate_fn=lambda payload, rules: {
|
|
202
|
+
... 'valid': True,
|
|
203
|
+
... 'data': payload,
|
|
204
|
+
... },
|
|
205
|
+
... print_json_fn=lambda payload: payload,
|
|
206
|
+
... )
|
|
207
|
+
{'valid': True}
|
|
208
|
+
"""
|
|
209
|
+
settings = ValidationSettings.from_raw(
|
|
210
|
+
enabled=enabled,
|
|
211
|
+
rules=rules,
|
|
212
|
+
phase=phase,
|
|
213
|
+
window=when,
|
|
214
|
+
severity=severity,
|
|
215
|
+
)
|
|
216
|
+
if not settings.should_run():
|
|
217
|
+
return payload
|
|
218
|
+
|
|
219
|
+
ruleset = settings.rules
|
|
220
|
+
assert ruleset is not None # Guarded by should_run()
|
|
221
|
+
|
|
222
|
+
result = validate_fn(payload, ruleset)
|
|
223
|
+
if result.get('valid', False):
|
|
224
|
+
return result.get('data', payload)
|
|
225
|
+
|
|
226
|
+
_log_failure(
|
|
227
|
+
print_json_fn,
|
|
228
|
+
phase=settings.phase,
|
|
229
|
+
window=settings.window,
|
|
230
|
+
ruleset_name=_rule_name(ruleset),
|
|
231
|
+
result=result,
|
|
232
|
+
)
|
|
233
|
+
if settings.severity == 'warn':
|
|
234
|
+
return payload
|
|
235
|
+
|
|
236
|
+
raise ValueError('Validation failed')
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# SECTION: INTERNAL FUNCTIONS ============================================== #
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _log_failure(
|
|
243
|
+
printer: PrintFn,
|
|
244
|
+
*,
|
|
245
|
+
phase: ValidationPhase,
|
|
246
|
+
window: ValidationWindow,
|
|
247
|
+
ruleset_name: str | None,
|
|
248
|
+
result: ValidationResult,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Emit a structured message describing the failed validation.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
printer : PrintFn
|
|
256
|
+
Structured logger invoked when validation fails.
|
|
257
|
+
phase : ValidationPhase
|
|
258
|
+
Current pipeline phase requesting validation.
|
|
259
|
+
window : ValidationWindow
|
|
260
|
+
Configured validation window.
|
|
261
|
+
ruleset_name : str | None
|
|
262
|
+
Name of the validation ruleset.
|
|
263
|
+
result : ValidationResult
|
|
264
|
+
Result of the failed validation.
|
|
265
|
+
"""
|
|
266
|
+
printer(
|
|
267
|
+
{
|
|
268
|
+
'status': 'validation_failed',
|
|
269
|
+
'phase': phase,
|
|
270
|
+
'when': window,
|
|
271
|
+
'ruleset': ruleset_name,
|
|
272
|
+
'result': result,
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _normalize_phase(
|
|
278
|
+
value: str | None,
|
|
279
|
+
) -> ValidationPhase:
|
|
280
|
+
"""
|
|
281
|
+
Normalize arbitrary text into a known validation phase.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
value : str | None
|
|
286
|
+
Untrusted text to normalize.
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
ValidationPhase
|
|
291
|
+
Normalized validation phase. Defaults to ``"before_transform"`` when
|
|
292
|
+
unspecified.
|
|
293
|
+
"""
|
|
294
|
+
match normalized_str(value):
|
|
295
|
+
case 'after_transform':
|
|
296
|
+
return 'after_transform'
|
|
297
|
+
case _:
|
|
298
|
+
return 'before_transform'
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _normalize_severity(
|
|
302
|
+
value: str | None,
|
|
303
|
+
) -> ValidationSeverity:
|
|
304
|
+
"""
|
|
305
|
+
Normalize severity, defaulting to ``"error"`` when unspecified.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
value : str | None
|
|
310
|
+
Untrusted text to normalize.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
ValidationSeverity
|
|
315
|
+
Normalized severity. Defaults to ``"error"`` when unspecified.
|
|
316
|
+
"""
|
|
317
|
+
return 'warn' if normalized_str(value) == 'warn' else 'error'
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _normalize_window(
|
|
321
|
+
value: str | None,
|
|
322
|
+
) -> ValidationWindow:
|
|
323
|
+
"""
|
|
324
|
+
Normalize the configured validation window.
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
value : str | None
|
|
329
|
+
Untrusted text to normalize.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
ValidationWindow
|
|
334
|
+
Normalized validation window. Defaults to ``"both"`` when unspecified.
|
|
335
|
+
"""
|
|
336
|
+
match normalized_str(value):
|
|
337
|
+
case 'before_transform':
|
|
338
|
+
return 'before_transform'
|
|
339
|
+
case 'after_transform':
|
|
340
|
+
return 'after_transform'
|
|
341
|
+
case _:
|
|
342
|
+
return 'both'
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _rule_name(
|
|
346
|
+
rules: Ruleset,
|
|
347
|
+
) -> str | None:
|
|
348
|
+
"""
|
|
349
|
+
Best-effort extraction of a ruleset identifier.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
rules : Ruleset
|
|
354
|
+
Untrusted ruleset configuration.
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
str | None
|
|
359
|
+
Name of the ruleset when available. Returns ``None`` when the ruleset
|
|
360
|
+
lacks a name or when the ruleset is not a mapping.
|
|
361
|
+
"""
|
|
362
|
+
getter = getattr(rules, 'get', None)
|
|
363
|
+
if callable(getter):
|
|
364
|
+
return getter('name')
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _should_validate(
|
|
369
|
+
window: ValidationWindow,
|
|
370
|
+
phase: ValidationPhase,
|
|
371
|
+
) -> bool:
|
|
372
|
+
"""
|
|
373
|
+
Return ``True`` when the validation window matches the phase.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
window : ValidationWindow
|
|
378
|
+
Configured validation window. Accepts ``"before_transform"``,
|
|
379
|
+
``"after_transform"``, or ``"both"``.
|
|
380
|
+
phase : ValidationPhase
|
|
381
|
+
Current pipeline phase requesting validation. Accepts
|
|
382
|
+
``"before_transform"`` or ``"after_transform"``.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
bool
|
|
387
|
+
``True`` when the validation window matches the phase.
|
|
388
|
+
"""
|
|
389
|
+
return window == 'both' or window == phase
|