etlplus 0.4.8__tar.gz → 0.4.9__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 (130) hide show
  1. {etlplus-0.4.8/etlplus.egg-info → etlplus-0.4.9}/PKG-INFO +1 -1
  2. {etlplus-0.4.8 → etlplus-0.4.9/etlplus.egg-info}/PKG-INFO +1 -1
  3. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/cli/test_u_cli_app.py +207 -186
  4. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/cli/test_u_cli_handlers.py +58 -35
  5. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/cli/test_u_cli_main.py +88 -68
  6. {etlplus-0.4.8 → etlplus-0.4.9}/.coveragerc +0 -0
  7. {etlplus-0.4.8 → etlplus-0.4.9}/.editorconfig +0 -0
  8. {etlplus-0.4.8 → etlplus-0.4.9}/.gitattributes +0 -0
  9. {etlplus-0.4.8 → etlplus-0.4.9}/.github/actions/python-bootstrap/action.yml +0 -0
  10. {etlplus-0.4.8 → etlplus-0.4.9}/.github/workflows/ci.yml +0 -0
  11. {etlplus-0.4.8 → etlplus-0.4.9}/.gitignore +0 -0
  12. {etlplus-0.4.8 → etlplus-0.4.9}/.pre-commit-config.yaml +0 -0
  13. {etlplus-0.4.8 → etlplus-0.4.9}/.ruff.toml +0 -0
  14. {etlplus-0.4.8 → etlplus-0.4.9}/CODE_OF_CONDUCT.md +0 -0
  15. {etlplus-0.4.8 → etlplus-0.4.9}/CONTRIBUTING.md +0 -0
  16. {etlplus-0.4.8 → etlplus-0.4.9}/DEMO.md +0 -0
  17. {etlplus-0.4.8 → etlplus-0.4.9}/LICENSE +0 -0
  18. {etlplus-0.4.8 → etlplus-0.4.9}/Makefile +0 -0
  19. {etlplus-0.4.8 → etlplus-0.4.9}/README.md +0 -0
  20. {etlplus-0.4.8 → etlplus-0.4.9}/REFERENCES.md +0 -0
  21. {etlplus-0.4.8 → etlplus-0.4.9}/docs/pipeline-guide.md +0 -0
  22. {etlplus-0.4.8 → etlplus-0.4.9}/docs/snippets/installation_version.md +0 -0
  23. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/__init__.py +0 -0
  24. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/__main__.py +0 -0
  25. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/__version__.py +0 -0
  26. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/README.md +0 -0
  27. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/__init__.py +0 -0
  28. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/auth.py +0 -0
  29. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/config.py +0 -0
  30. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/endpoint_client.py +0 -0
  31. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/errors.py +0 -0
  32. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/pagination/__init__.py +0 -0
  33. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/pagination/client.py +0 -0
  34. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/pagination/config.py +0 -0
  35. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/pagination/paginator.py +0 -0
  36. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/rate_limiting/__init__.py +0 -0
  37. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/rate_limiting/config.py +0 -0
  38. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  39. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/request_manager.py +0 -0
  40. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/retry_manager.py +0 -0
  41. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/transport.py +0 -0
  42. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/api/types.py +0 -0
  43. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/cli/__init__.py +0 -0
  44. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/cli/app.py +0 -0
  45. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/cli/handlers.py +0 -0
  46. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/cli/main.py +0 -0
  47. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/__init__.py +0 -0
  48. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/connector.py +0 -0
  49. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/jobs.py +0 -0
  50. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/pipeline.py +0 -0
  51. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/profile.py +0 -0
  52. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/types.py +0 -0
  53. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/config/utils.py +0 -0
  54. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/enums.py +0 -0
  55. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/extract.py +0 -0
  56. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/file.py +0 -0
  57. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/load.py +0 -0
  58. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/mixins.py +0 -0
  59. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/py.typed +0 -0
  60. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/run.py +0 -0
  61. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/run_helpers.py +0 -0
  62. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/transform.py +0 -0
  63. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/types.py +0 -0
  64. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/utils.py +0 -0
  65. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/validate.py +0 -0
  66. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/validation/__init__.py +0 -0
  67. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus/validation/utils.py +0 -0
  68. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus.egg-info/SOURCES.txt +0 -0
  69. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus.egg-info/dependency_links.txt +0 -0
  70. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus.egg-info/entry_points.txt +0 -0
  71. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus.egg-info/requires.txt +0 -0
  72. {etlplus-0.4.8 → etlplus-0.4.9}/etlplus.egg-info/top_level.txt +0 -0
  73. {etlplus-0.4.8 → etlplus-0.4.9}/examples/README.md +0 -0
  74. {etlplus-0.4.8 → etlplus-0.4.9}/examples/configs/pipeline.yml +0 -0
  75. {etlplus-0.4.8 → etlplus-0.4.9}/examples/data/sample.csv +0 -0
  76. {etlplus-0.4.8 → etlplus-0.4.9}/examples/data/sample.json +0 -0
  77. {etlplus-0.4.8 → etlplus-0.4.9}/examples/data/sample.xml +0 -0
  78. {etlplus-0.4.8 → etlplus-0.4.9}/examples/data/sample.xsd +0 -0
  79. {etlplus-0.4.8 → etlplus-0.4.9}/examples/data/sample.yaml +0 -0
  80. {etlplus-0.4.8 → etlplus-0.4.9}/examples/quickstart_python.py +0 -0
  81. {etlplus-0.4.8 → etlplus-0.4.9}/pyproject.toml +0 -0
  82. {etlplus-0.4.8 → etlplus-0.4.9}/pytest.ini +0 -0
  83. {etlplus-0.4.8 → etlplus-0.4.9}/setup.cfg +0 -0
  84. {etlplus-0.4.8 → etlplus-0.4.9}/setup.py +0 -0
  85. {etlplus-0.4.8 → etlplus-0.4.9}/tests/__init__.py +0 -0
  86. {etlplus-0.4.8 → etlplus-0.4.9}/tests/conftest.py +0 -0
  87. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/conftest.py +0 -0
  88. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_cli.py +0 -0
  89. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_examples_data_parity.py +0 -0
  90. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_pagination_strategy.py +0 -0
  91. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_pipeline_smoke.py +0 -0
  92. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  93. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_run.py +0 -0
  94. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  95. {etlplus-0.4.8 → etlplus-0.4.9}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  96. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/conftest.py +0 -0
  97. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_auth.py +0 -0
  98. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_config.py +0 -0
  99. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_endpoint_client.py +0 -0
  100. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_mocks.py +0 -0
  101. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_pagination_client.py +0 -0
  102. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_pagination_config.py +0 -0
  103. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_paginator.py +0 -0
  104. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  105. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_rate_limiter.py +0 -0
  106. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_request_manager.py +0 -0
  107. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_retry_manager.py +0 -0
  108. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_transport.py +0 -0
  109. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/api/test_u_types.py +0 -0
  110. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/cli/conftest.py +0 -0
  111. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/config/test_u_config_utils.py +0 -0
  112. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/config/test_u_connector.py +0 -0
  113. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/config/test_u_jobs.py +0 -0
  114. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/config/test_u_pipeline.py +0 -0
  115. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/conftest.py +0 -0
  116. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_enums.py +0 -0
  117. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_extract.py +0 -0
  118. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_file.py +0 -0
  119. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_load.py +0 -0
  120. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_main.py +0 -0
  121. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_mixins.py +0 -0
  122. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_run.py +0 -0
  123. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_run_helpers.py +0 -0
  124. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_transform.py +0 -0
  125. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_utils.py +0 -0
  126. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_validate.py +0 -0
  127. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/test_u_version.py +0 -0
  128. {etlplus-0.4.8 → etlplus-0.4.9}/tests/unit/validation/test_u_validation_utils.py +0 -0
  129. {etlplus-0.4.8 → etlplus-0.4.9}/tools/run_pipeline.py +0 -0
  130. {etlplus-0.4.8 → etlplus-0.4.9}/tools/update_demo_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.4.8
