etlplus 0.3.17__tar.gz → 0.3.21__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 (126) hide show
  1. {etlplus-0.3.17/etlplus.egg-info → etlplus-0.3.21}/PKG-INFO +1 -1
  2. {etlplus-0.3.17 → etlplus-0.3.21/etlplus.egg-info}/PKG-INFO +1 -1
  3. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus.egg-info/SOURCES.txt +5 -0
  4. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_cli.py +2 -2
  5. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_auth.py +1 -1
  6. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_endpoint_client.py +22 -21
  7. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_rate_limiter.py +1 -1
  8. etlplus-0.3.21/tests/unit/api/test_u_request_manager.py +349 -0
  9. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_transport.py +48 -1
  10. etlplus-0.3.21/tests/unit/api/test_u_types.py +135 -0
  11. etlplus-0.3.21/tests/unit/config/test_u_config_utils.py +129 -0
  12. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/config/test_u_connector.py +1 -1
  13. etlplus-0.3.21/tests/unit/config/test_u_jobs.py +131 -0
  14. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/config/test_u_pipeline.py +1 -1
  15. etlplus-0.3.21/tests/unit/test_u_cli.py +299 -0
  16. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_enums.py +1 -1
  17. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_extract.py +1 -1
  18. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_file.py +5 -6
  19. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_load.py +4 -4
  20. etlplus-0.3.21/tests/unit/test_u_main.py +58 -0
  21. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_mixins.py +1 -1
  22. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_run.py +33 -18
  23. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_run_helpers.py +1 -1
  24. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_transform.py +167 -40
  25. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_utils.py +3 -3
  26. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/test_u_validate.py +4 -3
  27. etlplus-0.3.21/tests/unit/test_u_version.py +53 -0
  28. etlplus-0.3.17/tests/unit/api/test_u_request_manager.py +0 -135
  29. etlplus-0.3.17/tests/unit/test_u_cli.py +0 -185
  30. {etlplus-0.3.17 → etlplus-0.3.21}/.coveragerc +0 -0
  31. {etlplus-0.3.17 → etlplus-0.3.21}/.editorconfig +0 -0
  32. {etlplus-0.3.17 → etlplus-0.3.21}/.gitattributes +0 -0
  33. {etlplus-0.3.17 → etlplus-0.3.21}/.github/actions/python-bootstrap/action.yml +0 -0
  34. {etlplus-0.3.17 → etlplus-0.3.21}/.github/workflows/ci.yml +0 -0
  35. {etlplus-0.3.17 → etlplus-0.3.21}/.gitignore +0 -0
  36. {etlplus-0.3.17 → etlplus-0.3.21}/.pre-commit-config.yaml +0 -0
  37. {etlplus-0.3.17 → etlplus-0.3.21}/.ruff.toml +0 -0
  38. {etlplus-0.3.17 → etlplus-0.3.21}/CODE_OF_CONDUCT.md +0 -0
  39. {etlplus-0.3.17 → etlplus-0.3.21}/CONTRIBUTING.md +0 -0
  40. {etlplus-0.3.17 → etlplus-0.3.21}/DEMO.md +0 -0
  41. {etlplus-0.3.17 → etlplus-0.3.21}/LICENSE +0 -0
  42. {etlplus-0.3.17 → etlplus-0.3.21}/Makefile +0 -0
  43. {etlplus-0.3.17 → etlplus-0.3.21}/README.md +0 -0
  44. {etlplus-0.3.17 → etlplus-0.3.21}/REFERENCES.md +0 -0
  45. {etlplus-0.3.17 → etlplus-0.3.21}/docs/pipeline-guide.md +0 -0
  46. {etlplus-0.3.17 → etlplus-0.3.21}/docs/snippets/installation_version.md +0 -0
  47. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/__init__.py +0 -0
  48. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/__main__.py +0 -0
  49. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/__version__.py +0 -0
  50. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/README.md +0 -0
  51. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/__init__.py +0 -0
  52. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/auth.py +0 -0
  53. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/config.py +0 -0
  54. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/endpoint_client.py +0 -0
  55. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/errors.py +0 -0
  56. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/pagination/__init__.py +0 -0
  57. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/pagination/client.py +0 -0
  58. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/pagination/config.py +0 -0
  59. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/pagination/paginator.py +0 -0
  60. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/rate_limiting/__init__.py +0 -0
  61. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/rate_limiting/config.py +0 -0
  62. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  63. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/request_manager.py +0 -0
  64. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/retry_manager.py +0 -0
  65. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/transport.py +0 -0
  66. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/api/types.py +0 -0
  67. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/cli.py +0 -0
  68. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/__init__.py +0 -0
  69. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/connector.py +0 -0
  70. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/jobs.py +0 -0
  71. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/pipeline.py +0 -0
  72. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/profile.py +0 -0
  73. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/types.py +0 -0
  74. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/config/utils.py +0 -0
  75. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/enums.py +0 -0
  76. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/extract.py +0 -0
  77. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/file.py +0 -0
  78. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/load.py +0 -0
  79. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/mixins.py +0 -0
  80. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/py.typed +0 -0
  81. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/run.py +0 -0
  82. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/run_helpers.py +0 -0
  83. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/transform.py +0 -0
  84. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/types.py +0 -0
  85. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/utils.py +0 -0
  86. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/validate.py +0 -0
  87. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/validation/__init__.py +0 -0
  88. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus/validation/utils.py +0 -0
  89. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus.egg-info/dependency_links.txt +0 -0
  90. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus.egg-info/entry_points.txt +0 -0
  91. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus.egg-info/requires.txt +0 -0
  92. {etlplus-0.3.17 → etlplus-0.3.21}/etlplus.egg-info/top_level.txt +0 -0
  93. {etlplus-0.3.17 → etlplus-0.3.21}/examples/README.md +0 -0
  94. {etlplus-0.3.17 → etlplus-0.3.21}/examples/configs/pipeline.yml +0 -0
  95. {etlplus-0.3.17 → etlplus-0.3.21}/examples/data/sample.csv +0 -0
  96. {etlplus-0.3.17 → etlplus-0.3.21}/examples/data/sample.json +0 -0
  97. {etlplus-0.3.17 → etlplus-0.3.21}/examples/data/sample.xml +0 -0
  98. {etlplus-0.3.17 → etlplus-0.3.21}/examples/data/sample.xsd +0 -0
  99. {etlplus-0.3.17 → etlplus-0.3.21}/examples/data/sample.yaml +0 -0
  100. {etlplus-0.3.17 → etlplus-0.3.21}/examples/quickstart_python.py +0 -0
  101. {etlplus-0.3.17 → etlplus-0.3.21}/pyproject.toml +0 -0
  102. {etlplus-0.3.17 → etlplus-0.3.21}/pytest.ini +0 -0
  103. {etlplus-0.3.17 → etlplus-0.3.21}/setup.cfg +0 -0
  104. {etlplus-0.3.17 → etlplus-0.3.21}/setup.py +0 -0
  105. {etlplus-0.3.17 → etlplus-0.3.21}/tests/__init__.py +0 -0
  106. {etlplus-0.3.17 → etlplus-0.3.21}/tests/conftest.py +0 -0
  107. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/conftest.py +0 -0
  108. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_examples_data_parity.py +0 -0
  109. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_pagination_strategy.py +0 -0
  110. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_pipeline_smoke.py +0 -0
  111. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  112. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_run.py +0 -0
  113. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  114. {etlplus-0.3.17 → etlplus-0.3.21}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  115. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/conftest.py +0 -0
  116. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_config.py +0 -0
  117. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_mocks.py +0 -0
  118. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_pagination_client.py +0 -0
  119. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_pagination_config.py +0 -0
  120. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_paginator.py +0 -0
  121. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  122. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/api/test_u_retry_manager.py +0 -0
  123. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/conftest.py +0 -0
  124. {etlplus-0.3.17 → etlplus-0.3.21}/tests/unit/validation/test_u_validation_utils.py +0 -0
  125. {etlplus-0.3.17 → etlplus-0.3.21}/tools/run_pipeline.py +0 -0
  126. {etlplus-0.3.17 → etlplus-0.3.21}/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.21
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.21
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
@@ -91,12 +91,14 @@ tests/unit/test_u_enums.py
91
91
  tests/unit/test_u_extract.py
