etlplus 0.5.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. etlplus/__init__.py +43 -0
  2. etlplus/__main__.py +22 -0
  3. etlplus/__version__.py +14 -0
  4. etlplus/api/README.md +237 -0
  5. etlplus/api/__init__.py +136 -0
  6. etlplus/api/auth.py +432 -0
  7. etlplus/api/config.py +633 -0
  8. etlplus/api/endpoint_client.py +885 -0
  9. etlplus/api/errors.py +170 -0
  10. etlplus/api/pagination/__init__.py +47 -0
  11. etlplus/api/pagination/client.py +188 -0
  12. etlplus/api/pagination/config.py +440 -0
  13. etlplus/api/pagination/paginator.py +775 -0
  14. etlplus/api/rate_limiting/__init__.py +38 -0
  15. etlplus/api/rate_limiting/config.py +343 -0
  16. etlplus/api/rate_limiting/rate_limiter.py +266 -0
  17. etlplus/api/request_manager.py +589 -0
  18. etlplus/api/retry_manager.py +430 -0
  19. etlplus/api/transport.py +325 -0
  20. etlplus/api/types.py +172 -0
  21. etlplus/cli/__init__.py +15 -0
  22. etlplus/cli/app.py +1367 -0
  23. etlplus/cli/handlers.py +775 -0
  24. etlplus/cli/main.py +616 -0
  25. etlplus/config/__init__.py +56 -0
  26. etlplus/config/connector.py +372 -0
  27. etlplus/config/jobs.py +311 -0
  28. etlplus/config/pipeline.py +339 -0
  29. etlplus/config/profile.py +78 -0
  30. etlplus/config/types.py +204 -0
  31. etlplus/config/utils.py +120 -0
  32. etlplus/ddl.py +197 -0
  33. etlplus/enums.py +414 -0
  34. etlplus/extract.py +218 -0
  35. etlplus/file.py +657 -0
  36. etlplus/load.py +336 -0
  37. etlplus/mixins.py +62 -0
  38. etlplus/py.typed +0 -0
  39. etlplus/run.py +368 -0
  40. etlplus/run_helpers.py +843 -0
  41. etlplus/templates/__init__.py +5 -0
  42. etlplus/templates/ddl.sql.j2 +128 -0
  43. etlplus/templates/view.sql.j2 +69 -0
  44. etlplus/transform.py +1049 -0
  45. etlplus/types.py +227 -0
  46. etlplus/utils.py +638 -0
  47. etlplus/validate.py +493 -0
  48. etlplus/validation/__init__.py +44 -0
  49. etlplus/validation/utils.py +389 -0
  50. etlplus-0.5.4.dist-info/METADATA +616 -0
  51. etlplus-0.5.4.dist-info/RECORD +55 -0
  52. etlplus-0.5.4.dist-info/WHEEL +5 -0
  53. etlplus-0.5.4.dist-info/entry_points.txt +2 -0
  54. etlplus-0.5.4.dist-info/licenses/LICENSE +21 -0
  55. etlplus-0.5.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ )