etlplus 0.9.2__py3-none-any.whl → 0.10.1__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 (120) hide show
  1. etlplus/__init__.py +26 -1
  2. etlplus/api/README.md +3 -51
  3. etlplus/api/__init__.py +0 -10
  4. etlplus/api/config.py +28 -39
  5. etlplus/api/endpoint_client.py +3 -3
  6. etlplus/api/pagination/client.py +1 -1
  7. etlplus/api/rate_limiting/config.py +1 -13
  8. etlplus/api/rate_limiting/rate_limiter.py +11 -8
  9. etlplus/api/request_manager.py +6 -11
  10. etlplus/api/transport.py +2 -14
  11. etlplus/api/types.py +6 -96
  12. etlplus/cli/commands.py +43 -76
  13. etlplus/cli/constants.py +1 -1
  14. etlplus/cli/handlers.py +12 -40
  15. etlplus/cli/io.py +2 -2
  16. etlplus/cli/main.py +1 -1
  17. etlplus/cli/state.py +7 -4
  18. etlplus/{workflow → config}/__init__.py +23 -10
  19. etlplus/{workflow → config}/connector.py +44 -58
  20. etlplus/{workflow → config}/jobs.py +32 -105
  21. etlplus/{workflow → config}/pipeline.py +51 -59
  22. etlplus/{workflow → config}/profile.py +5 -8
  23. etlplus/config/types.py +204 -0
  24. etlplus/config/utils.py +120 -0
  25. etlplus/database/ddl.py +1 -1
  26. etlplus/database/engine.py +3 -19
  27. etlplus/database/orm.py +0 -2
  28. etlplus/database/schema.py +1 -1
  29. etlplus/enums.py +266 -0
  30. etlplus/{ops/extract.py → extract.py} +99 -81
  31. etlplus/file.py +652 -0
  32. etlplus/{ops/load.py → load.py} +101 -78
  33. etlplus/{ops/run.py → run.py} +127 -159
  34. etlplus/{api/utils.py → run_helpers.py} +153 -209
  35. etlplus/{ops/transform.py → transform.py} +68 -75
  36. etlplus/types.py +4 -5
  37. etlplus/utils.py +2 -136
  38. etlplus/{ops/validate.py → validate.py} +12 -22
  39. etlplus/validation/__init__.py +44 -0
  40. etlplus/{ops → validation}/utils.py +17 -53
  41. {etlplus-0.9.2.dist-info → etlplus-0.10.1.dist-info}/METADATA +17 -210
  42. etlplus-0.10.1.dist-info/RECORD +65 -0
  43. {etlplus-0.9.2.dist-info → etlplus-0.10.1.dist-info}/WHEEL +1 -1
  44. etlplus/README.md +0 -37
  45. etlplus/api/enums.py +0 -51
  46. etlplus/cli/README.md +0 -40
  47. etlplus/database/README.md +0 -48
  48. etlplus/file/README.md +0 -105
  49. etlplus/file/__init__.py +0 -25
  50. etlplus/file/_imports.py +0 -141
  51. etlplus/file/_io.py +0 -160
  52. etlplus/file/accdb.py +0 -78
  53. etlplus/file/arrow.py +0 -78
  54. etlplus/file/avro.py +0 -176
  55. etlplus/file/bson.py +0 -77
  56. etlplus/file/cbor.py +0 -78
  57. etlplus/file/cfg.py +0 -79
  58. etlplus/file/conf.py +0 -80
  59. etlplus/file/core.py +0 -322
  60. etlplus/file/csv.py +0 -79
  61. etlplus/file/dat.py +0 -78
  62. etlplus/file/dta.py +0 -77
  63. etlplus/file/duckdb.py +0 -78
  64. etlplus/file/enums.py +0 -343
  65. etlplus/file/feather.py +0 -111
  66. etlplus/file/fwf.py +0 -77
  67. etlplus/file/gz.py +0 -123
  68. etlplus/file/hbs.py +0 -78
  69. etlplus/file/hdf5.py +0 -78
  70. etlplus/file/ini.py +0 -79
  71. etlplus/file/ion.py +0 -78
  72. etlplus/file/jinja2.py +0 -78
  73. etlplus/file/json.py +0 -98
  74. etlplus/file/log.py +0 -78
  75. etlplus/file/mat.py +0 -78
  76. etlplus/file/mdb.py +0 -78
  77. etlplus/file/msgpack.py +0 -78
  78. etlplus/file/mustache.py +0 -78
  79. etlplus/file/nc.py +0 -78
  80. etlplus/file/ndjson.py +0 -108
  81. etlplus/file/numbers.py +0 -75
  82. etlplus/file/ods.py +0 -79
  83. etlplus/file/orc.py +0 -111
  84. etlplus/file/parquet.py +0 -113
  85. etlplus/file/pb.py +0 -78
  86. etlplus/file/pbf.py +0 -77
  87. etlplus/file/properties.py +0 -78
  88. etlplus/file/proto.py +0 -77
  89. etlplus/file/psv.py +0 -79
  90. etlplus/file/rda.py +0 -78
  91. etlplus/file/rds.py +0 -78
  92. etlplus/file/sas7bdat.py +0 -78
  93. etlplus/file/sav.py +0 -77
  94. etlplus/file/sqlite.py +0 -78
  95. etlplus/file/stub.py +0 -84
  96. etlplus/file/sylk.py +0 -77
  97. etlplus/file/tab.py +0 -81
  98. etlplus/file/toml.py +0 -78
  99. etlplus/file/tsv.py +0 -80
  100. etlplus/file/txt.py +0 -102
  101. etlplus/file/vm.py +0 -78
  102. etlplus/file/wks.py +0 -77
  103. etlplus/file/xls.py +0 -88
  104. etlplus/file/xlsm.py +0 -79
  105. etlplus/file/xlsx.py +0 -99
  106. etlplus/file/xml.py +0 -185
  107. etlplus/file/xpt.py +0 -78
  108. etlplus/file/yaml.py +0 -95
  109. etlplus/file/zip.py +0 -175
  110. etlplus/file/zsav.py +0 -77
  111. etlplus/ops/README.md +0 -50
  112. etlplus/ops/__init__.py +0 -61
  113. etlplus/templates/README.md +0 -46
  114. etlplus/workflow/README.md +0 -52
  115. etlplus/workflow/dag.py +0 -105
  116. etlplus/workflow/types.py +0 -115
  117. etlplus-0.9.2.dist-info/RECORD +0 -134
  118. {etlplus-0.9.2.dist-info → etlplus-0.10.1.dist-info}/entry_points.txt +0 -0
  119. {etlplus-0.9.2.dist-info → etlplus-0.10.1.dist-info}/licenses/LICENSE +0 -0
  120. {etlplus-0.9.2.dist-info → etlplus-0.10.1.dist-info}/top_level.txt +0 -0
