etlplus 0.5.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- etlplus/__init__.py +43 -0
- etlplus/__main__.py +22 -0
- etlplus/__version__.py +14 -0
- etlplus/api/README.md +237 -0
- etlplus/api/__init__.py +136 -0
- etlplus/api/auth.py +432 -0
- etlplus/api/config.py +633 -0
- etlplus/api/endpoint_client.py +885 -0
- etlplus/api/errors.py +170 -0
- etlplus/api/pagination/__init__.py +47 -0
- etlplus/api/pagination/client.py +188 -0
- etlplus/api/pagination/config.py +440 -0
- etlplus/api/pagination/paginator.py +775 -0
- etlplus/api/rate_limiting/__init__.py +38 -0
- etlplus/api/rate_limiting/config.py +343 -0
- etlplus/api/rate_limiting/rate_limiter.py +266 -0
- etlplus/api/request_manager.py +589 -0
- etlplus/api/retry_manager.py +430 -0
- etlplus/api/transport.py +325 -0
- etlplus/api/types.py +172 -0
- etlplus/cli/__init__.py +15 -0
- etlplus/cli/app.py +1367 -0
- etlplus/cli/handlers.py +775 -0
- etlplus/cli/main.py +616 -0
- etlplus/config/__init__.py +56 -0
- etlplus/config/connector.py +372 -0
- etlplus/config/jobs.py +311 -0
- etlplus/config/pipeline.py +339 -0
- etlplus/config/profile.py +78 -0
- etlplus/config/types.py +204 -0
- etlplus/config/utils.py +120 -0
- etlplus/ddl.py +197 -0
- etlplus/enums.py +414 -0
- etlplus/extract.py +218 -0
- etlplus/file.py +657 -0
- etlplus/load.py +336 -0
- etlplus/mixins.py +62 -0
- etlplus/py.typed +0 -0
- etlplus/run.py +368 -0
- etlplus/run_helpers.py +843 -0
- etlplus/templates/__init__.py +5 -0
- etlplus/templates/ddl.sql.j2 +128 -0
- etlplus/templates/view.sql.j2 +69 -0
- etlplus/transform.py +1049 -0
- etlplus/types.py +227 -0
- etlplus/utils.py +638 -0
- etlplus/validate.py +493 -0
- etlplus/validation/__init__.py +44 -0
- etlplus/validation/utils.py +389 -0
- etlplus-0.5.4.dist-info/METADATA +616 -0
- etlplus-0.5.4.dist-info/RECORD +55 -0
- etlplus-0.5.4.dist-info/WHEEL +5 -0
- etlplus-0.5.4.dist-info/entry_points.txt +2 -0
- etlplus-0.5.4.dist-info/licenses/LICENSE +21 -0
- etlplus-0.5.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.api.pagination.config` module.
|
|
3
|
+
|
|
4
|
+
Pagination configuration shapes for REST API pagination.
|
|
5
|
+
|
|
6
|
+
This module defines the configuration schema for pagination strategies used
|
|
7
|
+
by :mod:`etlplus.api.pagination`. It exposes:
|
|
8
|
+
|
|
9
|
+
- :class:`PaginationType` – enumeration of supported pagination modes.
|
|
10
|
+
- :class:`PaginationConfig` – normalized configuration container.
|
|
11
|
+
- ``*PaginationConfigMap`` TypedDicts – loose, user-facing config mappings.
|
|
12
|
+
|
|
13
|
+
Notes
|
|
14
|
+
-----
|
|
15
|
+
- TypedDict shapes are editor hints; runtime parsing remains permissive
|
|
16
|
+
(``from_obj`` accepts any :class:`collections.abc.Mapping`).
|
|
17
|
+
- Numeric fields are normalized with tolerant casts; ``validate_bounds``
|
|
18
|
+
returns warnings instead of raising.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Any
|
|
26
|
+
from typing import Literal
|
|
27
|
+
from typing import Required
|
|
28
|
+
from typing import Self
|
|
29
|
+
from typing import TypedDict
|
|
30
|
+
from typing import overload
|
|
31
|
+
|
|
32
|
+
from ...enums import CoercibleStrEnum
|
|
33
|
+
from ...mixins import BoundsWarningsMixin
|
|
34
|
+
from ...types import StrAnyMap
|
|
35
|
+
from ...utils import maybe_mapping
|
|
36
|
+
from ...utils import to_int
|
|
37
|
+
|
|
38
|
+
# SECTION: EXPORTS ========================================================== #
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Data Classes
|
|
43
|
+
'PaginationConfig',
|
|
44
|
+
# Enums
|
|
45
|
+
'PaginationType',
|
|
46
|
+
# Type Aliases
|
|
47
|
+
'PaginationConfigMap',
|
|
48
|
+
'PaginationInput',
|
|
49
|
+
# Typed Dicts
|
|
50
|
+
'CursorPaginationConfigMap',
|
|
51
|
+
'PagePaginationConfigMap',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# SECTION: CONSTANTS ======================================================== #
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_MISSING = object()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# SECTION: ENUMS ============================================================ #
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PaginationType(CoercibleStrEnum):
|
|
65
|
+
"""Enumeration of supported pagination types for REST API responses."""
|
|
66
|
+
|
|
67
|
+
# -- Constants -- #
|
|
68
|
+
|
|
69
|
+
PAGE = 'page'
|
|
70
|
+
OFFSET = 'offset'
|
|
71
|
+
CURSOR = 'cursor'
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CursorPaginationConfigMap(TypedDict, total=False):
|
|
78
|
+
"""
|
|
79
|
+
Configuration mapping for cursor-based REST API response pagination.
|
|
80
|
+
|
|
81
|
+
Supports fetching successive result pages using a cursor token returned in
|
|
82
|
+
each response. Values are all optional except ``type``.
|
|
83
|
+
|
|
84
|
+
Attributes
|
|
85
|
+
----------
|
|
86
|
+
type : Required[Literal[PaginationType.CURSOR]]
|
|
87
|
+
Pagination type discriminator.
|
|
88
|
+
records_path : str
|
|
89
|
+
Dotted path to the records list in each page payload.
|
|
90
|
+
fallback_path : str
|
|
91
|
+
Secondary dotted path consulted when ``records_path`` resolves to an
|
|
92
|
+
empty collection or ``None``.
|
|
93
|
+
max_pages : int
|
|
94
|
+
Maximum number of pages to fetch.
|
|
95
|
+
max_records : int
|
|
96
|
+
Maximum number of records to fetch across all pages.
|
|
97
|
+
cursor_param : str
|
|
98
|
+
Query parameter name carrying the cursor value.
|
|
99
|
+
cursor_path : str
|
|
100
|
+
Dotted path inside the payload to the next cursor.
|
|
101
|
+
start_cursor : str | int
|
|
102
|
+
Initial cursor value used for the first request.
|
|
103
|
+
page_size : int
|
|
104
|
+
Number of records per page.
|
|
105
|
+
limit_param : str
|
|
106
|
+
Query parameter name carrying the page size for cursor-based
|
|
107
|
+
pagination when the API uses a separate limit field.
|
|
108
|
+
|
|
109
|
+
Examples
|
|
110
|
+
--------
|
|
111
|
+
>>> cfg: CursorPaginationConfig = {
|
|
112
|
+
... 'type': 'cursor',
|
|
113
|
+
... 'records_path': 'data.items',
|
|
114
|
+
... 'cursor_param': 'cursor',
|
|
115
|
+
... 'cursor_path': 'data.nextCursor',
|
|
116
|
+
... 'page_size': 100,
|
|
117
|
+
... }
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# -- Attributes -- #
|
|
121
|
+
|
|
122
|
+
type: Required[Literal[PaginationType.CURSOR]]
|
|
123
|
+
records_path: str
|
|
124
|
+
fallback_path: str
|
|
125
|
+
max_pages: int
|
|
126
|
+
max_records: int
|
|
127
|
+
cursor_param: str
|
|
128
|
+
cursor_path: str
|
|
129
|
+
start_cursor: str | int
|
|
130
|
+
page_size: int
|
|
131
|
+
limit_param: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PagePaginationConfigMap(TypedDict, total=False):
|
|
135
|
+
"""
|
|
136
|
+
Configuration mapping for page-based and offset-based REST API response
|
|
137
|
+
pagination.
|
|
138
|
+
|
|
139
|
+
Controls page-number or offset-based pagination. Values are optional
|
|
140
|
+
except ``type``.
|
|
141
|
+
|
|
142
|
+
Attributes
|
|
143
|
+
----------
|
|
144
|
+
type : Required[Literal[PaginationType.PAGE, PaginationType.OFFSET]]
|
|
145
|
+
Pagination type discriminator.
|
|
146
|
+
records_path : str
|
|
147
|
+
Dotted path to the records list in each page payload.
|
|
148
|
+
fallback_path : str
|
|
149
|
+
Secondary dotted path consulted when ``records_path`` resolves to an
|
|
150
|
+
empty collection or ``None``.
|
|
151
|
+
max_pages : int
|
|
152
|
+
Maximum number of pages to fetch.
|
|
153
|
+
max_records : int
|
|
154
|
+
Maximum number of records to fetch across all pages.
|
|
155
|
+
page_param : str
|
|
156
|
+
Query parameter name carrying the page number or offset.
|
|
157
|
+
size_param : str
|
|
158
|
+
Query parameter name carrying the page size.
|
|
159
|
+
start_page : int
|
|
160
|
+
Starting page number or offset (1-based).
|
|
161
|
+
page_size : int
|
|
162
|
+
Number of records per page.
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
>>> cfg: PagePaginationConfig = {
|
|
167
|
+
... 'type': 'page',
|
|
168
|
+
... 'records_path': 'data.items',
|
|
169
|
+
... 'page_param': 'page',
|
|
170
|
+
... 'size_param': 'per_page',
|
|
171
|
+
... 'start_page': 1,
|
|
172
|
+
... 'page_size': 100,
|
|
173
|
+
... }
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# -- Attributes -- #
|
|
177
|
+
|
|
178
|
+
type: Required[Literal[PaginationType.PAGE, PaginationType.OFFSET]]
|
|
179
|
+
records_path: str
|
|
180
|
+
fallback_path: str
|
|
181
|
+
max_pages: int
|
|
182
|
+
max_records: int
|
|
183
|
+
page_param: str
|
|
184
|
+
size_param: str
|
|
185
|
+
start_page: int
|
|
186
|
+
page_size: int
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# SECTION: DATA CLASSES ===================================================== #
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(kw_only=True, slots=True)
|
|
193
|
+
class PaginationConfig(BoundsWarningsMixin):
|
|
194
|
+
"""
|
|
195
|
+
Configuration container for API request pagination settings.
|
|
196
|
+
|
|
197
|
+
Attributes
|
|
198
|
+
----------
|
|
199
|
+
type : PaginationType | None
|
|
200
|
+
Pagination type: "page", "offset", or "cursor".
|
|
201
|
+
page_param : str | None
|
|
202
|
+
Name of the page parameter.
|
|
203
|
+
size_param : str | None
|
|
204
|
+
Name of the page size parameter.
|
|
205
|
+
start_page : int | None
|
|
206
|
+
Starting page number.
|
|
207
|
+
page_size : int | None
|
|
208
|
+
Number of records per page.
|
|
209
|
+
cursor_param : str | None
|
|
210
|
+
Name of the cursor parameter.
|
|
211
|
+
cursor_path : str | None
|
|
212
|
+
JSONPath expression to extract the cursor from the response.
|
|
213
|
+
start_cursor : str | int | None
|
|
214
|
+
Starting cursor value.
|
|
215
|
+
limit_param : str | None
|
|
216
|
+
Query parameter name carrying the page size for cursor-based
|
|
217
|
+
pagination when the API uses a separate limit field.
|
|
218
|
+
records_path : str | None
|
|
219
|
+
JSONPath expression to extract the records from the response.
|
|
220
|
+
fallback_path : str | None
|
|
221
|
+
Secondary JSONPath checked when ``records_path`` yields nothing.
|
|
222
|
+
max_pages : int | None
|
|
223
|
+
Maximum number of pages to retrieve.
|
|
224
|
+
max_records : int | None
|
|
225
|
+
Maximum number of records to retrieve.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
# -- Attributes -- #
|
|
229
|
+
|
|
230
|
+
type: PaginationType | None = None # "page" | "offset" | "cursor"
|
|
231
|
+
|
|
232
|
+
# Page/offset
|
|
233
|
+
page_param: str | None = None
|
|
234
|
+
size_param: str | None = None
|
|
235
|
+
start_page: int | None = None
|
|
236
|
+
page_size: int | None = None
|
|
237
|
+
|
|
238
|
+
# Cursor
|
|
239
|
+
cursor_param: str | None = None
|
|
240
|
+
cursor_path: str | None = None
|
|
241
|
+
start_cursor: str | int | None = None
|
|
242
|
+
limit_param: str | None = None
|
|
243
|
+
|
|
244
|
+
# General
|
|
245
|
+
records_path: str | None = None
|
|
246
|
+
fallback_path: str | None = None
|
|
247
|
+
max_pages: int | None = None
|
|
248
|
+
max_records: int | None = None
|
|
249
|
+
|
|
250
|
+
# -- Instance Methods -- #
|
|
251
|
+
|
|
252
|
+
def validate_bounds(self) -> list[str]:
|
|
253
|
+
"""
|
|
254
|
+
Return non-raising warnings for suspicious numeric bounds.
|
|
255
|
+
|
|
256
|
+
Uses structural pattern matching to keep branching concise.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
list[str]
|
|
261
|
+
Warning messages (empty if all values look sane).
|
|
262
|
+
"""
|
|
263
|
+
warnings: list[str] = []
|
|
264
|
+
|
|
265
|
+
# General limits
|
|
266
|
+
self._warn_if(
|
|
267
|
+
(mp := self.max_pages) is not None and mp <= 0,
|
|
268
|
+
'max_pages should be > 0',
|
|
269
|
+
warnings,
|
|
270
|
+
)
|
|
271
|
+
self._warn_if(
|
|
272
|
+
(mr := self.max_records) is not None and mr <= 0,
|
|
273
|
+
'max_records should be > 0',
|
|
274
|
+
warnings,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
match (self.type or '').strip().lower():
|
|
278
|
+
case 'page' | 'offset':
|
|
279
|
+
self._warn_if(
|
|
280
|
+
(sp := self.start_page) is not None and sp < 1,
|
|
281
|
+
'start_page should be >= 1',
|
|
282
|
+
warnings,
|
|
283
|
+
)
|
|
284
|
+
self._warn_if(
|
|
285
|
+
(ps := self.page_size) is not None and ps <= 0,
|
|
286
|
+
'page_size should be > 0',
|
|
287
|
+
warnings,
|
|
288
|
+
)
|
|
289
|
+
case 'cursor':
|
|
290
|
+
self._warn_if(
|
|
291
|
+
(ps := self.page_size) is not None and ps <= 0,
|
|
292
|
+
'page_size should be > 0 for cursor pagination',
|
|
293
|
+
warnings,
|
|
294
|
+
)
|
|
295
|
+
case _:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
return warnings
|
|
299
|
+
|
|
300
|
+
# -- Class Methods -- #
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def from_defaults(
|
|
304
|
+
cls,
|
|
305
|
+
obj: StrAnyMap | None,
|
|
306
|
+
) -> Self | None:
|
|
307
|
+
"""
|
|
308
|
+
Parse nested defaults mapping used by profile + endpoint configs.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
obj : StrAnyMap | None
|
|
313
|
+
Defaults mapping (non-mapping inputs return ``None``).
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
Self | None
|
|
318
|
+
A :class:`PaginationConfig` instance with numeric fields coerced to
|
|
319
|
+
int/float where applicable, or ``None`` if parsing failed.
|
|
320
|
+
"""
|
|
321
|
+
if not isinstance(obj, Mapping):
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
# Start with direct keys if present.
|
|
325
|
+
page_param = obj.get('page_param')
|
|
326
|
+
size_param = obj.get('size_param')
|
|
327
|
+
start_page = obj.get('start_page')
|
|
328
|
+
page_size = obj.get('page_size')
|
|
329
|
+
cursor_param = obj.get('cursor_param')
|
|
330
|
+
cursor_path = obj.get('cursor_path')
|
|
331
|
+
start_cursor = obj.get('start_cursor')
|
|
332
|
+
records_path = obj.get('records_path')
|
|
333
|
+
fallback_path = obj.get('fallback_path')
|
|
334
|
+
max_pages = obj.get('max_pages')
|
|
335
|
+
max_records = obj.get('max_records')
|
|
336
|
+
limit_param = obj.get('limit_param')
|
|
337
|
+
|
|
338
|
+
# Map from nested shapes when provided.
|
|
339
|
+
if params_blk := maybe_mapping(obj.get('params')):
|
|
340
|
+
page_param = page_param or params_blk.get('page')
|
|
341
|
+
size_param = (
|
|
342
|
+
size_param
|
|
343
|
+
or params_blk.get('per_page')
|
|
344
|
+
or params_blk.get('limit')
|
|
345
|
+
)
|
|
346
|
+
cursor_param = cursor_param or params_blk.get('cursor')
|
|
347
|
+
fallback_path = fallback_path or params_blk.get('fallback_path')
|
|
348
|
+
if resp_blk := maybe_mapping(obj.get('response')):
|
|
349
|
+
records_path = records_path or resp_blk.get('items_path')
|
|
350
|
+
cursor_path = cursor_path or resp_blk.get('next_cursor_path')
|
|
351
|
+
fallback_path = fallback_path or resp_blk.get('fallback_path')
|
|
352
|
+
if dflt_blk := maybe_mapping(obj.get('defaults')):
|
|
353
|
+
page_size = page_size or dflt_blk.get('per_page')
|
|
354
|
+
|
|
355
|
+
return cls(
|
|
356
|
+
type=PaginationType.try_coerce(obj.get('type')),
|
|
357
|
+
page_param=page_param,
|
|
358
|
+
size_param=size_param,
|
|
359
|
+
start_page=to_int(start_page),
|
|
360
|
+
page_size=to_int(page_size),
|
|
361
|
+
cursor_param=cursor_param,
|
|
362
|
+
cursor_path=cursor_path,
|
|
363
|
+
start_cursor=start_cursor,
|
|
364
|
+
records_path=records_path,
|
|
365
|
+
fallback_path=fallback_path,
|
|
366
|
+
max_pages=to_int(max_pages),
|
|
367
|
+
max_records=to_int(max_records),
|
|
368
|
+
limit_param=limit_param,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
@overload
|
|
373
|
+
def from_obj(
|
|
374
|
+
cls,
|
|
375
|
+
obj: None,
|
|
376
|
+
) -> None: ...
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
@overload
|
|
380
|
+
def from_obj(
|
|
381
|
+
cls,
|
|
382
|
+
obj: PaginationConfigMap,
|
|
383
|
+
) -> Self: ...
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def from_obj(
|
|
387
|
+
cls,
|
|
388
|
+
obj: Mapping[str, Any] | None,
|
|
389
|
+
) -> Self | None:
|
|
390
|
+
"""
|
|
391
|
+
Parse a mapping into a :class:`PaginationConfig` instance.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
obj : Mapping[str, Any] | None
|
|
396
|
+
Mapping with optional pagination fields, or ``None``.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
Self | None
|
|
401
|
+
Parsed pagination configuration, or ``None`` if ``obj`` isn't a
|
|
402
|
+
mapping.
|
|
403
|
+
|
|
404
|
+
Notes
|
|
405
|
+
-----
|
|
406
|
+
Tolerant: unknown keys ignored; numeric fields coerced via
|
|
407
|
+
``to_int``; non-mapping inputs return ``None``.
|
|
408
|
+
"""
|
|
409
|
+
if not isinstance(obj, Mapping):
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
return cls(
|
|
413
|
+
type=PaginationType.try_coerce(obj.get('type')),
|
|
414
|
+
page_param=obj.get('page_param'),
|
|
415
|
+
size_param=obj.get('size_param'),
|
|
416
|
+
start_page=to_int(obj.get('start_page')),
|
|
417
|
+
page_size=to_int(obj.get('page_size')),
|
|
418
|
+
cursor_param=obj.get('cursor_param'),
|
|
419
|
+
cursor_path=obj.get('cursor_path'),
|
|
420
|
+
start_cursor=obj.get('start_cursor'),
|
|
421
|
+
records_path=obj.get('records_path'),
|
|
422
|
+
fallback_path=obj.get('fallback_path'),
|
|
423
|
+
max_pages=to_int(obj.get('max_pages')),
|
|
424
|
+
max_records=to_int(obj.get('max_records')),
|
|
425
|
+
limit_param=obj.get('limit_param'),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# SECTION: TYPE ALIASES ===================================================== #
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
type PaginationConfigMap = PagePaginationConfigMap | CursorPaginationConfigMap
|
|
433
|
+
|
|
434
|
+
# External callers may pass either a raw mapping-shaped config or an already
|
|
435
|
+
# constructed PaginationConfig instance, or omit pagination entirely. Accept a
|
|
436
|
+
# loose mapping here to reflect the runtime behavior while still providing
|
|
437
|
+
# stronger TypedDict hints for common shapes.
|
|
438
|
+
type PaginationInput = (
|
|
439
|
+
PaginationConfigMap | PaginationConfig | StrAnyMap | None
|
|
440
|
+
)
|