92
92
  tests/unit/test_u_file.py
93
93
  tests/unit/test_u_load.py
94
+ tests/unit/test_u_main.py
94
95
  tests/unit/test_u_mixins.py
95
96
  tests/unit/test_u_run.py
96
97
  tests/unit/test_u_run_helpers.py
97
98
  tests/unit/test_u_transform.py
98
99
  tests/unit/test_u_utils.py
99
100
  tests/unit/test_u_validate.py
101
+ tests/unit/test_u_version.py
100
102
  tests/unit/api/conftest.py
101
103
  tests/unit/api/test_u_auth.py
102
104
  tests/unit/api/test_u_config.py
@@ -110,7 +112,10 @@ tests/unit/api/test_u_rate_limiter.py
110
112
  tests/unit/api/test_u_request_manager.py
111
113
  tests/unit/api/test_u_retry_manager.py
112
114
  tests/unit/api/test_u_transport.py
115
+ tests/unit/api/test_u_types.py
116
+ tests/unit/config/test_u_config_utils.py
113
117
  tests/unit/config/test_u_connector.py
118
+ tests/unit/config/test_u_jobs.py
114
119
  tests/unit/config/test_u_pipeline.py
115
120
  tests/unit/validation/test_u_validation_utils.py
116
121
  tools/run_pipeline.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 ========================================================== #
