etlplus 0.3.17__tar.gz → 0.3.19__tar.gz
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-0.3.17/etlplus.egg-info → etlplus-0.3.19}/PKG-INFO +1 -1
- {etlplus-0.3.17 → etlplus-0.3.19/etlplus.egg-info}/PKG-INFO +1 -1
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/SOURCES.txt +1 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_cli.py +2 -2
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_endpoint_client.py +21 -20
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_request_manager.py +68 -2
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_transport.py +46 -0
- etlplus-0.3.19/tests/unit/api/test_u_types.py +136 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_file.py +2 -3
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_run.py +1 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_validate.py +2 -1
- {etlplus-0.3.17 → etlplus-0.3.19}/.coveragerc +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.editorconfig +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.gitattributes +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.github/actions/python-bootstrap/action.yml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.github/workflows/ci.yml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.gitignore +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.pre-commit-config.yaml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/.ruff.toml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/CODE_OF_CONDUCT.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/CONTRIBUTING.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/DEMO.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/LICENSE +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/Makefile +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/README.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/REFERENCES.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/docs/pipeline-guide.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/docs/snippets/installation_version.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__main__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__version__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/README.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/auth.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/endpoint_client.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/errors.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/client.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/paginator.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/request_manager.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/retry_manager.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/transport.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/types.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/cli.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/connector.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/jobs.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/pipeline.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/profile.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/types.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/utils.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/enums.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/extract.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/file.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/load.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/mixins.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/py.typed +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/run.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/run_helpers.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/transform.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/types.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/utils.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validate.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validation/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validation/utils.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/dependency_links.txt +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/entry_points.txt +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/requires.txt +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/top_level.txt +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/README.md +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/configs/pipeline.yml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.csv +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.json +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.xml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.xsd +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.yaml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/examples/quickstart_python.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/pyproject.toml +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/pytest.ini +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/setup.cfg +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/setup.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/__init__.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/conftest.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/conftest.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_examples_data_parity.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pagination_strategy.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pipeline_smoke.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/conftest.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_auth.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_mocks.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_pagination_client.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_pagination_config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_paginator.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_rate_limit_config.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_rate_limiter.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_retry_manager.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/config/test_u_connector.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/config/test_u_pipeline.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/conftest.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_cli.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_enums.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_extract.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_load.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_mixins.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_run_helpers.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_transform.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_utils.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/validation/test_u_validation_utils.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tools/run_pipeline.py +0 -0
- {etlplus-0.3.17 → etlplus-0.3.19}/tools/update_demo_snippets.py +0 -0
|
@@ -110,6 +110,7 @@ tests/unit/api/test_u_rate_limiter.py
|
|
|
110
110
|
tests/unit/api/test_u_request_manager.py
|
|
111
111
|
tests/unit/api/test_u_retry_manager.py
|
|
112
112
|
tests/unit/api/test_u_transport.py
|
|
113
|
+
tests/unit/api/test_u_types.py
|
|
113
114
|
tests/unit/config/test_u_connector.py
|
|
114
115
|
tests/unit/config/test_u_pipeline.py
|
|
115
116
|
tests/unit/validation/test_u_validation_utils.py
|
|
@@ -21,8 +21,8 @@ from typing import TYPE_CHECKING
|
|
|
21
21
|
import pytest
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING: # pragma: no cover - typing helpers only
|
|
24
|
-
from tests.
|
|
25
|
-
from tests.
|
|
24
|
+
from tests.conftest import CliInvoke
|
|
25
|
+
from tests.conftest import JsonFactory
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# SECTION: HELPERS ========================================================== #
|
|
@@ -41,6 +41,12 @@ pytestmark = pytest.mark.unit
|
|
|
41
41
|
|
|
42
42
|
EXAMPLE_BASE_URL = 'https://example.test'
|
|
43
43
|
|
|
44
|
+
type CursorConfigFactory = Callable[..., CursorPaginationConfigMap]
|
|
45
|
+
type StubRequestManager = Callable[
|
|
46
|
+
[Sequence[dict[str, Any]]],
|
|
47
|
+
list[dict[str, Any]],
|
|
48
|
+
]
|
|
49
|
+
|
|
44
50
|
# Optional Hypothesis import with safe stubs when missing.
|
|
45
51
|
try: # pragma: no try
|
|
46
52
|
from hypothesis import given # type: ignore[import-not-found]
|
|
@@ -110,6 +116,7 @@ def _page_responder(
|
|
|
110
116
|
Callable[..., list[dict[str, Any]]]
|
|
111
117
|
Handler compatible with ``patch_request_once``.
|
|
112
118
|
"""
|
|
119
|
+
# pylint: disable=unused-argument
|
|
113
120
|
|
|
114
121
|
def _handler(
|
|
115
122
|
self: EndpointClient,
|
|
@@ -155,6 +162,7 @@ def _stub_request_manager(
|
|
|
155
162
|
ValueError
|
|
156
163
|
If ``responses`` is empty.
|
|
157
164
|
"""
|
|
165
|
+
# pylint: disable=unused-argument
|
|
158
166
|
|
|
159
167
|
if not responses:
|
|
160
168
|
msg = 'responses must contain at least one payload'
|
|
@@ -375,12 +383,9 @@ class TestCursorPagination:
|
|
|
375
383
|
)
|
|
376
384
|
def test_page_size_normalizes(
|
|
377
385
|
self,
|
|
378
|
-
cursor_cfg:
|
|
386
|
+
cursor_cfg: CursorConfigFactory,
|
|
379
387
|
client_factory: Callable[..., EndpointClient],
|
|
380
|
-
stub_request_manager:
|
|
381
|
-
[Sequence[dict[str, Any]]],
|
|
382
|
-
list[dict[str, Any]],
|
|
383
|
-
],
|
|
388
|
+
stub_request_manager: StubRequestManager,
|
|
384
389
|
raw_page_size: Any,
|
|
385
390
|
expected_limit: int,
|
|
386
391
|
) -> None:
|
|
@@ -389,12 +394,11 @@ class TestCursorPagination:
|
|
|
389
394
|
|
|
390
395
|
Parameters
|
|
391
396
|
----------
|
|
392
|
-
cursor_cfg :
|
|
397
|
+
cursor_cfg : CursorConfigFactory
|
|
393
398
|
Factory for cursor pagination config.
|
|
394
399
|
client_factory : Callable[..., EndpointClient]
|
|
395
400
|
Factory fixture used to construct :class:`EndpointClient`.
|
|
396
|
-
stub_request_manager :
|
|
397
|
-
list[dict[str, Any]]]
|
|
401
|
+
stub_request_manager : StubRequestManager
|
|
398
402
|
Fixture that patches the underlying :class:`RequestManager`.
|
|
399
403
|
raw_page_size : Any
|
|
400
404
|
Raw page size input.
|
|
@@ -473,6 +477,7 @@ class TestRequestOptionIntegration:
|
|
|
473
477
|
client_factory: Callable[..., EndpointClient],
|
|
474
478
|
) -> None:
|
|
475
479
|
"""Paginated iterations override RequestOptions params per call."""
|
|
480
|
+
# pylint: disable=unused-argument
|
|
476
481
|
|
|
477
482
|
client = client_factory(base_url=EXAMPLE_BASE_URL, endpoints={})
|
|
478
483
|
observed: list[RequestOptions] = []
|
|
@@ -512,24 +517,20 @@ class TestRequestOptionIntegration:
|
|
|
512
517
|
|
|
513
518
|
def test_adds_limit_and_advances_cursor(
|
|
514
519
|
self,
|
|
515
|
-
cursor_cfg:
|
|
520
|
+
cursor_cfg: CursorConfigFactory,
|
|
516
521
|
client_factory: Callable[..., EndpointClient],
|
|
517
|
-
stub_request_manager:
|
|
518
|
-
[Sequence[dict[str, Any]]],
|
|
519
|
-
list[dict[str, Any]],
|
|
520
|
-
],
|
|
522
|
+
stub_request_manager: StubRequestManager,
|
|
521
523
|
) -> None:
|
|
522
524
|
"""
|
|
523
525
|
Test that limit is added and cursor advances correctly.
|
|
524
526
|
|
|
525
527
|
Parameters
|
|
526
528
|
----------
|
|
527
|
-
cursor_cfg :
|
|
529
|
+
cursor_cfg : CursorConfigFactory
|
|
528
530
|
Factory for cursor pagination config.
|
|
529
531
|
client_factory : Callable[..., EndpointClient]
|
|
530
532
|
Factory fixture used to construct :class:`EndpointClient`.
|
|
531
|
-
stub_request_manager :
|
|
532
|
-
list[dict[str, Any]]]
|
|
533
|
+
stub_request_manager : StubRequestManager
|
|
533
534
|
Fixture that patches :class:`RequestManager` responses.
|
|
534
535
|
"""
|
|
535
536
|
calls = stub_request_manager(
|
|
@@ -561,7 +562,7 @@ class TestRequestOptionIntegration:
|
|
|
561
562
|
self,
|
|
562
563
|
base_url: str,
|
|
563
564
|
patch_request_once: Callable[[Callable[..., Any]], Callable[..., Any]],
|
|
564
|
-
cursor_cfg:
|
|
565
|
+
cursor_cfg: CursorConfigFactory,
|
|
565
566
|
client_factory: Callable[..., EndpointClient],
|
|
566
567
|
) -> None:
|
|
567
568
|
"""
|
|
@@ -577,7 +578,7 @@ class TestRequestOptionIntegration:
|
|
|
577
578
|
Common base URL used across tests.
|
|
578
579
|
patch_request_once : Callable[[Callable[..., Any]], Callable[..., Any]]
|
|
579
580
|
Helper that patches the request helper for deterministic failures.
|
|
580
|
-
cursor_cfg :
|
|
581
|
+
cursor_cfg : CursorConfigFactory
|
|
581
582
|
Factory for cursor pagination config.
|
|
582
583
|
client_factory : Callable[..., EndpointClient]
|
|
583
584
|
Factory fixture used to construct :class:`EndpointClient`.
|
|
@@ -700,7 +701,7 @@ class TestRequestOptionIntegration:
|
|
|
700
701
|
|
|
701
702
|
def test_retry_backoff_sleeps(
|
|
702
703
|
self,
|
|
703
|
-
cursor_cfg:
|
|
704
|
+
cursor_cfg: CursorConfigFactory,
|
|
704
705
|
capture_sleeps: list[float],
|
|
705
706
|
jitter: Callable[[list[float]], list[float]],
|
|
706
707
|
patch_request_once: Callable[[Callable[..., Any]], Callable[..., Any]],
|
|
@@ -713,7 +714,7 @@ class TestRequestOptionIntegration:
|
|
|
713
714
|
|
|
714
715
|
Parameters
|
|
715
716
|
----------
|
|
716
|
-
cursor_cfg :
|
|
717
|
+
cursor_cfg : CursorConfigFactory
|
|
717
718
|
Factory for cursor pagination config.
|
|
718
719
|
capture_sleeps : list[float]
|
|
719
720
|
List to capture sleep durations.
|
|
@@ -36,7 +36,7 @@ class DummySession:
|
|
|
36
36
|
def test_request_manager_builds_adapter_session(
|
|
37
37
|
monkeypatch: pytest.MonkeyPatch,
|
|
38
38
|
) -> None:
|
|
39
|
-
"""
|
|
39
|
+
"""Test that adapter configs yield a managed session that gets closed."""
|
|
40
40
|
captured: dict[str, Any] = {}
|
|
41
41
|
dummy_session = DummySession()
|
|
42
42
|
|
|
@@ -82,7 +82,7 @@ def test_request_manager_builds_adapter_session(
|
|
|
82
82
|
def test_request_manager_context_reuses_adapter_session(
|
|
83
83
|
monkeypatch: pytest.MonkeyPatch,
|
|
84
84
|
) -> None:
|
|
85
|
-
"""
|
|
85
|
+
"""Test that context manager reuses one adapter-backed session."""
|
|
86
86
|
dummy_session = DummySession()
|
|
87
87
|
builder_calls: list[Any] = []
|
|
88
88
|
sessions_used: list[Any] = []
|
|
@@ -133,3 +133,69 @@ def test_request_manager_context_reuses_adapter_session(
|
|
|
133
133
|
assert sessions_used == [dummy_session, dummy_session]
|
|
134
134
|
assert timeouts == [manager.default_timeout, manager.default_timeout]
|
|
135
135
|
assert extra_kwargs == [{}, {}]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_request_manager_invalid_session_adapters(
|
|
139
|
+
monkeypatch,
|
|
140
|
+
):
|
|
141
|
+
"""Test that invalid session_adapters do not raise on context enter."""
|
|
142
|
+
# Should not raise if session_adapters is invalid
|
|
143
|
+
manager = RequestManager(
|
|
144
|
+
session_adapters=[{'prefix': 'https://', 'pool_connections': 'bad'}],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def fake_builder(cfg: Any) -> None:
|
|
148
|
+
raise ValueError('bad config')
|
|
149
|
+
|
|
150
|
+
monkeypatch.setattr(
|
|
151
|
+
'etlplus.api.request_manager.build_session_with_adapters',
|
|
152
|
+
fake_builder,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with manager:
|
|
157
|
+
pass
|
|
158
|
+
except Exception: # pylint: disable=broad-except
|
|
159
|
+
pytest.fail('RequestManager context should not raise on bad adapters')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_request_manager_request_callable(
|
|
163
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Test that request_callable is invoked and its result returned."""
|
|
166
|
+
manager = RequestManager()
|
|
167
|
+
called: dict[str, Any] = {}
|
|
168
|
+
|
|
169
|
+
def fake_request_once(
|
|
170
|
+
method: str,
|
|
171
|
+
url: str,
|
|
172
|
+
*,
|
|
173
|
+
session: Any,
|
|
174
|
+
timeout: Any,
|
|
175
|
+
request_callable: Any | None = None,
|
|
176
|
+
**kwargs: Any,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
called['method'] = method
|
|
179
|
+
called['url'] = url
|
|
180
|
+
called['session'] = session
|
|
181
|
+
called['timeout'] = timeout
|
|
182
|
+
called['request_callable'] = request_callable
|
|
183
|
+
called['kwargs'] = kwargs
|
|
184
|
+
return {'ok': True}
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(
|
|
187
|
+
type(manager),
|
|
188
|
+
'request_once',
|
|
189
|
+
staticmethod(fake_request_once),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
result = manager.request(
|
|
193
|
+
'POST',
|
|
194
|
+
'http://test',
|
|
195
|
+
request_callable=lambda *a, **k: {'ok': 'cb'},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert result == {'ok': True}
|
|
199
|
+
assert called['method'] == 'POST'
|
|
200
|
+
assert called['url'] == 'http://test'
|
|
201
|
+
assert callable(called['request_callable'])
|
|
@@ -20,6 +20,7 @@ import pytest
|
|
|
20
20
|
import requests # type: ignore[import]
|
|
21
21
|
|
|
22
22
|
from etlplus.api.transport import build_http_adapter
|
|
23
|
+
from etlplus.api.transport import build_session_with_adapters
|
|
23
24
|
|
|
24
25
|
# SECTION: HELPERS ========================================================== #
|
|
25
26
|
|
|
@@ -67,6 +68,51 @@ class TestBuildHttpAdapter:
|
|
|
67
68
|
total = getattr(mr, 'total', None)
|
|
68
69
|
assert total in (0, 3)
|
|
69
70
|
|
|
71
|
+
def test_build_http_adapter_invalid_config(self):
|
|
72
|
+
"""
|
|
73
|
+
Test that invalid config does not raise and returns a usable adapter.
|
|
74
|
+
"""
|
|
75
|
+
cfg = {
|
|
76
|
+
'pool_connections': 'not-an-int',
|
|
77
|
+
'max_retries': {'total': 'bad'},
|
|
78
|
+
}
|
|
79
|
+
adapter = build_http_adapter(cfg)
|
|
80
|
+
assert isinstance(adapter, requests.adapters.HTTPAdapter)
|
|
81
|
+
|
|
82
|
+
def test_build_http_adapter_missing_keys(self):
|
|
83
|
+
"""Test that missing keys are handled gracefully."""
|
|
84
|
+
cfg = {}
|
|
85
|
+
adapter = build_http_adapter(cfg)
|
|
86
|
+
assert isinstance(adapter, requests.adapters.HTTPAdapter)
|
|
87
|
+
|
|
88
|
+
def test_build_http_adapter_retry_dict_edge(self):
|
|
89
|
+
"""Test retry dict with unknown keys is ignored."""
|
|
90
|
+
cfg = {
|
|
91
|
+
'max_retries': {'total': 2, 'unknown_key': 123},
|
|
92
|
+
}
|
|
93
|
+
adapter = build_http_adapter(cfg)
|
|
94
|
+
mr = adapter.max_retries
|
|
95
|
+
if isinstance(mr, int):
|
|
96
|
+
assert mr in (0, 2)
|
|
97
|
+
else:
|
|
98
|
+
assert getattr(mr, 'total', None) in (0, 2)
|
|
99
|
+
|
|
100
|
+
def test_build_session_with_adapters_invalid(self):
|
|
101
|
+
"""
|
|
102
|
+
Test that invalid adapter configs are skipped but session is usable.
|
|
103
|
+
"""
|
|
104
|
+
adapters_cfg = [
|
|
105
|
+
{'prefix': 'https://', 'pool_connections': 'bad'},
|
|
106
|
+
{'prefix': 'http://', 'max_retries': {'total': 'bad'}},
|
|
107
|
+
]
|
|
108
|
+
session = requests.Session()
|
|
109
|
+
# Should not raise
|
|
110
|
+
try:
|
|
111
|
+
session = build_session_with_adapters(adapters_cfg)
|
|
112
|
+
except Exception:
|
|
113
|
+
pytest.fail('build_session_with_adapters should not raise')
|
|
114
|
+
assert isinstance(session, requests.Session)
|
|
115
|
+
|
|
70
116
|
def test_integer_retries_fallback(self) -> None:
|
|
71
117
|
"""Test handling integer max_retries fallback."""
|
|
72
118
|
cfg = {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`tests.unit.api.test_u_types` module.
|
|
3
|
+
|
|
4
|
+
Unit tests for custom type aliases and data structures in
|
|
5
|
+
:mod:`etlplus.api.types`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from etlplus.api.types import FetchPageCallable
|
|
11
|
+
from etlplus.api.types import Headers
|
|
12
|
+
from etlplus.api.types import Params
|
|
13
|
+
from etlplus.api.types import RequestOptions
|
|
14
|
+
from etlplus.api.types import Url
|
|
15
|
+
|
|
16
|
+
# SECTION: HELPERS ========================================================== #
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
pytestmark = pytest.mark.unit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# SECTION: TESTS ============================================================ #
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_request_options_as_kwargs():
|
|
26
|
+
"""Test that :meth:`RequestOptions.as_kwargs` produces correct dict."""
|
|
27
|
+
opts = RequestOptions(params={'a': 1}, headers={'X': 'y'}, timeout=5.0)
|
|
28
|
+
kw = opts.as_kwargs()
|
|
29
|
+
assert kw['params'] == {'a': 1}
|
|
30
|
+
assert kw['headers'] == {'X': 'y'}
|
|
31
|
+
assert kw['timeout'] == 5.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_request_options_as_kwargs_edge_cases():
|
|
35
|
+
"""Test :meth:`RequestOptions.as_kwargs` with unset and ``None`` fields."""
|
|
36
|
+
opts = RequestOptions()
|
|
37
|
+
kw = opts.as_kwargs()
|
|
38
|
+
assert not kw
|
|
39
|
+
|
|
40
|
+
opts2 = RequestOptions(params={'x': 1})
|
|
41
|
+
kw2 = opts2.as_kwargs()
|
|
42
|
+
assert kw2['params'] == {'x': 1}
|
|
43
|
+
assert 'headers' not in kw2
|
|
44
|
+
assert 'timeout' not in kw2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_request_options_defaults():
|
|
48
|
+
"""Test that :class:`RequestOptions` defaults to None fields."""
|
|
49
|
+
opts = RequestOptions()
|
|
50
|
+
assert opts.params is None
|
|
51
|
+
assert opts.headers is None
|
|
52
|
+
assert opts.timeout is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_request_options_evolve():
|
|
56
|
+
"""
|
|
57
|
+
Test that :meth:`RequestOptions.evolve` creates modified copies correctly.
|
|
58
|
+
"""
|
|
59
|
+
opts = RequestOptions(params={'a': 1}, headers={'X': 'y'}, timeout=5.0)
|
|
60
|
+
evolved = opts.evolve(params={'b': 2}, headers=None, timeout=None)
|
|
61
|
+
assert evolved.params == {'b': 2}
|
|
62
|
+
assert evolved.headers is None
|
|
63
|
+
assert evolved.timeout is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_request_options_evolve_edge_cases():
|
|
67
|
+
"""Test :meth:`RequestOptions.evolve` with unset and ``None`` fields."""
|
|
68
|
+
opts = RequestOptions(params={'a': 1}, headers={'X': 'y'}, timeout=5.0)
|
|
69
|
+
|
|
70
|
+
# Evolve with _UNSET (should preserve existing).
|
|
71
|
+
evolved = opts.evolve()
|
|
72
|
+
assert evolved.params == {'a': 1}
|
|
73
|
+
assert evolved.headers == {'X': 'y'}
|
|
74
|
+
assert evolved.timeout == 5.0
|
|
75
|
+
|
|
76
|
+
# Evolve with None (should clear).
|
|
77
|
+
evolved2 = opts.evolve(params=None, headers=None, timeout=None)
|
|
78
|
+
assert evolved2.params is None
|
|
79
|
+
assert evolved2.headers is None
|
|
80
|
+
assert evolved2.timeout is None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_request_options_invalid_params_headers():
|
|
84
|
+
"""
|
|
85
|
+
Test that :class:`RequestOptions` coerces mapping-like objects to dict.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Should coerce mapping-like objects to dict.
|
|
89
|
+
class DummyMap(dict):
|
|
90
|
+
"""Dummy mapping-like class for testing."""
|
|
91
|
+
|
|
92
|
+
opts = RequestOptions(params=DummyMap(a=1), headers=DummyMap(X='y'))
|
|
93
|
+
assert isinstance(opts.params, dict)
|
|
94
|
+
assert isinstance(opts.headers, dict)
|
|
95
|
+
|
|
96
|
+
# Should handle None gracefully.
|
|
97
|
+
opts2 = RequestOptions(params=None, headers=None)
|
|
98
|
+
assert opts2.params is None
|
|
99
|
+
assert opts2.headers is None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_type_aliases():
|
|
103
|
+
"""Test that type aliases are correct."""
|
|
104
|
+
# pylint: disable=unused-argument
|
|
105
|
+
|
|
106
|
+
# url: Url = 'https://api.example.com/data'
|
|
107
|
+
# headers: Headers = {'Authorization': 'token'}
|
|
108
|
+
# params: Params = {'q': 'search'}
|
|
109
|
+
|
|
110
|
+
def fetch(url: Url, opts: RequestOptions, page: int | None):
|
|
111
|
+
return {'data': [1, 2, 3]}
|
|
112
|
+
|
|
113
|
+
cb: FetchPageCallable = fetch
|
|
114
|
+
assert callable(cb)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_type_aliases_edge_cases():
|
|
118
|
+
"""Test type aliases with edge case values."""
|
|
119
|
+
# pylint: disable=unused-argument
|
|
120
|
+
|
|
121
|
+
# Url must be str.
|
|
122
|
+
url: Url = 'http://test/'
|
|
123
|
+
assert isinstance(url, str)
|
|
124
|
+
# Headers must be dict[str, str].
|
|
125
|
+
headers: Headers = {'A': 'B'}
|
|
126
|
+
assert isinstance(headers, dict)
|
|
127
|
+
# Params must be dict[str, Any].
|
|
128
|
+
params: Params = {'A': 1, 'B': [1, 2]}
|
|
129
|
+
assert isinstance(params, dict)
|
|
130
|
+
|
|
131
|
+
# FetchPageCallable must accept correct signature.
|
|
132
|
+
def fetch(url: Url, opts: RequestOptions, page: int | None):
|
|
133
|
+
return {'data': []}
|
|
134
|
+
|
|
135
|
+
cb: FetchPageCallable = fetch
|
|
136
|
+
assert callable(cb)
|
|
@@ -56,10 +56,9 @@ class _StubYaml:
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@pytest.fixture
|
|
59
|
-
def yaml_stub(
|
|
60
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
61
|
-
) -> Generator[_StubYaml]:
|
|
59
|
+
def yaml_stub() -> Generator[_StubYaml]:
|
|
62
60
|
"""Install a stub PyYAML module for YAML tests."""
|
|
61
|
+
# pylint: disable=protected-access
|
|
63
62
|
|
|
64
63
|
stub = _StubYaml()
|
|
65
64
|
file_module._YAML_CACHE.clear()
|
|
@@ -184,6 +184,7 @@ class TestRun:
|
|
|
184
184
|
monkeypatch: pytest.MonkeyPatch,
|
|
185
185
|
) -> None:
|
|
186
186
|
"""Test a file-to-file ETL pipeline execution."""
|
|
187
|
+
# pylint: disable=unused-argument
|
|
187
188
|
job = _make_job(name='file_job', source='file_src', target='file_tgt')
|
|
188
189
|
cfg = _base_config(
|
|
189
190
|
job,
|
|
@@ -48,7 +48,8 @@ class TestValidateField:
|
|
|
48
48
|
def test_enum_rule_requires_list(self) -> None:
|
|
49
49
|
"""Test non-list enum rules adding an error entry."""
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# Test expects the value for key ``enum`` to not be a list.
|
|
52
|
+
result = validate_field('a', {'enum': 'abc'}) # type: ignore
|
|
52
53
|
assert result['valid'] is False
|
|
53
54
|
assert any('enum' in err for err in result['errors'])
|
|
54
55
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_pagination_defaults.py
RENAMED
|
File without changes
|
{etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_rate_limit_defaults.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|