etlplus/__init__.py CHANGED
@@ -2,17 +2,42 @@
2
2
  :mod:`etlplus` package.
3
3
 
4
4
  Top-level facade for the ETLPlus toolkit.
5
+
6
+ Importing :mod:`etlplus` exposes the handful of coarse-grained helpers most
7
+ users care about: ``extract``, ``transform``, ``load``, ``validate``, and
8
+ ``run``. Each helper delegates to the richer modules under ``etlplus.*`` while
9
+ presenting a compact public API surface.
10
+
11
+ Examples
12
+ --------
13
+ >>> from etlplus import extract, transform
14
+ >>> raw = extract('file', 'input.json')
15
+ >>> curated = transform(raw, {'select': ['id', 'name']})
16
+
17
+ See Also
18
+ --------
19
+ - :mod:`etlplus.cli` for the command-line interface
20
+ - :mod:`etlplus.run` for orchestrating pipeline jobs
5
21
  """
6
22
 
7
23
  from .__version__ import __version__
8
24
 
9
25
  __author__ = 'ETLPlus Team'
10
26
 
27
+ from .extract import extract
28
+ from .load import load
29
+ from .run import run
30
+ from .transform import transform
31
+ from .validate import validate
11
32
 
12
33
  # SECTION: EXPORTS ========================================================== #
13
34
 
14
35
 
15
36
  __all__ = [
16
- '__author__',
17
37
  '__version__',
38
+ 'extract',
39
+ 'load',
40
+ 'run',
41
+ 'transform',
42
+ 'validate',
18
43
  ]
etlplus/api/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # `etlplus.api` Subpackage
1
+ # etlplus.api module.
2
2
 
3
- Documentation for the `etlplus.api` subpackage: a lightweight HTTP client and helpers for paginated
4
- REST endpoints.
3
+ Focused documentation for the `etlplus.api` subpackage: a lightweight HTTP client and helpers for
4
+ paginated REST endpoints.
5
5
 
6
6
  - Provides a small `EndpointClient` for calling JSON APIs
7
7
  - Supports page-, offset-, and cursor-based pagination via `PaginationConfig`
@@ -12,21 +12,6 @@ REST endpoints.
12
12
 
13
13
  Back to project overview: see the top-level [README](../../README.md).
14
14
 
15
- - [`etlplus.api` Subpackage](#etlplusapi-subpackage)
16
- - [Installation](#installation)
17
- - [Quickstart](#quickstart)
18
- - [Overriding Rate Limits Per Call](#overriding-rate-limits-per-call)
19
- - [Choosing `records_path` and `cursor_path`](#choosing-records_path-and-cursor_path)
20
- - [Cursor-Based Pagination Example](#cursor-based-pagination-example)
21
- - [Offset-based pagination example](#offset-based-pagination-example)
22
- - [Authentication](#authentication)
23
- - [Errors and Rate Limiting](#errors-and-rate-limiting)
24
- - [Types and Transport](#types-and-transport)
25
- - [Config Schemas](#config-schemas)
26
- - [Supporting Modules](#supporting-modules)
27
- - [Minimal Contract](#minimal-contract)
28
- - [See also](#see-also)
29
-
30
15
  ## Installation
31
16
 
32
17
  `etlplus.api` ships as part of the `etlplus` package. Install the package as usual:
@@ -226,36 +211,6 @@ providers can fall back to their own defaults. If you already possess a static t
226
211
  `etlplus/api/request_manager.py` wraps `requests` sessions plus retry orchestration. Advanced
227
212
  users may consult those modules to adapt behavior.
228
213
 
229
- ## Config Schemas
230
-
231
- `etlplus.api.types` defines TypedDict-based configuration shapes for API profiles and endpoints.
232
- Runtime parsing remains permissive in `etlplus.api.config`, but these types improve IDE
233
- autocomplete and static analysis.
234
-
235
- Exported types:
236
-
237
- - `ApiConfigMap`: top-level API config shape
238
- - `ApiProfileConfigMap`: per-profile API config shape
239
- - `ApiProfileDefaultsMap`: defaults block within a profile
240
- - `EndpointMap`: endpoint config shape
241
-
242
- Example:
243
-
244
- ```python
245
- from etlplus.api import ApiConfigMap
246
-
247
- api_cfg: ApiConfigMap = {
248
- "base_url": "https://example.test",
249
- "headers": {"Authorization": "Bearer token"},
250
- "endpoints": {
251
- "users": {
252
- "path": "/users",
253
- "method": "GET",
254
- },
255
- },
256
- }
257
- ```
258
-
259
214
  ## Supporting Modules
260
215
 
261
216
  - `etlplus.api.types` collects friendly aliases such as `Headers`, `Params`, `Url`, and
@@ -278,6 +233,3 @@ api_cfg: ApiConfigMap = {
278
233
  ## See also
279
234
 
280
235
  - Top-level CLI and library usage in the main [README](../../README.md)
281
-
282
-
283
- [def]: #installation
etlplus/api/__init__.py CHANGED
@@ -78,7 +78,6 @@ from .config import ApiConfig
78
78
  from .config import ApiProfileConfig
79
79
  from .config import EndpointConfig
80
80
  from .endpoint_client import EndpointClient
81
- from .enums import HttpMethod
82
81
  from .pagination import CursorPaginationConfigMap
83
82
  from .pagination import PagePaginationConfigMap
84
83
  from .pagination import PaginationClient
@@ -99,10 +98,6 @@ from .types import Headers
99
98
  from .types import Params
100
99
  from .types import RequestOptions
101
100
  from .types import Url
102
- from .utils import compose_api_request_env
103
- from .utils import compose_api_target_env
104
- from .utils import paginate_with_client
105
- from .utils import resolve_request
106
101
 
107
102
  # SECTION: EXPORTS ========================================================== #
108
103
 
@@ -124,14 +119,9 @@ __all__ = [
124
119
  'RequestOptions',
125
120
  'RetryStrategy',
126
121
  # Enums
127
- 'HttpMethod',
128
122
  'PaginationType',
129
123
  # Functions
130
124
  'build_http_adapter',
131
- 'compose_api_request_env',
132
- 'compose_api_target_env',
133
- 'paginate_with_client',
134
- 'resolve_request',
135
125
  # Type Aliases
136
126
  'CursorPaginationConfigMap',
137
127
  'Headers',
etlplus/api/config.py CHANGED
@@ -3,6 +3,11 @@
3
3
 
4
4
  Configuration dataclasses for REST API services, profiles, and endpoints.
5
5
 
6
+ These models used to live under :mod:`etlplus.config`, but they belong in the
7
+ API layer because they compose runtime types such as
8
+ :class:`etlplus.api.EndpointClient`, :class:`etlplus.api.PaginationConfig`, and
9
+ :class:`etlplus.api.RateLimitConfig`.
10
+
6
11
  Notes
7
12
  -----
8
13
  - TypedDict references remain editor hints only; :meth:`from_obj` accepts
@@ -13,7 +18,6 @@ Notes
13
18
 
14
19
  from __future__ import annotations
15
20
 
16
- from collections.abc import Callable
17
21
  from collections.abc import Mapping
18
22
  from dataclasses import dataclass
19
23
  from dataclasses import field
@@ -25,20 +29,20 @@ from typing import overload
25
29
  from urllib.parse import urlsplit
26
30
  from urllib.parse import urlunsplit
27
31
 
32
+ from ..enums import HttpMethod
28
33
  from ..types import StrAnyMap
29
34
  from ..types import StrStrMap
30
35
  from ..utils import cast_str_dict
31
36
  from ..utils import coerce_dict
32
37
  from ..utils import maybe_mapping
33
38
  from .endpoint_client import EndpointClient
34
- from .enums import HttpMethod
35
39
  from .pagination import PaginationConfig
36
40
  from .rate_limiting import RateLimitConfig
37
41
 
38
42
  if TYPE_CHECKING:
39
- from .types import ApiConfigMap
40
- from .types import ApiProfileConfigMap
41
- from .types import EndpointMap
43
+ from ..config.types import ApiConfigMap
44
+ from ..config.types import ApiProfileConfigMap
45
+ from ..config.types import EndpointMap
42
46
 
43
47
 
44
48
  # SECTION: EXPORTS ========================================================== #
@@ -102,33 +106,6 @@ def _effective_service_defaults(
102
106
  return fallback_base, fallback_headers
103
107
 
104
108
 
105
- def _freeze_mapping(
106
- mapping: Mapping[Any, Any],
107
- *,
108
- key_cast: Callable[[Any], Any] | None = None,
109
- ) -> MappingProxyType:
110
- """
111
- Return an immutable copy of a mapping, optionally normalizing keys.
112
-
113
- Parameters
114
- ----------
115
- mapping : Mapping[Any, Any]
116
- Source mapping to freeze.
117
- key_cast : Callable[[Any], Any] | None, optional
118
- Optional key coercion applied to each key.
119
-
120
- Returns
121
- -------
122
- MappingProxyType
123
- Read-only mapping proxy with normalized keys.
124
- """
125
- if key_cast is None:
126
- data = dict(mapping)
127
- else:
128
- data = {key_cast(key): value for key, value in mapping.items()}
129
- return MappingProxyType(data)
130
-
131
-
132
109
  def _normalize_method(
133
110
  value: Any,
134
111
  ) -> Any | None:
@@ -255,8 +232,16 @@ class ApiProfileConfig:
255
232
  # -- Magic Methods (Object Lifecycle) -- #
256
233
 
257
234
  def __post_init__(self) -> None:
258
- object.__setattr__(self, 'headers', _freeze_mapping(self.headers))
259
- object.__setattr__(self, 'auth', _freeze_mapping(self.auth))
235
+ object.__setattr__(
236
+ self,
237
+ 'headers',
238
+ MappingProxyType(dict(self.headers)),
239
+ )
240
+ object.__setattr__(
241
+ self,
242
+ 'auth',
243
+ MappingProxyType(dict(self.auth)),
244
+ )
260
245
 
261
246
  # -- Class Methods -- #
262
247
 
@@ -355,16 +340,20 @@ class ApiConfig:
355
340
  # -- Magic Methods (Object Lifecycle) -- #
356
341
 
357
342
  def __post_init__(self) -> None:
358
- object.__setattr__(self, 'headers', _freeze_mapping(self.headers))
343
+ object.__setattr__(
344
+ self,
345
+ 'headers',
346
+ MappingProxyType(dict(self.headers)),
347
+ )
359
348
  object.__setattr__(
360
349
  self,
361
350
  'endpoints',
362
- _freeze_mapping(self.endpoints, key_cast=str),
351
+ MappingProxyType({str(k): v for k, v in self.endpoints.items()}),
363
352
  )
364
353
  object.__setattr__(
365
354
  self,
366
355
  'profiles',
367
- _freeze_mapping(self.profiles, key_cast=str),
356
+ MappingProxyType({str(k): v for k, v in self.profiles.items()}),
368
357
  )
369
358
 
370
359
  # -- Internal Instance Methods -- #
@@ -556,12 +545,12 @@ class EndpointConfig:
556
545
  object.__setattr__(
557
546
  self,
558
547
  'path_params',
559
- _freeze_mapping(self.path_params),
548
+ MappingProxyType(dict(self.path_params)),
560
549
  )
561
550
  object.__setattr__(
562
551
  self,
563
552
  'query_params',
564
- _freeze_mapping(self.query_params),
553
+ MappingProxyType(dict(self.query_params)),
565
554
  )
566
555
 
567
556
  # -- Class Methods -- #
@@ -455,7 +455,7 @@ class EndpointClient:
455
455
  -------
456
456
  JSONData
457
457
  Parsed JSON payload or fallback structure matching
458
- :func:`etlplus.ops.extract.extract_from_api` semantics.
458
+ :func:`etlplus.extract.extract_from_api` semantics.
459
459
  """
