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.
Files changed (55) hide show
  1. etlplus/__init__.py +43 -0
  2. etlplus/__main__.py +22 -0
  3. etlplus/__version__.py +14 -0
  4. etlplus/api/README.md +237 -0
  5. etlplus/api/__init__.py +136 -0
  6. etlplus/api/auth.py +432 -0
  7. etlplus/api/config.py +633 -0
  8. etlplus/api/endpoint_client.py +885 -0
  9. etlplus/api/errors.py +170 -0
  10. etlplus/api/pagination/__init__.py +47 -0
  11. etlplus/api/pagination/client.py +188 -0
  12. etlplus/api/pagination/config.py +440 -0
  13. etlplus/api/pagination/paginator.py +775 -0
  14. etlplus/api/rate_limiting/__init__.py +38 -0
  15. etlplus/api/rate_limiting/config.py +343 -0
  16. etlplus/api/rate_limiting/rate_limiter.py +266 -0
  17. etlplus/api/request_manager.py +589 -0
  18. etlplus/api/retry_manager.py +430 -0
  19. etlplus/api/transport.py +325 -0
  20. etlplus/api/types.py +172 -0
  21. etlplus/cli/__init__.py +15 -0
  22. etlplus/cli/app.py +1367 -0
  23. etlplus/cli/handlers.py +775 -0
  24. etlplus/cli/main.py +616 -0
  25. etlplus/config/__init__.py +56 -0
  26. etlplus/config/connector.py +372 -0
  27. etlplus/config/jobs.py +311 -0
  28. etlplus/config/pipeline.py +339 -0
  29. etlplus/config/profile.py +78 -0
  30. etlplus/config/types.py +204 -0
  31. etlplus/config/utils.py +120 -0
  32. etlplus/ddl.py +197 -0
  33. etlplus/enums.py +414 -0
  34. etlplus/extract.py +218 -0
  35. etlplus/file.py +657 -0
  36. etlplus/load.py +336 -0
  37. etlplus/mixins.py +62 -0
  38. etlplus/py.typed +0 -0
  39. etlplus/run.py +368 -0
  40. etlplus/run_helpers.py +843 -0
  41. etlplus/templates/__init__.py +5 -0
  42. etlplus/templates/ddl.sql.j2 +128 -0
  43. etlplus/templates/view.sql.j2 +69 -0
  44. etlplus/transform.py +1049 -0
  45. etlplus/types.py +227 -0
  46. etlplus/utils.py +638 -0
  47. etlplus/validate.py +493 -0
  48. etlplus/validation/__init__.py +44 -0
  49. etlplus/validation/utils.py +389 -0
  50. etlplus-0.5.4.dist-info/METADATA +616 -0
  51. etlplus-0.5.4.dist-info/RECORD +55 -0
  52. etlplus-0.5.4.dist-info/WHEEL +5 -0
  53. etlplus-0.5.4.dist-info/entry_points.txt +2 -0
  54. etlplus-0.5.4.dist-info/licenses/LICENSE +21 -0
  55. 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