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.
Files changed (120) hide show
  1. {etlplus-0.3.17/etlplus.egg-info → etlplus-0.3.19}/PKG-INFO +1 -1
  2. {etlplus-0.3.17 → etlplus-0.3.19/etlplus.egg-info}/PKG-INFO +1 -1
  3. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/SOURCES.txt +1 -0
  4. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_cli.py +2 -2
  5. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_endpoint_client.py +21 -20
  6. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_request_manager.py +68 -2
  7. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_transport.py +46 -0
  8. etlplus-0.3.19/tests/unit/api/test_u_types.py +136 -0
  9. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_file.py +2 -3
  10. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_run.py +1 -0
  11. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_validate.py +2 -1
  12. {etlplus-0.3.17 → etlplus-0.3.19}/.coveragerc +0 -0
  13. {etlplus-0.3.17 → etlplus-0.3.19}/.editorconfig +0 -0
  14. {etlplus-0.3.17 → etlplus-0.3.19}/.gitattributes +0 -0
  15. {etlplus-0.3.17 → etlplus-0.3.19}/.github/actions/python-bootstrap/action.yml +0 -0
  16. {etlplus-0.3.17 → etlplus-0.3.19}/.github/workflows/ci.yml +0 -0
  17. {etlplus-0.3.17 → etlplus-0.3.19}/.gitignore +0 -0
  18. {etlplus-0.3.17 → etlplus-0.3.19}/.pre-commit-config.yaml +0 -0
  19. {etlplus-0.3.17 → etlplus-0.3.19}/.ruff.toml +0 -0
  20. {etlplus-0.3.17 → etlplus-0.3.19}/CODE_OF_CONDUCT.md +0 -0
  21. {etlplus-0.3.17 → etlplus-0.3.19}/CONTRIBUTING.md +0 -0
  22. {etlplus-0.3.17 → etlplus-0.3.19}/DEMO.md +0 -0
  23. {etlplus-0.3.17 → etlplus-0.3.19}/LICENSE +0 -0
  24. {etlplus-0.3.17 → etlplus-0.3.19}/Makefile +0 -0
  25. {etlplus-0.3.17 → etlplus-0.3.19}/README.md +0 -0
  26. {etlplus-0.3.17 → etlplus-0.3.19}/REFERENCES.md +0 -0
  27. {etlplus-0.3.17 → etlplus-0.3.19}/docs/pipeline-guide.md +0 -0
  28. {etlplus-0.3.17 → etlplus-0.3.19}/docs/snippets/installation_version.md +0 -0
  29. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__init__.py +0 -0
  30. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__main__.py +0 -0
  31. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/__version__.py +0 -0
  32. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/README.md +0 -0
  33. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/__init__.py +0 -0
  34. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/auth.py +0 -0
  35. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/config.py +0 -0
  36. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/endpoint_client.py +0 -0
  37. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/errors.py +0 -0
  38. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/__init__.py +0 -0
  39. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/client.py +0 -0
  40. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/config.py +0 -0
  41. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/pagination/paginator.py +0 -0
  42. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/__init__.py +0 -0
  43. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/config.py +0 -0
  44. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  45. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/request_manager.py +0 -0
  46. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/retry_manager.py +0 -0
  47. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/transport.py +0 -0
  48. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/api/types.py +0 -0
  49. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/cli.py +0 -0
  50. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/__init__.py +0 -0
  51. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/connector.py +0 -0
  52. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/jobs.py +0 -0
  53. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/pipeline.py +0 -0
  54. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/profile.py +0 -0
  55. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/types.py +0 -0
  56. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/config/utils.py +0 -0
  57. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/enums.py +0 -0
  58. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/extract.py +0 -0
  59. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/file.py +0 -0
  60. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/load.py +0 -0
  61. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/mixins.py +0 -0
  62. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/py.typed +0 -0
  63. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/run.py +0 -0
  64. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/run_helpers.py +0 -0
  65. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/transform.py +0 -0
  66. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/types.py +0 -0
  67. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/utils.py +0 -0
  68. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validate.py +0 -0
  69. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validation/__init__.py +0 -0
  70. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus/validation/utils.py +0 -0
  71. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/dependency_links.txt +0 -0
  72. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/entry_points.txt +0 -0
  73. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/requires.txt +0 -0
  74. {etlplus-0.3.17 → etlplus-0.3.19}/etlplus.egg-info/top_level.txt +0 -0
  75. {etlplus-0.3.17 → etlplus-0.3.19}/examples/README.md +0 -0
  76. {etlplus-0.3.17 → etlplus-0.3.19}/examples/configs/pipeline.yml +0 -0
  77. {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.csv +0 -0
  78. {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.json +0 -0
  79. {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.xml +0 -0
  80. {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.xsd +0 -0
  81. {etlplus-0.3.17 → etlplus-0.3.19}/examples/data/sample.yaml +0 -0
  82. {etlplus-0.3.17 → etlplus-0.3.19}/examples/quickstart_python.py +0 -0
  83. {etlplus-0.3.17 → etlplus-0.3.19}/pyproject.toml +0 -0
  84. {etlplus-0.3.17 → etlplus-0.3.19}/pytest.ini +0 -0
  85. {etlplus-0.3.17 → etlplus-0.3.19}/setup.cfg +0 -0
  86. {etlplus-0.3.17 → etlplus-0.3.19}/setup.py +0 -0
  87. {etlplus-0.3.17 → etlplus-0.3.19}/tests/__init__.py +0 -0
  88. {etlplus-0.3.17 → etlplus-0.3.19}/tests/conftest.py +0 -0
  89. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/conftest.py +0 -0
  90. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_examples_data_parity.py +0 -0
  91. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pagination_strategy.py +0 -0
  92. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pipeline_smoke.py +0 -0
  93. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  94. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run.py +0 -0
  95. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  96. {etlplus-0.3.17 → etlplus-0.3.19}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  97. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/conftest.py +0 -0
  98. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_auth.py +0 -0
  99. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_config.py +0 -0
  100. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_mocks.py +0 -0
  101. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_pagination_client.py +0 -0
  102. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_pagination_config.py +0 -0
  103. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_paginator.py +0 -0
  104. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  105. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_rate_limiter.py +0 -0
  106. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/api/test_u_retry_manager.py +0 -0
  107. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/config/test_u_connector.py +0 -0
  108. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/config/test_u_pipeline.py +0 -0
  109. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/conftest.py +0 -0
  110. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_cli.py +0 -0
  111. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_enums.py +0 -0
  112. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_extract.py +0 -0
  113. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_load.py +0 -0
  114. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_mixins.py +0 -0
  115. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_run_helpers.py +0 -0
  116. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_transform.py +0 -0
  117. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/test_u_utils.py +0 -0
  118. {etlplus-0.3.17 → etlplus-0.3.19}/tests/unit/validation/test_u_validation_utils.py +0 -0
  119. {etlplus-0.3.17 → etlplus-0.3.19}/tools/run_pipeline.py +0 -0
  120. {etlplus-0.3.17 → etlplus-0.3.19}/tools/update_demo_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -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._typing import CliInvoke
25
- from tests._typing import JsonFactory
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: Callable[..., CursorPaginationConfigMap],
386
+ cursor_cfg: CursorConfigFactory,
379
387
  client_factory: Callable[..., EndpointClient],
380
- stub_request_manager: Callable[
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 : Callable[..., CursorPaginationConfigMap]
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 : Callable[[Sequence[dict[str, Any]]],
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: Callable[..., CursorPaginationConfigMap],
520
+ cursor_cfg: CursorConfigFactory,
516
521
  client_factory: Callable[..., EndpointClient],
517
- stub_request_manager: Callable[
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 : Callable[..., CursorPaginationConfigMap]
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 : Callable[[Sequence[dict[str, Any]]],
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: Callable[..., CursorPaginationConfigMap],
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 : Callable[..., CursorPaginationConfigMap]
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: Callable[..., CursorPaginationConfigMap],
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 : Callable[..., CursorPaginationConfigMap]
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
- """Ensure adapter configs yield a managed session that gets closed."""
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
- """Context manager should reuse one adapter-backed session."""
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
- result = validate_field('a', {'enum': 'abc'})
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