460
460
  return self._request_manager.get(url, **kwargs)
461
461
 
@@ -479,7 +479,7 @@ class EndpointClient:
479
479
  -------
480
480
  JSONData
481
481
  Parsed JSON payload or fallback structure matching
482
- :func:`etlplus.ops.extract.extract_from_api` semantics.
482
+ :func:`etlplus.extract.extract_from_api` semantics.
483
483
  """
484
484
  return self._request_manager.post(url, **kwargs)
485
485
 
@@ -506,7 +506,7 @@ class EndpointClient:
506
506
  -------
507
507
  JSONData
508
508
  Parsed JSON payload or fallback structure matching
509
- :func:`etlplus.ops.extract.extract_from_api` semantics.
509
+ :func:`etlplus.extract.extract_from_api` semantics.
510
510
  """
511
511
  return self._request_manager.request(method, url, **kwargs)
512
512
 
@@ -1,5 +1,5 @@
1
1
  """
2
- :mod:`etlplus.api.pagination.client` module.
2
+ :mod:`etlplus.api.client` module.
3
3
 
4
4
  Client-facing pagination driver for REST API responses.
5
5
 
@@ -1,5 +1,5 @@
1
1
  """
2
- :mod:`etlplus.api.rate_limiting.config` module.
2
+ :mod:`etlplus.api.rate_limiting.rate_limiter` module.
3
3
 