@@ -1,7 +1,7 @@
1
1
  """
2
2
  :mod:`tests.unit.api.test_u_auth` module.
3
3
 
4
- Unit tests for ``etlplus.api.auth``.
4
+ Unit tests for :mod:`etlplus.api.auth`.
5
5
 
6
6
  Notes
7
7
  -----
@@ -1,7 +1,7 @@
1
1
  """
2
2
  :mod:`tests.unit.api.test_u_endpoint_client` module.
3
3
 
4
- Unit tests for ``etlplus.api.endpoint_client``.
4
+ Unit tests for :mod:`etlplus.api.endpoint_client`.
5
5
 
6
6
  Notes
7
7
  -----
@@ -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.
@@ -48,7 +48,7 @@ def fixed_limiter_fixture() -> RateLimiter:
48
48
  return RateLimiter.fixed(0.25)
49
49
 
50
50
 
51
- # SECTION: TESTS =========================================================== #
51
+ # SECTION: TESTS ============================================================ #
52
52
 
53
53
 
54
54
  @pytest.mark.unit
@@ -0,0 +1,349 @@
1
+ """
2
+ :mod:`tests.unit.api.test_u_request_manager` module.
3
+
4
+ Unit tests for :class:`etlplus.api.request_manager.RequestManager`.
5
+
6
+ These tests focus on:
7
+
8
+ - Session-adapter plumbing (building and closing sessions).
9
+ - Context-manager semantics (reuse + cleanup).
10
+ - Delegation to request callables.
11
+
12
+ The suite is intentionally lightweight and uses small doubles/mocks rather than
13
+ real network sessions.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+ from typing import cast
22
+ from unittest.mock import Mock
23
+
24
+ import pytest
25
+
26
+ from etlplus.api.request_manager import RequestManager
27
+
28
+ # SECTION: HELPERS ========================================================== #
29
+
30
+
31
+ pytestmark = pytest.mark.unit
32
+
33
+
34
+ def _make_request_callable(
35
+ probe: RequestProbe,
36
+ ) -> Callable[..., dict[str, Any]]:
37
+ """Create a request callable that records inputs into ``probe``."""
38
+
39
+ def _request(
40
+ _method: str,
41
+ url: str,
42
+ *,
43
+ session: Any,
44
+ timeout: Any,
45
+ **kwargs: Any,
46
+ ) -> dict[str, Any]:
47
+ probe.sessions_used.append(session)
48
+ probe.timeouts.append(timeout)
49
+ probe.urls.append(url)
50
+ probe.extra_kwargs.append(kwargs)
51
+ return {'url': url}
52
+
53
+ return _request
54
+
55
+
56
+ @dataclass(slots=True)
57
+ class DummySession:
58
+ """Lightweight session double-tracking ``close`` calls."""
59
+
60
+ closed: bool = False
61
+
62
+ def close(self) -> None:
63
+ """Close the session."""
64
+ self.closed = True
65
+
66
+
67
+ @dataclass(slots=True)
68
+ class SessionBuilderProbe:
69
+ """Callable probe for session-builder usage."""
70
+
71
+ session: DummySession
72
+ calls: list[Any]
73
+
74
+ def __call__(self, cfg: Any) -> DummySession:
75
+ self.calls.append(cfg)
76
+ return self.session
77
+
78
+
79
+ @dataclass(slots=True)
80
+ class RequestProbe:
81
+ """Callable probe for capturing arguments passed to a request callable."""
82
+
83
+ sessions_used: list[Any]
84
+ timeouts: list[Any]
85
+ urls: list[str]
86
+ extra_kwargs: list[dict[str, Any]]
87
+
88
+
89
+ # SECTION: FIXTURES ========================================================= #
90
+
91
+
92
+ @pytest.fixture(name='dummy_session')
93
+ def dummy_session_fixture() -> DummySession:
94
+ """Return a fresh dummy session for each test."""
95
+
96
+ return DummySession()
97
+
98
+
99
+ @pytest.fixture(name='session_builder')
100
+ def session_builder_fixture(
101
+ dummy_session: DummySession,
102
+ ) -> SessionBuilderProbe:
103
+ """Provide a probe callable for adapter-session creation."""
104
+
105
+ return SessionBuilderProbe(session=dummy_session, calls=[])
106
+
107
+
108
+ # SECTION: TESTS ============================================================ #
109
+
110
+
111
+ class TestRequestManager:
112
+ """Unit tests for :class:`etlplus.api.request_manager.RequestManager`."""
113
+
114
+ def test_adapter_session_built_and_closed(
115
+ self,
116
+ monkeypatch: pytest.MonkeyPatch,
117
+ session_builder: SessionBuilderProbe,
118
+ ) -> None:
119
+ """
120
+ Test that adapter configs yield a managed session that gets closed.
121
+ """
122
+
123
+ monkeypatch.setattr(
124
+ 'etlplus.api.request_manager.build_session_with_adapters',
125
+ session_builder,
126
+ )
127
+
128
+ manager = RequestManager(
129
+ session_adapters=[{'prefix': 'https://', 'pool_connections': 2}],
130
+ )
131
+
132
+ probe = RequestProbe([], [], [], [])
133
+ request_callable = _make_request_callable(probe)
134
+
135
+ result = manager.request(
136
+ 'GET',
137
+ 'https://example.com/resource',
138
+ request_callable=request_callable,
139
+ )
140
+
141
+ assert result == {'url': 'https://example.com/resource'}
142
+ assert probe.sessions_used == [session_builder.session]
143
+ assert isinstance(session_builder.calls[0], tuple)
144
+ assert session_builder.session.closed is True
145
+
146
+ def test_context_manager_reuses_adapter_session(
147
+ self,
148
+ monkeypatch: pytest.MonkeyPatch,
149
+ session_builder: SessionBuilderProbe,
150
+ ) -> None:
151
+ """Test that context manager reuses one adapter-backed session."""
152
+
153
+ monkeypatch.setattr(
154
+ 'etlplus.api.request_manager.build_session_with_adapters',
155
+ session_builder,
156
+ )
157
+
158
+ manager = RequestManager(
159
+ session_adapters=[{'prefix': 'https://', 'pool_connections': 1}],
160
+ )
161
+
162
+ probe = RequestProbe([], [], [], [])
163
+ request_callable = _make_request_callable(probe)
164
+
165
+ with manager:
166
+ manager.request(
167
+ 'GET',
168
+ 'https://example.com/a',
169
+ request_callable=request_callable,
170
+ )
171
+ manager.request(
172
+ 'GET',
173
+ 'https://example.com/b',
174
+ request_callable=request_callable,
175
+ )
176
+ assert session_builder.session.closed is False
177
+
178
+ assert session_builder.session.closed is True
179
+ assert len(session_builder.calls) == 1
180
+ assert probe.sessions_used == [session_builder.session] * 2
181
+ assert probe.urls == ['https://example.com/a', 'https://example.com/b']
182
+ assert probe.timeouts == [
183
+ manager.default_timeout,
184
+ manager.default_timeout,
185
+ ]
186
+ assert probe.extra_kwargs == [{}, {}]
187
+
188
+ def test_invalid_session_adapters(
189
+ self,
190
+ monkeypatch: pytest.MonkeyPatch,
191
+ ) -> None:
192
+ """
193
+ Test that bad adapter config does not raise during context enter/exit.
194
+ """
195
+ bad_adapters = cast(
196
+ Any,
197
+ [{'prefix': 'https://', 'pool_connections': 'bad'}],
198
+ )
199
+ manager = RequestManager(session_adapters=bad_adapters)
200
+
201
+ def _bad_builder(cfg: Any) -> None: # pragma: no cover
202
+ raise ValueError('bad config')
203
+
204
+ monkeypatch.setattr(
205
+ 'etlplus.api.request_manager.build_session_with_adapters',
206
+ _bad_builder,
207
+ )
208
+
209
+ # If this raises, pytest will fail the test automatically.
210
+ with manager:
211
+ pass
212
+
213
+ def test_request_delegates_to_request_once(
214
+ self,
215
+ monkeypatch: pytest.MonkeyPatch,
216
+ ) -> None:
217
+ """
218
+ Test that ``request`` passes through the ``request_callable`` to
219
+ :meth:`request_once`.
220
+ """
221
+
222
+ manager = RequestManager()
223
+ request_once = Mock(return_value={'ok': True})
224
+ cb = Mock(return_value={'ok': 'cb'})
225
+
226
+ monkeypatch.setattr(
227
+ type(manager),
228
+ 'request_once',
229
+ staticmethod(request_once),
230
+ )
231
+
232
+ result = manager.request('POST', 'http://test', request_callable=cb)
233
+
234
+ assert result == {'ok': True}
235
+ assert request_once.call_count == 1
236
+ args = request_once.call_args.args
237
+ kwargs = request_once.call_args.kwargs
238
+ assert args[:2] == ('POST', 'http://test')
239
+ assert kwargs['session'] is None
240
+ assert kwargs['timeout'] == manager.default_timeout
241
+ assert kwargs['request_callable'] is cb
242
+
243
+ def test_default_init_values(self) -> None:
244
+ """
245
+ Test that :class:`RequestManager` default initialization values are
246
+ stable and explicit.
247
+ """
248
+ manager = RequestManager()
249
+ assert manager.retry is None
250
+ assert manager.retry_network_errors is False
251
+ assert manager.default_timeout == 10.0
252
+ assert manager.session is None
253
+ assert manager.session_factory is None
254
+ assert manager.retry_cap == 30.0
255
+ assert manager.session_adapters is None
256
+
257
+ def test_context_manager_handles_exceptions(self) -> None:
258
+ """
259
+ Test that :meth:`__exit__` cleans up even when the managed block
260
+ raises an exception.
261
+ """
262
+ # pylint: disable=protected-access
263
+
264
+ manager = RequestManager()
265
+
266
+ class DummyExc(Exception):
267
+ """Dummy exception for context-manager testing."""
268
+
269
+ manager._ctx_session = DummySession()
270
+ manager._ctx_owns_session = True
271
+
272
+ with pytest.raises(DummyExc):
273
+ with manager:
274
+ raise DummyExc()
275
+
276
+ assert manager._ctx_session is None
277
+ assert manager._ctx_owns_session is False
278
+
279
+ def test_request_once_returns_callable(self) -> None:
280
+ """
281
+ Test that :meth:`request_once` returns the underlying callable's
282
+ result.
283
+ """
284
+ # pylint: disable=unused-argument
285
+
286
+ manager = RequestManager()
287
+
288
+ def _callable(*args: Any, **kwargs: Any) -> dict[str, Any]:
289
+ return {'ok': True}
290
+
291
+ result = manager.request_once(
292
+ 'GET',
293
+ 'http://x',
294
+ session=None,
295
+ timeout=1,
296
+ request_callable=_callable,
297
+ )
298
+
299
+ assert result == {'ok': True}
300
+
301
+ @pytest.mark.parametrize(
302
+ ('api_method', 'expected_method'),
303
+ [('get', 'GET'), ('post', 'POST')],
304
+ )
305
+ def test_http_shortcuts_delegate_to_request_once(
306
+ self,
307
+ monkeypatch: pytest.MonkeyPatch,
308
+ api_method: str,
309
+ expected_method: str,
310
+ ) -> None:
311
+ """
312
+ Test that ``GET``/``POST`` call into :meth:`request_once` with the
313
+ right method.
314
+ """
315
+
316
+ manager = RequestManager()
317
+ request_once = Mock(return_value={'ok': True})
318
+
319
+ monkeypatch.setattr(
320
+ type(manager),
321
+ 'request_once',
322
+ staticmethod(request_once),
323
+ )
324
+
325
+ func = getattr(manager, api_method)
326
+ assert callable(func)
327
+
328
+ assert func('http://x') == {'ok': True}
329
+ assert request_once.call_args.args[:2] == (expected_method, 'http://x')
330
+
331
+ def test_request_accepts_unknown_methods(
332
+ self,
333
+ monkeypatch: pytest.MonkeyPatch,
334
+ ) -> None:
335
+ """
336
+ Test that unknown HTTP method strings are passed through unchanged.
337
+ """
338
+
339
+ manager = RequestManager()
340
+ request_once = Mock(return_value={'ok': True})
341
+
342
+ monkeypatch.setattr(
343
+ type(manager),
344
+ 'request_once',
345
+ staticmethod(request_once),
346
+ )
347
+
348
+ assert manager.request('FOO', 'http://x') == {'ok': True}
349
+ assert request_once.call_args.args[:2] == ('FOO', 'http://x')
@@ -1,7 +1,7 @@
1
1
  """
2
2
  :mod:`tests.unit.api.test_u_transport` module.
3
3
 
4
- Unit tests for ``etlplus.api.transport``.
4
+ Unit tests for :mod:`etlplus.api.transport`.
5
5
 
6
6
  Notes
7
7
  -----
@@ -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,52 @@ 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
+ # pylint: disable=broad-exception-caught
105
+ adapters_cfg = [
106
+ {'prefix': 'https://', 'pool_connections': 'bad'},
107
+ {'prefix': 'http://', 'max_retries': {'total': 'bad'}},
108
+ ]
109
+ session = requests.Session()
110
+ # Should not raise
111
+ try:
112
+ session = build_session_with_adapters(adapters_cfg)
113
+ except Exception:
114
+ pytest.fail('build_session_with_adapters should not raise')
115
+ assert isinstance(session, requests.Session)
116
+
70
117
  def test_integer_retries_fallback(self) -> None:
71
118
  """Test handling integer max_retries fallback."""
72
119
  cfg = {