etlplus 0.4.5__tar.gz → 0.4.7__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.4.5 → etlplus-0.4.7}/.pre-commit-config.yaml +5 -1
- {etlplus-0.4.5/etlplus.egg-info → etlplus-0.4.7}/PKG-INFO +1 -1
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/cli/main.py +131 -26
- {etlplus-0.4.5 → etlplus-0.4.7/etlplus.egg-info}/PKG-INFO +1 -1
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_main.py +78 -5
- {etlplus-0.4.5 → etlplus-0.4.7}/.coveragerc +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.editorconfig +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.gitattributes +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.github/actions/python-bootstrap/action.yml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.github/workflows/ci.yml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.gitignore +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/.ruff.toml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/CODE_OF_CONDUCT.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/CONTRIBUTING.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/DEMO.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/LICENSE +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/Makefile +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/README.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/REFERENCES.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/docs/pipeline-guide.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/docs/snippets/installation_version.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/__main__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/__version__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/README.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/auth.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/endpoint_client.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/errors.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/pagination/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/pagination/client.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/pagination/config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/pagination/paginator.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/rate_limiting/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/rate_limiting/config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/request_manager.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/retry_manager.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/transport.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/api/types.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/cli/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/cli/app.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/cli/handlers.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/connector.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/jobs.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/pipeline.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/profile.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/types.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/config/utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/enums.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/extract.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/file.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/load.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/mixins.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/py.typed +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/run.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/run_helpers.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/transform.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/types.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/validate.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/validation/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus/validation/utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus.egg-info/SOURCES.txt +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus.egg-info/dependency_links.txt +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus.egg-info/entry_points.txt +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus.egg-info/requires.txt +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/etlplus.egg-info/top_level.txt +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/README.md +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/configs/pipeline.yml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/data/sample.csv +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/data/sample.json +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/data/sample.xml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/data/sample.xsd +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/data/sample.yaml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/examples/quickstart_python.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/pyproject.toml +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/pytest.ini +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/setup.cfg +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/setup.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/__init__.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/conftest.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/conftest.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_cli.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_examples_data_parity.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_pagination_strategy.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_pipeline_smoke.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_run.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/conftest.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_auth.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_endpoint_client.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_mocks.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_pagination_client.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_pagination_config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_paginator.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_rate_limit_config.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_rate_limiter.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_request_manager.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_retry_manager.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_transport.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/api/test_u_types.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/cli/conftest.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_app.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_handlers.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/config/test_u_config_utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/config/test_u_connector.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/config/test_u_jobs.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/config/test_u_pipeline.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/conftest.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_enums.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_extract.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_file.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_load.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_main.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_mixins.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_run.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_run_helpers.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_transform.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_validate.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/test_u_version.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tests/unit/validation/test_u_validation_utils.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tools/run_pipeline.py +0 -0
- {etlplus-0.4.5 → etlplus-0.4.7}/tools/update_demo_snippets.py +0 -0
|
@@ -159,7 +159,11 @@ repos:
|
|
|
159
159
|
rev: v1.19.0
|
|
160
160
|
hooks:
|
|
161
161
|
- id: mypy
|
|
162
|
-
args:
|
|
162
|
+
args:
|
|
163
|
+
- --cache-dir=.mypy_cache/pre-commit
|
|
164
|
+
- --ignore-missing-imports
|
|
165
|
+
- --install-types
|
|
166
|
+
- --non-interactive
|
|
163
167
|
|
|
164
168
|
- repo: https://github.com/pycqa/flake8
|
|
165
169
|
rev: 7.3.0
|
|
@@ -10,10 +10,12 @@ This module exposes :func:`main` for the console script as well as
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
|
+
import contextlib
|
|
13
14
|
import sys
|
|
14
15
|
from collections.abc import Sequence
|
|
15
16
|
from typing import Literal
|
|
16
17
|
|
|
18
|
+
import click
|
|
17
19
|
import typer
|
|
18
20
|
|
|
19
21
|
from .. import __version__
|
|
@@ -68,6 +70,32 @@ class _FormatAction(argparse.Action):
|
|
|
68
70
|
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
def _add_boolean_flag(
|
|
74
|
+
parser: argparse.ArgumentParser,
|
|
75
|
+
*,
|
|
76
|
+
name: str,
|
|
77
|
+
help_text: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Add a toggle that also supports the ``--no-`` prefix via 3.13.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
parser : argparse.ArgumentParser
|
|
84
|
+
Parser receiving the flag.
|
|
85
|
+
name : str
|
|
86
|
+
Primary flag name without leading dashes.
|
|
87
|
+
help_text : str
|
|
88
|
+
Help text rendered in ``--help`` output.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
f'--{name}',
|
|
93
|
+
action=argparse.BooleanOptionalAction,
|
|
94
|
+
default=False,
|
|
95
|
+
help=help_text,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
71
99
|
def _add_config_option(
|
|
72
100
|
parser: argparse.ArgumentParser,
|
|
73
101
|
*,
|
|
@@ -129,32 +157,6 @@ def _add_format_options(
|
|
|
129
157
|
)
|
|
130
158
|
|
|
131
159
|
|
|
132
|
-
def _add_boolean_flag(
|
|
133
|
-
parser: argparse.ArgumentParser,
|
|
134
|
-
*,
|
|
135
|
-
name: str,
|
|
136
|
-
help_text: str,
|
|
137
|
-
) -> None:
|
|
138
|
-
"""Add a toggle that also supports the ``--no-`` prefix via 3.13.
|
|
139
|
-
|
|
140
|
-
Parameters
|
|
141
|
-
----------
|
|
142
|
-
parser : argparse.ArgumentParser
|
|
143
|
-
Parser receiving the flag.
|
|
144
|
-
name : str
|
|
145
|
-
Primary flag name without leading dashes.
|
|
146
|
-
help_text : str
|
|
147
|
-
Help text rendered in ``--help`` output.
|
|
148
|
-
"""
|
|
149
|
-
|
|
150
|
-
parser.add_argument(
|
|
151
|
-
f'--{name}',
|
|
152
|
-
action=argparse.BooleanOptionalAction,
|
|
153
|
-
default=False,
|
|
154
|
-
help=help_text,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
160
|
def _cli_description() -> str:
|
|
159
161
|
return '\n'.join(
|
|
160
162
|
[
|
|
@@ -188,6 +190,93 @@ def _cli_epilog() -> str:
|
|
|
188
190
|
)
|
|
189
191
|
|
|
190
192
|
|
|
193
|
+
def _emit_context_help(
|
|
194
|
+
ctx: click.Context | None,
|
|
195
|
+
) -> bool:
|
|
196
|
+
"""
|
|
197
|
+
Mirror Click help output for the provided context onto stderr.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
ctx : click.Context | None
|
|
202
|
+
The Click context to emit help for.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
bool
|
|
207
|
+
``True`` when help was emitted, ``False`` when ``ctx`` was ``None``.
|
|
208
|
+
"""
|
|
209
|
+
if ctx is None:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
213
|
+
ctx.get_help()
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _emit_root_help(
|
|
218
|
+
command: click.Command,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Print the root ``etlplus`` help text to stderr.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
command : click.Command
|
|
226
|
+
The root Typer/Click command.
|
|
227
|
+
"""
|
|
228
|
+
ctx = command.make_context('etlplus', [], resilient_parsing=True)
|
|
229
|
+
try:
|
|
230
|
+
_emit_context_help(ctx)
|
|
231
|
+
finally:
|
|
232
|
+
ctx.close()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _is_illegal_option_error(
|
|
236
|
+
exc: click.exceptions.UsageError,
|
|
237
|
+
) -> bool:
|
|
238
|
+
"""
|
|
239
|
+
Return ``True`` when usage errors stem from invalid options.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
exc : click.exceptions.UsageError
|
|
244
|
+
The usage error to inspect.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
bool
|
|
249
|
+
``True`` when the error indicates illegal options.
|
|
250
|
+
"""
|
|
251
|
+
return isinstance(
|
|
252
|
+
exc,
|
|
253
|
+
(
|
|
254
|
+
click.exceptions.BadOptionUsage,
|
|
255
|
+
click.exceptions.NoSuchOption,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _is_unknown_command_error(
|
|
261
|
+
exc: click.exceptions.UsageError,
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Return ``True`` when a :class:`UsageError` indicates bad subcommand.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
exc : click.exceptions.UsageError
|
|
269
|
+
The usage error to inspect.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
bool
|
|
274
|
+
``True`` when the error indicates an unknown command.
|
|
275
|
+
"""
|
|
276
|
+
message = getattr(exc, 'message', None) or str(exc)
|
|
277
|
+
return message.startswith('No such command ')
|
|
278
|
+
|
|
279
|
+
|
|
191
280
|
# SECTION: FUNCTIONS ======================================================== #
|
|
192
281
|
|
|
193
282
|
|
|
@@ -422,6 +511,9 @@ def main(
|
|
|
422
511
|
|
|
423
512
|
Raises
|
|
424
513
|
------
|
|
514
|
+
click.exceptions.UsageError
|
|
515
|
+
Re-raises Typer/Click usage errors after printing help for unknown
|
|
516
|
+
commands.
|
|
425
517
|
SystemExit
|
|
426
518
|
Re-raises SystemExit exceptions to preserve exit codes.
|
|
427
519
|
|
|
@@ -442,6 +534,19 @@ def main(
|
|
|
442
534
|
)
|
|
443
535
|
return int(result or 0)
|
|
444
536
|
|
|
537
|
+
except click.exceptions.UsageError as exc:
|
|
538
|
+
if _is_unknown_command_error(exc):
|
|
539
|
+
typer.echo(f'Error: {exc}', err=True)
|
|
540
|
+
_emit_root_help(command)
|
|
541
|
+
return int(getattr(exc, 'exit_code', 2))
|
|
542
|
+
if _is_illegal_option_error(exc):
|
|
543
|
+
typer.echo(f'Error: {exc}', err=True)
|
|
544
|
+
if not _emit_context_help(exc.ctx):
|
|
545
|
+
_emit_root_help(command)
|
|
546
|
+
return int(getattr(exc, 'exit_code', 2))
|
|
547
|
+
|
|
548
|
+
raise
|
|
549
|
+
|
|
445
550
|
except typer.Exit as exc:
|
|
446
551
|
return int(exc.exit_code)
|
|
447
552
|
|
|
@@ -7,6 +7,7 @@ Unit tests for :mod:`etlplus.cli.main`.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from collections.abc import Callable
|
|
10
|
+
from importlib import import_module
|
|
10
11
|
from unittest.mock import Mock
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
@@ -20,6 +21,8 @@ from etlplus.cli.main import main as cli_main
|
|
|
20
21
|
|
|
21
22
|
pytestmark = pytest.mark.unit
|
|
22
23
|
|
|
24
|
+
cli_main_module = import_module('etlplus.cli.main')
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
def _install_stub_command(
|
|
25
28
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -52,6 +55,38 @@ def _install_stub_command(
|
|
|
52
55
|
# SECTION: TESTS ============================================================ #
|
|
53
56
|
|
|
54
57
|
|
|
58
|
+
class TestCreateParser:
|
|
59
|
+
"""Unit tests for :func:`etlplus.cli.main.create_parser`."""
|
|
60
|
+
|
|
61
|
+
def test_extract_parser_sets_handler_and_format_flag(self) -> None:
|
|
62
|
+
"""Extract parser should bind handlers and flag explicit formats."""
|
|
63
|
+
# pylint: disable=protected-access
|
|
64
|
+
|
|
65
|
+
parser = cli_main_module.create_parser()
|
|
66
|
+
namespace = parser.parse_args(
|
|
67
|
+
['extract', 'file', 'data.csv', '--source-format', 'json'],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert namespace.func is cli_main_module.cmd_extract
|
|
71
|
+
assert namespace.source_type == 'file'
|
|
72
|
+
assert namespace.source == 'data.csv'
|
|
73
|
+
assert namespace.source_format == 'json'
|
|
74
|
+
assert namespace._format_explicit is True
|
|
75
|
+
|
|
76
|
+
def test_list_parser_supports_boolean_flags(self) -> None:
|
|
77
|
+
"""List parser should surface boolean flag wiring."""
|
|
78
|
+
parser = cli_main_module.create_parser()
|
|
79
|
+
namespace = parser.parse_args(
|
|
80
|
+
['list', '--config', 'pipelines.yml', '--targets', '--transforms'],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert namespace.func is cli_main_module.cmd_list
|
|
84
|
+
assert namespace.command == 'list'
|
|
85
|
+
assert namespace.config == 'pipelines.yml'
|
|
86
|
+
assert namespace.targets is True
|
|
87
|
+
assert namespace.transforms is True
|
|
88
|
+
|
|
89
|
+
|
|
55
90
|
class TestMain:
|
|
56
91
|
"""Unit test suite for :func:`etlplus.cli.main`."""
|
|
57
92
|
|
|
@@ -60,7 +95,6 @@ class TestMain:
|
|
|
60
95
|
monkeypatch: pytest.MonkeyPatch,
|
|
61
96
|
) -> None:
|
|
62
97
|
"""Test that the command return value is normalized into an ``int``."""
|
|
63
|
-
|
|
64
98
|
captured: dict[str, object] = {}
|
|
65
99
|
|
|
66
100
|
def _action(**kwargs: object) -> object:
|
|
@@ -126,7 +160,8 @@ class TestMain:
|
|
|
126
160
|
monkeypatch: pytest.MonkeyPatch,
|
|
127
161
|
) -> None:
|
|
128
162
|
"""
|
|
129
|
-
``typer.Abort``
|
|
163
|
+
Test that ``typer.Abort`` propagates as a generic failure (exit code
|
|
164
|
+
1).
|
|
130
165
|
"""
|
|
131
166
|
|
|
132
167
|
def _action(**kwargs: object) -> object: # noqa: ARG001
|
|
@@ -140,7 +175,7 @@ class TestMain:
|
|
|
140
175
|
self,
|
|
141
176
|
monkeypatch: pytest.MonkeyPatch,
|
|
142
177
|
) -> None:
|
|
143
|
-
"""``typer.Exit``
|
|
178
|
+
"""Test that ``typer.Exit`` propagates its exit code."""
|
|
144
179
|
|
|
145
180
|
def _action(**kwargs: object) -> object: # noqa: ARG001
|
|
146
181
|
raise typer.Exit(17)
|
|
@@ -153,14 +188,52 @@ class TestMain:
|
|
|
153
188
|
"""Test that no args prints help and exits with exit code 0."""
|
|
154
189
|
assert cli_main([]) == 0
|
|
155
190
|
|
|
191
|
+
def test_unknown_subcommand_emits_usage(
|
|
192
|
+
self,
|
|
193
|
+
capsys: pytest.CaptureFixture[str],
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Test that illegal subcommands show help and exit with code 2."""
|
|
196
|
+
exit_code = cli_main(['definitely-not-real'])
|
|
197
|
+
captured = capsys.readouterr()
|
|
198
|
+
|
|
199
|
+
assert exit_code == 2
|
|
200
|
+
assert 'No such command' in captured.err
|
|
201
|
+
assert 'Usage:' in captured.err
|
|
202
|
+
|
|
203
|
+
def test_unknown_root_option_emits_usage(
|
|
204
|
+
self,
|
|
205
|
+
capsys: pytest.CaptureFixture[str],
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Test that Unknown root options echo usage details to stderr."""
|
|
208
|
+
|
|
209
|
+
exit_code = cli_main(['--definitely-not-real-option'])
|
|
210
|
+
captured = capsys.readouterr()
|
|
211
|
+
|
|
212
|
+
assert exit_code == 2
|
|
213
|
+
assert 'No such option' in captured.err
|
|
214
|
+
assert 'Usage:' in captured.err
|
|
215
|
+
|
|
216
|
+
def test_unknown_subcommand_option_emits_usage(
|
|
217
|
+
self,
|
|
218
|
+
capsys: pytest.CaptureFixture[str],
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Test that unknown subcommand options surface usage help."""
|
|
221
|
+
|
|
222
|
+
exit_code = cli_main(['extract', '--definitely-not-real-option'])
|
|
223
|
+
captured = capsys.readouterr()
|
|
224
|
+
|
|
225
|
+
assert exit_code == 2
|
|
226
|
+
assert 'No such option' in captured.err
|
|
227
|
+
assert 'Usage:' in captured.err
|
|
228
|
+
|
|
156
229
|
def test_value_error_returns_exit_code_1(
|
|
157
230
|
self,
|
|
158
231
|
monkeypatch: pytest.MonkeyPatch,
|
|
159
232
|
capsys: pytest.CaptureFixture[str],
|
|
160
233
|
) -> None:
|
|
161
234
|
"""
|
|
162
|
-
Test that :class:`ValueError` from a command maps to exit code 1.
|
|
163
|
-
|
|
235
|
+
Test that :class:`ValueError` from a command maps to exit code 1.
|
|
236
|
+
"""
|
|
164
237
|
monkeypatch.setattr(
|
|
165
238
|
cli_app_module,
|
|
166
239
|
'cmd_extract',
|
|
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
|
|
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
|