4
4
  Rate limiting configuration primitives.
5
5
 
@@ -268,18 +268,6 @@ class RateLimitConfig(BoundsWarningsMixin):
268
268
  ) -> Self:
269
269
  """
270
270
  Normalize rate-limit config and overrides into a single instance.
271
-
272
- Parameters
273
- ----------
274
- rate_limit : StrAnyMap | RateLimitConfig | None, optional
275
- Base rate-limit configuration to normalize.
276
- overrides : RateLimitOverrides, optional
277
- Override values that take precedence over ``rate_limit``.
278
-
279
- Returns
280
- -------
281
- Self
282
- Normalized rate-limit configuration.
283
271
  """
284
272
  normalized = _coerce_rate_limit_map(rate_limit)
285
273
  cfg = _merge_rate_limit(normalized, overrides)
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
 
21
21
  import time
22
22
  from dataclasses import dataclass
23
- from typing import Self
24
23
 
25
24
  from ...utils import to_float
26
25
  from ...utils import to_positive_float
@@ -144,13 +143,13 @@ class RateLimiter:
144
143
  # -- Class Methods -- #
145
144
 
146
145
  @classmethod
147
- def disabled(cls) -> Self:
146
+ def disabled(cls) -> RateLimiter:
148
147
  """
149
148
  Create a limiter that never sleeps.
150
149
 
151
150
  Returns
152
151
  -------
153
- Self
152
+ RateLimiter
154
153
  Instance with rate limiting disabled.
155
154
  """