3
+ Version: 0.4.9
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.4.8
3
+ Version: 0.4.9
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
@@ -14,6 +14,7 @@ from unittest.mock import Mock
14
14
  import pytest
15
15
  import typer
16
16
  from typer.testing import CliRunner
17
+ from typer.testing import Result
17
18
 
18
19
  import etlplus
19
20
  import etlplus.cli.app as cli_app_module
@@ -24,7 +25,8 @@ from etlplus.cli.app import app as cli_app
24
25
 
25
26
  pytestmark = pytest.mark.unit
26
27
 
27
- CaptureHelper = Callable[[str], tuple[dict[str, object], Mock]]
28
+ CaptureHelper = Callable[[str], tuple[dict[str, argparse.Namespace], Mock]]
29
+ InvokeCli = Callable[..., tuple[Result, argparse.Namespace, Mock]]
28
30
 
29
31
 
30
32
  # SECTION: FIXTURES ========================================================= #
@@ -48,8 +50,8 @@ def capture_cmd_fixture(
48
50
  passed to the handler.
49
51
  """
50
52
 
51
- def _capture(name: str) -> tuple[dict[str, object], Mock]:
52
- captured: dict[str, object] = {}
53
+ def _capture(name: str) -> tuple[dict[str, argparse.Namespace], Mock]:
54
+ captured: dict[str, argparse.Namespace] = {}
53
55
 
54
56
  def _fake(ns: argparse.Namespace) -> int:
55
57
  captured['ns'] = ns
@@ -62,27 +64,71 @@ def capture_cmd_fixture(
62
64
  return _capture
63
65
 
64
66
 
67
+ @pytest.fixture(name='invoke_cli')
68
+ def invoke_cli_fixture(
69
+ runner: CliRunner,
70
+ capture_cmd: CaptureHelper,
71
+ ) -> InvokeCli:
72
+ """Invoke the Typer CLI and capture the patched handler call.
73
+
74
+ Parameters
75
+ ----------
76
+ runner : CliRunner
77
+ Typer CLI runner fixture.
78
+ capture_cmd : CaptureHelper
79
+ Helper that patches handler bindings and records the namespace.
80
+
81
+ Returns
82
+ -------
83
+ InvokeCli
84
+ Callable that invokes the CLI, returning the ``Result``, handler
85
+ namespace, and mock used for assertion.
86
+ """
87
+
88
+ def _invoke(
89
+ handler: str,
90
+ *args: str,
91
+ ) -> tuple[Result, argparse.Namespace, Mock]:
92
+ captured, cmd = capture_cmd(handler)
93
+ result = runner.invoke(cli_app, list(args))
94
+ return result, captured['ns'], cmd
95
+
96
+ return _invoke
97
+
98
+
65
99
  # SECTION: TESTS ============================================================ #
66
100
 
67
101
 
68
102
  class TestCliAppInternalHelpers:
69
103
  """Unit tests for private helper utilities."""
70
104
 
71
- def test_infer_resource_type_variants(self, tmp_path: Path) -> None:
72
- """`_infer_resource_type` recognizes URLs, DBs, files, and stdin."""
105
+ @pytest.mark.parametrize(
106
+ ('raw', 'expected'),
107
+ (
108
+ ('-', 'file'),
109
+ ('https://example.com/data.json', 'api'),
110
+ ('postgres://user@host/db', 'database'),
111
+ ),
112
+ )
113
+ def test_infer_resource_type_variants(
114
+ self,
115
+ raw: str,
116
+ expected: str,
117
+ ) -> None:
118
+ """
119
+ Test that :func:`_infer_resource_type` classifies common resource
120
+ inputs.
121
+ """
73
122
  # pylint: disable=protected-access
74
123
 
75
- assert cli_app_module._infer_resource_type('-') == 'file'
76
- assert (
77
- cli_app_module._infer_resource_type(
78
- 'https://example.com/data.json',
79
- )
80
- == 'api'
81
- )
82
- assert (
83
- cli_app_module._infer_resource_type('postgres://user@host/db')
84
- == 'database'
85
- )
124
+ assert cli_app_module._infer_resource_type(raw) == expected
125
+
126
+ def test_infer_resource_type_file_path(self, tmp_path: Path) -> None:
127
+ """
128
+ Test that :func:`_infer_resource_type` detects local files via
129
+ extension parsing.
130
+ """
131
+ # pylint: disable=protected-access
86
132
 
87
133
  path = tmp_path / 'payload.csv'
88
134
  path.write_text('a,b\n1,2\n', encoding='utf-8')
@@ -97,33 +143,40 @@ class TestCliAppInternalHelpers:
97
143
  with pytest.raises(ValueError):
98
144
  cli_app_module._infer_resource_type('unknown-resource')
99
145
 
100
- def test_optional_choice_passthrough_and_validation(self) -> None:
101
- """`_optional_choice` preserves None and validates provided values."""
146
+ @pytest.mark.parametrize(
147
+ ('choice', 'expected'),
148
+ ((None, None), ('json', 'json')),
149
+ )
150
+ def test_optional_choice_passthrough_and_validation(
151
+ self,
152
+ choice: str | None,
153
+ expected: str | None,
154
+ ) -> None:
155
+ """
156
+ Test that :func:`_optional_choice` preserves ``None`` and normalizes
157
+ valid values.
158
+ """
102
159
  # pylint: disable=protected-access
103
160
 
104
161
  assert (
105
162
  cli_app_module._optional_choice(
106
- None,
163
+ choice,
107
164
  {'json', 'csv'},
108
165
  label='format',
109
166
  )
110
- is None
167
+ == expected
111
168
  )
112
169
 
113
- assert (
114
- cli_app_module._optional_choice(
115
- 'json',
116
- {'json', 'csv'},
117
- label='format',
118
- )
119
- == 'json'
120
- )
170
+ @pytest.mark.parametrize('invalid', ('yaml', 'parquet'))
171
+ def test_optional_choice_rejects_invalid(self, invalid: str) -> None:
172
+ """Test that invalid choices raise :class:`typer.BadParameter`."""
173
+ # pylint: disable=protected-access
121
174
 
122
175
  with pytest.raises(typer.BadParameter):
123
- cli_app_module._optional_choice('yaml', {'json'}, label='format')
176
+ cli_app_module._optional_choice(invalid, {'json'}, label='format')
124
177
 
125
178
  def test_stateful_namespace_includes_cli_flags(self) -> None:
126
- """State flags propagate into handler namespaces."""
179
+ """Test that state flags propagate into handler namespaces."""
127
180
  # pylint: disable=protected-access
128
181
 
129
182
  state = cli_app_module.CliState(pretty=False, quiet=True, verbose=True)
@@ -144,25 +197,23 @@ class TestTyperCliAppWiring:
144
197
 
145
198
  def test_extract_default_format_maps_namespace(
146
199
  self,
147
- runner: CliRunner,
148
- capture_cmd: CaptureHelper,
200
+ invoke_cli: InvokeCli,
149
201
  ) -> None:
150
202
  """
151
- Test that the ``extract`` command defaults to JSON and marks the data
152
- format as implicit.
203
+ Test that ``extract`` defaults to JSON and marks the data format as
204
+ implicit.
153
205
  """
154
206
  # pylint: disable=protected-access
155
207
 
156
- captured, cmd = capture_cmd('cmd_extract')
157
- result = runner.invoke(
158
- cli_app,
159
- ['extract', '/path/to/file.json'],
208
+ result, ns, cmd = invoke_cli(
209
+ 'cmd_extract',
210
+ 'extract',
211
+ '/path/to/file.json',
160
212
  )
161
213
 
162
214
  assert result.exit_code == 0
163
215
  cmd.assert_called_once()
164
216
 
165
- ns = captured['ns']
166
217
  assert isinstance(ns, argparse.Namespace)
167
218
  assert ns.command == 'extract'
168
219
  assert ns.source_type == 'file'
@@ -172,57 +223,51 @@ class TestTyperCliAppWiring:
172
223
 
173
224
  def test_extract_explicit_format_maps_namespace(
174
225
  self,
175
- runner: CliRunner,
176
- capture_cmd: CaptureHelper,
226
+ invoke_cli: InvokeCli,
177
227
  ) -> None:
178
228
  """
179
- Test that the ``extract`` command marks the data format as explicit
180
- when provided.
229
+ Test that ``extract`` marks the data format as explicit when provided.
181
230
  """
182
231
  # pylint: disable=protected-access
183
232
 
184
- captured, cmd = capture_cmd('cmd_extract')
185
- result = runner.invoke(
186
- cli_app,
187
- ['extract', '/path/to/file.csv', '--source-format', 'csv'],
233
+ result, ns, cmd = invoke_cli(
234
+ 'cmd_extract',
235
+ 'extract',
236
+ '/path/to/file.csv',
237
+ '--source-format',
238
+ 'csv',
188
239
  )
189
240
 
190
241
  assert result.exit_code == 0
191
242
  cmd.assert_called_once()
192
243
 
193
- ns = captured['ns']
194
244
  assert isinstance(ns, argparse.Namespace)
195
245
  assert ns.format == 'csv'
196
246
  assert ns._format_explicit is True
197
247
 
198
248
  def test_extract_from_option_sets_source_type_and_state_flags(
199
249
  self,
200
- runner: CliRunner,
201
- capture_cmd: CaptureHelper,
250
+ invoke_cli: InvokeCli,
202
251
  ) -> None:
203
252
  """
204
- Test that providing the ``--from`` command-line option and root flags
205
- influence the handler namespace.
253
+ Test that root flags propagate into the handler namespace for
254
+ ``extract``.
206
255
  """
207
256
  # pylint: disable=protected-access
208
257
 
209
- captured, cmd = capture_cmd('cmd_extract')
210
- result = runner.invoke(
211
- cli_app,
212
- [
213
- '--no-pretty',
214
- '--quiet',
215
- 'extract',
216
- '--source-type',
217
- 'api',
218
- 'https://example.com/data.json',
219
- ],
258
+ result, ns, cmd = invoke_cli(
259
+ 'cmd_extract',
260
+ '--no-pretty',
261
+ '--quiet',
262
+ 'extract',
263
+ '--source-type',
264
+ 'api',
265
+ 'https://example.com/data.json',
220
266
  )
221
267
 
222
268
  assert result.exit_code == 0
223
269
  cmd.assert_called_once()
224
270
 
225
- ns = captured['ns']
226
271
  assert isinstance(ns, argparse.Namespace)
227
272
  assert ns.source_type == 'api'
228
273
  assert ns.source == 'https://example.com/data.json'
@@ -232,22 +277,22 @@ class TestTyperCliAppWiring:
232
277
 
233
278
  def test_list_maps_flags(
234
279
  self,
235
- runner: CliRunner,
236
- capture_cmd: CaptureHelper,
280
+ invoke_cli: InvokeCli,
237
281
  ) -> None:
238
282
  """
239
- Test that the ``list`` command maps section flags into the expected
240
- namespace.
283
+ Test that ``list`` maps section flags into the handler namespace.
241
284
  """
242
- captured, cmd = capture_cmd('cmd_list')
243
- result = runner.invoke(
244
- cli_app,
245
- ['list', '--config', 'p.yml', '--pipelines', '--sources'],
285
+ result, ns, cmd = invoke_cli(
286
+ 'cmd_list',
287
+ 'list',
288
+ '--config',
289
+ 'p.yml',
290
+ '--pipelines',
291
+ '--sources',
246
292
  )
247
293
  assert result.exit_code == 0
248
294
  cmd.assert_called_once()
249
295
 
250
- ns = captured['ns']
251
296
  assert isinstance(ns, argparse.Namespace)
252
297
  assert ns.command == 'list'
253
298
  assert ns.config == 'p.yml'
@@ -256,25 +301,25 @@ class TestTyperCliAppWiring:
256
301
 
257
302
  def test_load_default_format_maps_namespace(
258
303
  self,
259
- runner: CliRunner,
260
- capture_cmd: CaptureHelper,
304
+ invoke_cli: InvokeCli,
261
305
  ) -> None:
262
306
  """
263
- Test that the `load` command defaults to JSON and marks the data format
264
- as implicit.
307
+ Test that ``load`` defaults to JSON and marks the data format as
308
+ implicit.
265
309
  """
266
310
  # pylint: disable=protected-access
267
311
 
268
- captured, cmd = capture_cmd('cmd_load')
269
- result = runner.invoke(
270
- cli_app,
271
- ['load', '--target-type', 'file', '/path/to/out.json'],
312
+ result, ns, cmd = invoke_cli(
313
+ 'cmd_load',
314
+ 'load',
315
+ '--target-type',
316
+ 'file',
317
+ '/path/to/out.json',
272
318
  )
273
319
 
274
320
  assert result.exit_code == 0
275
321
  cmd.assert_called_once()
276
322
 
277
- ns = captured['ns']
278
323
  assert isinstance(ns, argparse.Namespace)
279
324
  assert ns.command == 'load'
280
325
  assert ns.source == '-'
@@ -285,30 +330,25 @@ class TestTyperCliAppWiring:
285
330
 
286
331
  def test_load_explicit_format_maps_namespace(
287
332
  self,
288
- runner: CliRunner,
289
- capture_cmd: CaptureHelper,
333
+ invoke_cli: InvokeCli,
290
334
  ) -> None:
291
335
  """
292
- Test that the ``load`` command marks the data format as explicit when
336
+ Test that ``load`` marks the target data format as explicit when
293
337
  provided.
294
338
  """
295
339
  # pylint: disable=protected-access
296
340
 
297
- captured, cmd = capture_cmd('cmd_load')
298
- result = runner.invoke(
299
- cli_app,
300
- [
301
- 'load',
302
- '--target-format',
303
- 'csv',
304
- '/path/to/out.csv',
305
- ],
341
+ result, ns, cmd = invoke_cli(
342
+ 'cmd_load',
343
+ 'load',
344
+ '--target-format',
345
+ 'csv',
346
+ '/path/to/out.csv',
306
347
  )
307
348
 
308
349
  assert result.exit_code == 0
309
350
  cmd.assert_called_once()
310
351
 
311
- ns = captured['ns']
312
352
  assert isinstance(ns, argparse.Namespace)
313
353
  assert ns.source == '-'
314
354
  assert ns.target_type == 'file'
@@ -317,29 +357,24 @@ class TestTyperCliAppWiring:
317
357
 
318
358
  def test_load_to_option_defaults_source_to_stdin(
319
359
  self,
320
- runner: CliRunner,
321
- capture_cmd: CaptureHelper,
360
+ invoke_cli: InvokeCli,
322
361
  ) -> None:
323
362
  """
324
- Test that ``source`` defaults to '-' and ``--to`` wins when only TARGET
325
- is provided wins.
363
+ Test that ``load`` defaults to stdin when only target options are
364
+ provided.
326
365
  """
327
366
 
328
- captured, cmd = capture_cmd('cmd_load')
329
- result = runner.invoke(
330
- cli_app,
331
- [
332
- 'load',
333
- '--target-type',
334
- 'database',
335
- 'postgres://db.example.org/app',
336
- ],
367
+ result, ns, cmd = invoke_cli(
368
+ 'cmd_load',
369
+ 'load',
370
+ '--target-type',
371
+ 'database',
372
+ 'postgres://db.example.org/app',
337
373
  )
338
374
 
339
375
  assert result.exit_code == 0
340
376
  cmd.assert_called_once()
341
377
 
342
- ns = captured['ns']
343
378
  assert isinstance(ns, argparse.Namespace)
344
379
  assert ns.source == '-'
345
380
  assert ns.target == 'postgres://db.example.org/app'
@@ -353,23 +388,21 @@ class TestTyperCliAppWiring:
353
388
 
354
389
  def test_pipeline_maps_flags(
355
390
  self,
356
- runner: CliRunner,
357
- capture_cmd: CaptureHelper,
391
+ invoke_cli: InvokeCli,
358
392
  ) -> None:
359
393
  """
360
- Test that the `pipeline` command maps the ``--list`` command-line
361
- argument and the ``--run`` command-line argument into the expected
362
- namespace.
394
+ Test that ``pipeline`` maps list flags into the handler namespace.
363
395
  """
364
- captured, cmd = capture_cmd('cmd_pipeline')
365
- result = runner.invoke(
366
- cli_app,
367
- ['pipeline', '--config', 'p.yml', '--jobs'],
396
+ result, ns, cmd = invoke_cli(
397
+ 'cmd_pipeline',
398
+ 'pipeline',
399
+ '--config',
400
+ 'p.yml',
401
+ '--jobs',
368
402
  )
369
403
  assert result.exit_code == 0
370
404
  cmd.assert_called_once()
371
405
 
372
- ns = captured['ns']
373
406
  assert isinstance(ns, argparse.Namespace)
374
407
  assert ns.command == 'pipeline'
375
408
  assert ns.config == 'p.yml'
@@ -378,42 +411,44 @@ class TestTyperCliAppWiring:
378
411
 
379
412
  def test_pipeline_run_sets_run_option(
380
413
  self,
381
- runner: CliRunner,
382
- capture_cmd: CaptureHelper,
414
+ invoke_cli: InvokeCli,
383
415
  ) -> None:
384
- """`pipeline --run` wires run metadata into the namespace."""
385
-
386
- captured, cmd = capture_cmd('cmd_pipeline')
387
- result = runner.invoke(
388
- cli_app,
389
- ['pipeline', '--config', 'p.yml', '--job', 'job-2'],
416
+ """
417
+ Test that ``pipeline --job`` wires run metadata into the namespace.
418
+ """
419
+ result, ns, cmd = invoke_cli(
420
+ 'cmd_pipeline',
421
+ 'pipeline',
422
+ '--config',
423
+ 'p.yml',
424
+ '--job',
425
+ 'job-2',
390
426
  )
391
427
 
392
428
  assert result.exit_code == 0
393
429
  cmd.assert_called_once()
394
- ns = captured['ns']
395
430
  assert isinstance(ns, argparse.Namespace)
396
431
  assert ns.run == 'job-2'
397
432
  assert ns.list is False
398
433
 
399
434
  def test_run_maps_flags(
400
435
  self,
401
- runner: CliRunner,
402
- capture_cmd: CaptureHelper,
436
+ invoke_cli: InvokeCli,
403
437
  ) -> None:
404
438
  """
405
- Test that the ``run`` command maps the ``--job``/``--pipeline``
406
- command-line argument into the expected namespace.
439
+ Test that ``run`` maps job flags into the handler namespace.
407
440
  """
408
- captured, cmd = capture_cmd('cmd_run')
409
- result = runner.invoke(
410
- cli_app,
411
- ['run', '--config', 'p.yml', '--job', 'j1'],
441
+ result, ns, cmd = invoke_cli(
442
+ 'cmd_run',
443
+ 'run',
444
+ '--config',
445
+ 'p.yml',
446
+ '--job',
447
+ 'j1',
412
448
  )
413
449
  assert result.exit_code == 0
414
450
  cmd.assert_called_once()
415
451
 
416
- ns = captured['ns']
417
452
  assert isinstance(ns, argparse.Namespace)
418
453
  assert ns.command == 'run'
419
454
  assert ns.config == 'p.yml'
@@ -421,28 +456,23 @@ class TestTyperCliAppWiring:
421
456
 
422
457
  def test_transform_parses_operations_json(
423
458
  self,
424
- runner: CliRunner,
425
- capture_cmd: CaptureHelper,
459
+ invoke_cli: InvokeCli,
426
460
  ) -> None:
427
461
  """
428
- Test that the ``transform`` command parses ``--operations``
429
- command-line argument via ``json_type``.
430
- """
431
- captured, cmd = capture_cmd('cmd_transform')
432
- result = runner.invoke(
433
- cli_app,
434
- [
435
- 'transform',
436
- '/path/to/file.json',
437
- '--operations',
438
- '{"select": ["id"]}',
439
- ],
462
+ Test that ``transform`` parses JSON operations passed via
463
+ ``--operations``.
464
+ """
465
+ result, ns, cmd = invoke_cli(
466
+ 'cmd_transform',
467
+ 'transform',
468
+ '/path/to/file.json',
469
+ '--operations',
470
+ '{"select": ["id"]}',
440
471
  )
441
472
 
442
473
  assert result.exit_code == 0
443
474
  cmd.assert_called_once()
444
475
 
445
- ns = captured['ns']
446
476
  assert isinstance(ns, argparse.Namespace)
447
477
  assert ns.command == 'transform'
448
478
  assert ns.source == '/path/to/file.json'
@@ -451,49 +481,42 @@ class TestTyperCliAppWiring:
451
481
 
452
482
  def test_transform_respects_source_format(
453
483
  self,
454
- runner: CliRunner,
455
- capture_cmd: CaptureHelper,
484
+ invoke_cli: InvokeCli,
456
485
  ) -> None:
457
486
  """
458
- Test that the command ``etlplus transform --source-format csv``
459
- propagates to the namespace.
487
+ Test that ``transform`` propagates ``--source-format`` into the
488
+ namespace.
460
489
  """
461
- captured, cmd = capture_cmd('cmd_transform')
462
- result = runner.invoke(
463
- cli_app,
464
- ['transform', '--source-format', 'csv'],
490
+ result, ns, cmd = invoke_cli(
491
+ 'cmd_transform',
492
+ 'transform',
493
+ '--source-format',
494
+ 'csv',
465
495
  )
466
496
 
467
497
  assert result.exit_code == 0
468
498
  cmd.assert_called_once()
469
- ns = captured['ns']
470
499
  assert isinstance(ns, argparse.Namespace)
471
500
  assert ns.source_format == 'csv'
472
501
 
473
502
  def test_validate_parses_rules_json(
474
503
  self,
475
- runner: CliRunner,
476
- capture_cmd: CaptureHelper,
504
+ invoke_cli: InvokeCli,
477
505
  ) -> None:
478
506
  """
479
- Test that the command ``validate`` parses the ``--rules`` command-line
480
- argument via ``json_type``.
481
- """
482
- captured, cmd = capture_cmd('cmd_validate')
483
- result = runner.invoke(
484
- cli_app,
485
- [
486
- 'validate',
487
- '/path/to/file.json',
488
- '--rules',
489
- '{"required": ["id"]}',
490
- ],
507
+ Test that ``validate`` parses JSON rules passed via ``--rules``.
508
+ """
509
+ result, ns, cmd = invoke_cli(
510
+ 'cmd_validate',
511
+ 'validate',
512
+ '/path/to/file.json',
513
+ '--rules',
514
+ '{"required": ["id"]}',
491
515
  )
492
516
 
493
517
  assert result.exit_code == 0
494
518
  cmd.assert_called_once()
495
519
 
496
- ns = captured['ns']
497
520
  assert isinstance(ns, argparse.Namespace)
498
521
  assert ns.command == 'validate'
499
522
  assert ns.source == '/path/to/file.json'
@@ -502,24 +525,22 @@ class TestTyperCliAppWiring:
502
525
 
503
526
  def test_validate_respects_source_format(
504
527
  self,
505
- runner: CliRunner,
506
- capture_cmd: CaptureHelper,
528
+ invoke_cli: InvokeCli,
507
529
  ) -> None:
508
530
  """
509
- Test that command ``validate --source-format csv`` sanitizes into a
510
- handler namespace.
531
+ Test that ``validate`` propagates ``--source-format`` into the
532
+ namespace.
511
533
  """
512
-
513
- captured, cmd = capture_cmd('cmd_validate')
514
- result = runner.invoke(
515
- cli_app,
516
- ['validate', '--source-format', 'csv'],
534
+ result, ns, cmd = invoke_cli(
535
+ 'cmd_validate',
536
+ 'validate',
537
+ '--source-format',
538
+ 'csv',
517
539
  )
518
540
 
519
541
  assert result.exit_code == 0
520
542
  cmd.assert_called_once()
521
543
 
522
- ns = captured['ns']
523
544
  assert isinstance(ns, argparse.Namespace)
524
545
  assert ns.source_format == 'csv'
525
546