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.
Files changed (118) hide show
  1. etlplus/README.md +3 -3
  2. etlplus/api/README.md +31 -0
  3. etlplus/api/auth.py +1 -1
  4. etlplus/api/config.py +5 -10
  5. etlplus/api/endpoint_client.py +4 -4
  6. etlplus/api/pagination/config.py +1 -1
  7. etlplus/api/pagination/paginator.py +6 -7
  8. etlplus/api/rate_limiting/config.py +4 -4
  9. etlplus/api/rate_limiting/rate_limiter.py +1 -1
  10. etlplus/api/retry_manager.py +2 -2
  11. etlplus/api/transport.py +1 -1
  12. etlplus/api/types.py +99 -0
  13. etlplus/api/utils.py +1 -1
  14. etlplus/cli/commands.py +75 -42
  15. etlplus/cli/constants.py +1 -1
  16. etlplus/cli/handlers.py +31 -13
  17. etlplus/cli/io.py +2 -2
  18. etlplus/cli/main.py +2 -2
  19. etlplus/cli/state.py +4 -7
  20. etlplus/connector/__init__.py +43 -0
  21. etlplus/connector/api.py +161 -0
  22. etlplus/connector/connector.py +26 -0
  23. etlplus/connector/core.py +132 -0
  24. etlplus/connector/database.py +122 -0
  25. etlplus/connector/enums.py +52 -0
  26. etlplus/connector/file.py +120 -0
  27. etlplus/connector/types.py +40 -0
  28. etlplus/connector/utils.py +122 -0
  29. etlplus/database/ddl.py +2 -2
  30. etlplus/database/engine.py +19 -3
  31. etlplus/database/orm.py +2 -0
  32. etlplus/enums.py +1 -33
  33. etlplus/file/_imports.py +1 -0
  34. etlplus/file/_io.py +52 -4
  35. etlplus/file/accdb.py +3 -2
  36. etlplus/file/arrow.py +3 -2
  37. etlplus/file/avro.py +3 -2
  38. etlplus/file/bson.py +3 -2
  39. etlplus/file/cbor.py +3 -2
  40. etlplus/file/cfg.py +3 -2
  41. etlplus/file/conf.py +3 -2
  42. etlplus/file/core.py +11 -8
  43. etlplus/file/csv.py +3 -2
  44. etlplus/file/dat.py +3 -2
  45. etlplus/file/dta.py +3 -2
  46. etlplus/file/duckdb.py +3 -2
  47. etlplus/file/enums.py +1 -1
  48. etlplus/file/feather.py +3 -2
  49. etlplus/file/fwf.py +3 -2
  50. etlplus/file/gz.py +3 -2
  51. etlplus/file/hbs.py +3 -2
  52. etlplus/file/hdf5.py +3 -2
  53. etlplus/file/ini.py +3 -2
  54. etlplus/file/ion.py +3 -2
  55. etlplus/file/jinja2.py +3 -2
  56. etlplus/file/json.py +5 -16
  57. etlplus/file/log.py +3 -2
  58. etlplus/file/mat.py +3 -2
  59. etlplus/file/mdb.py +3 -2
  60. etlplus/file/msgpack.py +3 -2
  61. etlplus/file/mustache.py +3 -2
  62. etlplus/file/nc.py +3 -2
  63. etlplus/file/ndjson.py +3 -2
  64. etlplus/file/numbers.py +3 -2
  65. etlplus/file/ods.py +3 -2
  66. etlplus/file/orc.py +3 -2
  67. etlplus/file/parquet.py +3 -2
  68. etlplus/file/pb.py +3 -2
  69. etlplus/file/pbf.py +3 -2
  70. etlplus/file/properties.py +3 -2
  71. etlplus/file/proto.py +3 -2
  72. etlplus/file/psv.py +3 -2
  73. etlplus/file/rda.py +3 -2
  74. etlplus/file/rds.py +3 -2
  75. etlplus/file/sas7bdat.py +3 -2
  76. etlplus/file/sav.py +3 -2
  77. etlplus/file/sqlite.py +3 -2
  78. etlplus/file/stub.py +1 -0
  79. etlplus/file/sylk.py +3 -2
  80. etlplus/file/tab.py +3 -2
  81. etlplus/file/toml.py +3 -2
  82. etlplus/file/tsv.py +3 -2
  83. etlplus/file/txt.py +4 -3
  84. etlplus/file/vm.py +3 -2
  85. etlplus/file/wks.py +3 -2
  86. etlplus/file/xls.py +3 -2
  87. etlplus/file/xlsm.py +3 -2
  88. etlplus/file/xlsx.py +3 -2
  89. etlplus/file/xml.py +9 -3
  90. etlplus/file/xpt.py +3 -2
  91. etlplus/file/yaml.py +5 -16
  92. etlplus/file/zip.py +3 -2
  93. etlplus/file/zsav.py +3 -2
  94. etlplus/ops/extract.py +13 -1
  95. etlplus/ops/load.py +15 -2
  96. etlplus/ops/run.py +4 -4
  97. etlplus/ops/transform.py +2 -2
  98. etlplus/ops/utils.py +6 -35
  99. etlplus/ops/validate.py +3 -3
  100. etlplus/types.py +3 -2
  101. etlplus/utils.py +163 -29
  102. etlplus/workflow/__init__.py +0 -11
  103. etlplus/workflow/jobs.py +84 -27
  104. etlplus/workflow/pipeline.py +48 -48
  105. {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/METADATA +4 -4
  106. etlplus-0.16.0.dist-info/RECORD +141 -0
  107. {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/WHEEL +1 -1
  108. etlplus/config/README.md +0 -50
  109. etlplus/config/__init__.py +0 -33
  110. etlplus/config/types.py +0 -140
  111. etlplus/dag.py +0 -103
  112. etlplus/workflow/connector.py +0 -373
  113. etlplus/workflow/types.py +0 -115
  114. etlplus/workflow/utils.py +0 -120
  115. etlplus-0.15.0.dist-info/RECORD +0 -139
  116. {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/entry_points.txt +0 -0
  117. {etlplus-0.15.0.dist-info → etlplus-0.16.0.dist-info}/licenses/LICENSE +0 -0
  118. {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 ..enums import DataConnectorType
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 ``raw`` text.
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 ..enums import DataConnectorType
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 ``config_path`` to override this.
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``, ``source`` is assumed
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
- ``source_type`` is ``None``.
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 ``present``.
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 ``present``.
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 normalized_str
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
- ``payload`` when validation is skipped or when severity is ``"warn"``
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 ``severity`` is ``"error"``.
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
- _normalize_choice(
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
- _normalize_choice(
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
- _normalize_choice(
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 ``load_data``, ``validate_field``,
15
- ``validate``.
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: CLASSES ========================================================== #
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.config.types` for TypedDict surfaces
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
- 'normalized_str',
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 ``value`` is mapping-like.
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 ``value`` converted to a standard ``dict``.
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 ``value`` when it is mapping-like; otherwise ``None``.
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 ``obj`` as UTF-8 JSON without ASCII escaping.
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 ``value`` to a float with optional fallback and bounds.
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 ``default`` is provided.
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 ``default`` and ``value`` after float coercion.
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
- ``default`` if coercion fails; else ``max(coerced, default)``.
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 ``default`` and ``value`` after float coercion.
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
- ``default`` if coercion fails; else ``min(coerced, default)``.
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 ``value`` to an integer with optional fallback and bounds.
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 ``default`` is provided.
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 ``default`` and ``value`` after integer coercion.
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
- ``default`` if coercion fails; else ``max(coerced, default)``.
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 ``default`` and ``value`` after integer coercion.
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
- ``default`` if coercion fails; else ``min(coerced, default)``.
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 ``minimum`` when needed.
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 ``minimum``.
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 ``minimum``.
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 ``value`` to a ``float`` using the internal float coercer.
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 normalized_str(
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 ``value`` constrained to the interval ``[minimum, maximum]``.
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 ``candidate`` is integral.
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 ``candidate``; else ``None`` if not integral.
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 ``value`` with ``coercer`` and optionally clamp it.
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 ``minimum`` does not exceed ``maximum``.
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 ``value`` if not ``None``; else ``default``.
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
- ``value`` or ``default``.
739
+ *value* or *default*.
606
740
  """
607
741
  return default if value is None else value
@@ -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
  ]