156
155
  return cls(sleep_seconds=0.0)
@@ -159,7 +158,7 @@ class RateLimiter:
159
158
  def fixed(
160
159
  cls,
161
160
  seconds: float,
162
- ) -> Self:
161
+ ) -> RateLimiter:
163
162
  """
164
163
  Create a limiter with a fixed non-negative delay.
165
164
 
@@ -171,7 +170,7 @@ class RateLimiter:
171
170
 
172
171
  Returns
173
172
  -------
174
- Self
173
+ RateLimiter
175
174
  Instance with the specified delay.
176
175
  """
177
176
  value = to_float(seconds, 0.0, minimum=0.0) or 0.0
@@ -182,7 +181,7 @@ class RateLimiter:
182
181
  def from_config(
183
182
  cls,
184
183
  cfg: RateLimitInput,
185
- ) -> Self:
184
+ ) -> RateLimiter:
186
185
  """
187
186
  Build a :class:`RateLimiter` from a configuration mapping.
188
187
 
@@ -202,10 +201,12 @@ class RateLimiter:
202
201
 
203
202
  Returns
204
203
  -------
205
- Self
204
+ RateLimiter
206
205
  Instance with normalized ``sleep_seconds`` and ``max_per_sec``.
207
206
  """
208
207
  config = RateLimitConfig.from_inputs(rate_limit=cfg)
