etlplus 0.9.2__py3-none-any.whl → 0.10.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/__init__.py +26 -1
- etlplus/api/README.md +3 -51
- etlplus/api/__init__.py +0 -10
- etlplus/api/config.py +28 -39
- etlplus/api/endpoint_client.py +3 -3
- etlplus/api/pagination/client.py +1 -1
- etlplus/api/rate_limiting/config.py +1 -13
- etlplus/api/rate_limiting/rate_limiter.py +11 -8
- etlplus/api/request_manager.py +6 -11
- etlplus/api/transport.py +2 -14
- etlplus/api/types.py +6 -96
- etlplus/cli/commands.py +43 -76
- etlplus/cli/constants.py +1 -1
- etlplus/cli/handlers.py +12 -40
- etlplus/cli/io.py +2 -2
- etlplus/cli/main.py +1 -1
- etlplus/cli/state.py +7 -4
- etlplus/{workflow → config}/__init__.py +23 -10
- etlplus/{workflow → config}/connector.py +44 -58
- etlplus/{workflow → config}/jobs.py +32 -105
- etlplus/{workflow → config}/pipeline.py +51 -59
- etlplus/{workflow → config}/profile.py +5 -8
- etlplus/config/types.py +204 -0
- etlplus/config/utils.py +120 -0
- etlplus/database/ddl.py +1 -1
- etlplus/database/engine.py +3 -19
- etlplus/database/orm.py +0 -2
- etlplus/database/schema.py +1 -1
- etlplus/enums.py +288 -0
- etlplus/{ops/extract.py → extract.py} +99 -81
- etlplus/file.py +652 -0
- etlplus/{ops/load.py → load.py} +101 -78
- etlplus/{ops/run.py → run.py} +127 -159
- etlplus/{api/utils.py → run_helpers.py} +153 -209
- etlplus/{ops/transform.py → transform.py} +68 -75
- etlplus/types.py +4 -5
- etlplus/utils.py +2 -136
- etlplus/{ops/validate.py → validate.py} +12 -22
- etlplus/validation/__init__.py +44 -0
- etlplus/{ops → validation}/utils.py +17 -53
- {etlplus-0.9.2.dist-info → etlplus-0.10.2.dist-info}/METADATA +17 -210
- etlplus-0.10.2.dist-info/RECORD +65 -0
- {etlplus-0.9.2.dist-info → etlplus-0.10.2.dist-info}/WHEEL +1 -1
- etlplus/README.md +0 -37
- etlplus/api/enums.py +0 -51
- etlplus/cli/README.md +0 -40
- etlplus/database/README.md +0 -48
- etlplus/file/README.md +0 -105
- etlplus/file/__init__.py +0 -25
- etlplus/file/_imports.py +0 -141
- etlplus/file/_io.py +0 -160
- etlplus/file/accdb.py +0 -78
- etlplus/file/arrow.py +0 -78
- etlplus/file/avro.py +0 -176
- etlplus/file/bson.py +0 -77
- etlplus/file/cbor.py +0 -78
- etlplus/file/cfg.py +0 -79
- etlplus/file/conf.py +0 -80
- etlplus/file/core.py +0 -322
- etlplus/file/csv.py +0 -79
- etlplus/file/dat.py +0 -78
- etlplus/file/dta.py +0 -77
- etlplus/file/duckdb.py +0 -78
- etlplus/file/enums.py +0 -343
- etlplus/file/feather.py +0 -111
- etlplus/file/fwf.py +0 -77
- etlplus/file/gz.py +0 -123
- etlplus/file/hbs.py +0 -78
- etlplus/file/hdf5.py +0 -78
- etlplus/file/ini.py +0 -79
- etlplus/file/ion.py +0 -78
- etlplus/file/jinja2.py +0 -78
- etlplus/file/json.py +0 -98
- etlplus/file/log.py +0 -78
- etlplus/file/mat.py +0 -78
- etlplus/file/mdb.py +0 -78
- etlplus/file/msgpack.py +0 -78
- etlplus/file/mustache.py +0 -78
- etlplus/file/nc.py +0 -78
- etlplus/file/ndjson.py +0 -108
- etlplus/file/numbers.py +0 -75
- etlplus/file/ods.py +0 -79
- etlplus/file/orc.py +0 -111
- etlplus/file/parquet.py +0 -113
- etlplus/file/pb.py +0 -78
- etlplus/file/pbf.py +0 -77
- etlplus/file/properties.py +0 -78
- etlplus/file/proto.py +0 -77
- etlplus/file/psv.py +0 -79
- etlplus/file/rda.py +0 -78
- etlplus/file/rds.py +0 -78
- etlplus/file/sas7bdat.py +0 -78
- etlplus/file/sav.py +0 -77
- etlplus/file/sqlite.py +0 -78
- etlplus/file/stub.py +0 -84
- etlplus/file/sylk.py +0 -77
- etlplus/file/tab.py +0 -81
- etlplus/file/toml.py +0 -78
- etlplus/file/tsv.py +0 -80
- etlplus/file/txt.py +0 -102
- etlplus/file/vm.py +0 -78
- etlplus/file/wks.py +0 -77
- etlplus/file/xls.py +0 -88
- etlplus/file/xlsm.py +0 -79
- etlplus/file/xlsx.py +0 -99
- etlplus/file/xml.py +0 -185
- etlplus/file/xpt.py +0 -78
- etlplus/file/yaml.py +0 -95
- etlplus/file/zip.py +0 -175
- etlplus/file/zsav.py +0 -77
- etlplus/ops/README.md +0 -50
- etlplus/ops/__init__.py +0 -61
- etlplus/templates/README.md +0 -46
- etlplus/workflow/README.md +0 -52
- etlplus/workflow/dag.py +0 -105
- etlplus/workflow/types.py +0 -115
- etlplus-0.9.2.dist-info/RECORD +0 -134
- {etlplus-0.9.2.dist-info → etlplus-0.10.2.dist-info}/entry_points.txt +0 -0
- {etlplus-0.9.2.dist-info → etlplus-0.10.2.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.9.2.dist-info → etlplus-0.10.2.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
|
-
#
|
|
1
|
+
# etlplus.api module.
|
|
2
2
|
|
|
3
|
-
|
|
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__(
|
|
259
|
-
|
|
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__(
|
|
343
|
+
object.__setattr__(
|
|
344
|
+
self,
|
|
345
|
+
'headers',
|
|
346
|
+
MappingProxyType(dict(self.headers)),
|
|
347
|
+
)
|
|
359
348
|
object.__setattr__(
|
|
360
349
|
self,
|
|
361
350
|
'endpoints',
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
+
MappingProxyType(dict(self.path_params)),
|
|
560
549
|
)
|
|
561
550
|
object.__setattr__(
|
|
562
551
|
self,
|
|
563
552
|
'query_params',
|
|
564
|
-
|
|
553
|
+
MappingProxyType(dict(self.query_params)),
|
|
565
554
|
)
|
|
566
555
|
|
|
567
556
|
# -- Class Methods -- #
|
etlplus/api/endpoint_client.py
CHANGED
|
@@ -455,7 +455,7 @@ class EndpointClient:
|
|
|
455
455
|
-------
|
|
456
456
|
JSONData
|
|
457
457
|
Parsed JSON payload or fallback structure matching
|
|
458
|
-
:func:`etlplus.
|
|
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.
|
|
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.
|
|
509
|
+
:func:`etlplus.extract.extract_from_api` semantics.
|
|
510
510
|
"""
|
|
511
511
|
return self._request_manager.request(method, url, **kwargs)
|
|
512
512
|
|
etlplus/api/pagination/client.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
:mod:`etlplus.api.rate_limiting.
|
|
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) ->
|
|
146
|
+
def disabled(cls) -> RateLimiter:
|
|
148
147
|
"""
|
|
149
148
|
Create a limiter that never sleeps.
|
|
150
149
|
|
|
151
150
|
Returns
|
|
152
151
|
-------
|
|
153
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
|
|
264
|
+
if config is None or not config.sleep_seconds:
|
|
265
|
+
return 0.0
|
|
266
|
+
return float(config.sleep_seconds)
|
etlplus/api/request_manager.py
CHANGED
|
@@ -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:
|
|
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 :
|
|
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
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
|
78
|
+
if self.params:
|
|
169
79
|
object.__setattr__(self, 'params', dict(self.params))
|
|
170
|
-
if self.headers
|
|
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
|
|
95
|
+
if self.params:
|
|
186
96
|
kw['params'] = dict(self.params)
|
|
187
|
-
if self.headers
|
|
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
|
-
) ->
|
|
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
|
|
149
|
+
return RequestOptions(
|
|
240
150
|
params=next_params,
|
|
241
151
|
headers=next_headers,
|
|
242
152
|
timeout=next_timeout,
|