208
+ if config is None:
209
+ return cls.disabled()
209
210
 
210
211
  # RateLimiter.__post_init__ will normalize and enforce invariants.
211
212
  return cls(**config.as_mapping())
@@ -260,4 +261,6 @@ class RateLimiter:
260
261
  rate_limit=rate_limit,
261
262
  overrides=overrides,
262
263
  )
263
- return float(config.sleep_seconds) if config.sleep_seconds else 0.0
264
+ if config is None or not config.sleep_seconds:
265
+ return 0.0
266
+ return float(config.sleep_seconds)
@@ -14,7 +14,6 @@ from collections.abc import Sequence
14
14
  from dataclasses import dataclass
15
15
  from dataclasses import field
16
16
  from functools import partial
17
- from types import TracebackType
18
17
  from typing import Any
19
18
  from typing import cast
20
19
 
@@ -138,7 +137,7 @@ class RequestManager:
138
137
  self,
139
138
  exc_type: type[BaseException] | None,
140
139
  exc: BaseException | None,
141
- tb: TracebackType | None,
140
+ tb: Any,
142
141
  ) -> None:
143
142
  """
144
143
  Exit the runtime context and close owned sessions.
@@ -149,7 +148,7 @@ class RequestManager:
149
148
  Exception type if raised, else ``None``.
150
149
  exc : BaseException | None
151
150
  Exception instance if raised, else ``None``.
152
- tb : TracebackType | None
151
+ tb : Any
153
152
  Traceback if an exception was raised, else ``None``.
154
153
  """
155
154
  if self._ctx_session is None:
@@ -276,7 +275,7 @@ class RequestManager:
276
275
 
277
276
  try:
278
277
  policy = self.retry
279
- if policy is None:
278
+ if not policy:
280
279
  try:
281
280
  return fetch(url, **call_kwargs)
282
281
  except requests.RequestException as exc: # pragma: no cover
@@ -439,13 +438,9 @@ class RequestManager:
439
438
  if isinstance(payload, dict):
440
439
  return cast(JSONDict, payload)
441
440
  if isinstance(payload, list):
442
- out: list[JSONDict] = []
443
- for item in payload:
444
- if isinstance(item, dict):
445
- out.append(cast(JSONDict, item))
446
- else:
447
- out.append({'value': item})
448
- return cast(JSONData, out)
441
+ if all(isinstance(item, dict) for item in payload):
442
+ return cast(JSONData, payload)
443
+ return [{'value': item} for item in payload]
449
444
  return {'value': payload}
450
445
  return {
451
446
  'content': response.text,
etlplus/api/transport.py CHANGED
@@ -191,19 +191,7 @@ def _build_retry_value(
191
191
  def _normalize_retry_kwargs(
192
192
  retries_cfg: Mapping[str, Any],
193
193
  ) -> dict[str, Any]:
194
- """
195
- Filter and normalize urllib3 ``Retry`` kwargs from a mapping.
196
-
197
- Parameters
198
- ----------
199
- retries_cfg : Mapping[str, Any]
200
- Raw retry configuration mapping.
201
-
202
- Returns
203
- -------
204
- dict[str, Any]
205
- Filtered and normalized keyword arguments for ``Retry``.
206
- """
194
+ """Filter and normalize urllib3 ``Retry`` kwargs from a mapping."""
207
195
  allowed_keys = {
208
196
  'total',
209
197
  'connect',
@@ -251,7 +239,7 @@ def _resolve_max_retries(
251
239
  """
252
240
  match retries_cfg:
253
241
  case int():
254
- return to_maximum_int(retries_cfg, 0)
242
+ return retries_cfg
255
243
  case Mapping():
256
244
  try:
257
245
  return _build_retry_value(retries_cfg)
etlplus/api/types.py CHANGED
@@ -20,11 +20,8 @@ Examples
20
20
  from __future__ import annotations
21
21
 
22
22
  from collections.abc import Callable
23
- from collections.abc import Mapping
24
23
  from dataclasses import dataclass
25
24
  from typing import Any
26
- from typing import Self
27
- from typing import TypedDict
28
25
  from typing import cast
29
26
 
30
27
  from ..types import JSONData
@@ -42,11 +39,6 @@ __all__ = [
42
39
  'Headers',
43
40
  'Params',
44
41
  'Url',
45
- # Typed Dicts
46
- 'ApiConfigMap',
47
- 'ApiProfileConfigMap',
48
- 'ApiProfileDefaultsMap',
49
- 'EndpointMap',
50
42
  ]
51
43
 
52
44
 
@@ -56,88 +48,6 @@ __all__ = [
56
48
  _UNSET = object()
57
49
 
58
50
 
59
- # SECTION: TYPED DICTS ====================================================== #
60
-
61
-
62
- class ApiConfigMap(TypedDict, total=False):
63
- """
64
- Top-level API config shape parsed by ApiConfig.from_obj.
65
-
66
- Either provide a ``base_url`` with optional ``headers`` and ``endpoints``,
67
- or provide ``profiles`` with at least one profile having a ``base_url``.
68
-
69
- See Also
70
- --------
71
- - :class:`etlplus.api.config.ApiConfig`
72
- """
73
-
74
- base_url: str
75
- headers: StrAnyMap
76
- endpoints: Mapping[str, EndpointMap | str]
77
- profiles: Mapping[str, ApiProfileConfigMap]
78
-
79
-
80
- class ApiProfileConfigMap(TypedDict, total=False):
81
- """
82
- Shape accepted for a profile entry under ApiConfigMap.profiles.
83
-
84
- Notes
85
- -----
86
- ``base_url`` is required at runtime when profiles are provided.
87
-
88
- See Also
89
- --------
90
- - :class:`etlplus.api.config.ApiProfileConfig`
91
- """
92
-
93
- base_url: str
94
- headers: StrAnyMap
95
- base_path: str
96
- auth: StrAnyMap
97
- defaults: ApiProfileDefaultsMap
98
-
99
-
100
- class ApiProfileDefaultsMap(TypedDict, total=False):
101
- """
102
- Defaults block available under a profile (all keys optional).
103
-
104
- Notes
105
- -----
106
- Runtime expects header values to be str; typing remains permissive.
107
-
108
- See Also
109
- --------
110
- - :class:`etlplus.api.config.ApiProfileConfig`
111
- - :class:`etlplus.api.pagination.PaginationConfig`
112
- - :class:`etlplus.api.rate_limiting.RateLimitConfig`
113
- """
114
-
115
- headers: StrAnyMap
116
- pagination: Any
117
- rate_limit: Any
118
-
119
-
120
- class EndpointMap(TypedDict, total=False):
121
- """
122
- Shape accepted by EndpointConfig.from_obj.
123
-
124
- One of ``path`` or ``url`` should be provided.
125
-
126
- See Also
127
- --------
128
- - :class:`etlplus.api.config.EndpointConfig`
129
- """
130
-
131
- path: str
132
- url: str
133
- method: str
134
- path_params: StrAnyMap
135
- query_params: StrAnyMap
136
- body: Any
137
- pagination: Any
138
- rate_limit: Any
139
-
140
-
141
51
  # SECTION: DATA CLASSES ===================================================== #
142
52
 
143
53
 
@@ -165,9 +75,9 @@ class RequestOptions:
165
75
  # -- Magic Methods (Object Lifecycle) -- #
166
76
 
167
77
  def __post_init__(self) -> None:
168
- if self.params is not None:
78
+ if self.params:
169
79
  object.__setattr__(self, 'params', dict(self.params))
170
- if self.headers is not None:
80
+ if self.headers:
171
81
  object.__setattr__(self, 'headers', dict(self.headers))
172
82
 
173
83
  # -- Instance Methods -- #
@@ -182,9 +92,9 @@ class RequestOptions:
182
92
  Keyword arguments for ``requests`` methods.
183
93
  """
184
94
  kw: dict[str, Any] = {}
185
- if self.params is not None:
95
+ if self.params:
186
96
  kw['params'] = dict(self.params)
187
- if self.headers is not None:
97
+ if self.headers:
188
98
  kw['headers'] = dict(self.headers)
189
99
  if self.timeout is not None:
190
100
  kw['timeout'] = self.timeout
@@ -196,7 +106,7 @@ class RequestOptions:
196
106
  params: Params | None | object = _UNSET,
197
107
  headers: Headers | None | object = _UNSET,
198
108
  timeout: float | None | object = _UNSET,
199
- ) -> Self:
109
+ ) -> RequestOptions:
200
110
  """
201
111
  Return a copy with the provided fields replaced.
202
112
 
@@ -236,7 +146,7 @@ class RequestOptions:
236
146
  else:
237
147
  next_timeout = cast(float | None, timeout)
238
148
 
239
- return self.__class__(
149
+ return RequestOptions(
240
150
  params=next_params,
241
151
  headers=next_headers,
242
152
  timeout=next_timeout,