etlplus 0.4.6__tar.gz → 0.4.8__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.6 → etlplus-0.4.8}/.pre-commit-config.yaml +5 -1
- {etlplus-0.4.6/etlplus.egg-info → etlplus-0.4.8}/PKG-INFO +14 -1
- {etlplus-0.4.6 → etlplus-0.4.8}/README.md +13 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/docs/pipeline-guide.md +8 -7
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/cli/app.py +19 -3
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/cli/handlers.py +12 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/cli/main.py +146 -28
- {etlplus-0.4.6 → etlplus-0.4.8/etlplus.egg-info}/PKG-INFO +14 -1
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/README.md +4 -4
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_pipeline_smoke.py +4 -3
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/cli/test_u_cli_main.py +44 -6
- {etlplus-0.4.6 → etlplus-0.4.8}/.coveragerc +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.editorconfig +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.gitattributes +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.github/actions/python-bootstrap/action.yml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.github/workflows/ci.yml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.gitignore +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/.ruff.toml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/CODE_OF_CONDUCT.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/CONTRIBUTING.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/DEMO.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/LICENSE +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/Makefile +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/REFERENCES.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/docs/snippets/installation_version.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/__main__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/__version__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/README.md +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/auth.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/endpoint_client.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/errors.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/pagination/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/pagination/client.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/pagination/config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/pagination/paginator.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/rate_limiting/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/rate_limiting/config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/request_manager.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/retry_manager.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/transport.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/api/types.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/cli/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/connector.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/jobs.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/pipeline.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/profile.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/types.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/config/utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/enums.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/extract.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/file.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/load.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/mixins.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/py.typed +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/run.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/run_helpers.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/transform.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/types.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/validate.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/validation/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus/validation/utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus.egg-info/SOURCES.txt +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus.egg-info/dependency_links.txt +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus.egg-info/entry_points.txt +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus.egg-info/requires.txt +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/etlplus.egg-info/top_level.txt +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/configs/pipeline.yml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/data/sample.csv +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/data/sample.json +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/data/sample.xml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/data/sample.xsd +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/data/sample.yaml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/examples/quickstart_python.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/pyproject.toml +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/pytest.ini +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/setup.cfg +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/setup.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/__init__.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/conftest.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/conftest.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_cli.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_examples_data_parity.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_pagination_strategy.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_run.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/conftest.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_auth.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_endpoint_client.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_mocks.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_pagination_client.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_pagination_config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_paginator.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_rate_limit_config.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_rate_limiter.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_request_manager.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_retry_manager.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_transport.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/api/test_u_types.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/cli/conftest.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/cli/test_u_cli_app.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/cli/test_u_cli_handlers.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/config/test_u_config_utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/config/test_u_connector.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/config/test_u_jobs.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/config/test_u_pipeline.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/conftest.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_enums.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_extract.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_file.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_load.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_main.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_mixins.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_run.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_run_helpers.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_transform.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_validate.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/test_u_version.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tests/unit/validation/test_u_validation_utils.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/tools/run_pipeline.py +0 -0
- {etlplus-0.4.6 → etlplus-0.4.8}/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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etlplus
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
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
|
|
@@ -306,6 +306,19 @@ For YAML-driven pipelines executed end-to-end (extract → validate → transfor
|
|
|
306
306
|
- Authoring: [`docs/pipeline-guide.md`](docs/pipeline-guide.md)
|
|
307
307
|
- Runner API and internals: [`docs/run-module.md`](docs/run-module.md)
|
|
308
308
|
|
|
309
|
+
CLI quick reference for pipelines:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# List jobs or show a pipeline summary
|
|
313
|
+
etlplus list --config examples/configs/pipeline.yml --jobs
|
|
314
|
+
etlplus list --config examples/configs/pipeline.yml --summary
|
|
315
|
+
|
|
316
|
+
# Run a job
|
|
317
|
+
etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
|
|
318
|
+
|
|
319
|
+
# Deprecated shim (will be removed): etlplus pipeline
|
|
320
|
+
```
|
|
321
|
+
|
|
309
322
|
### Complete ETL Pipeline Example
|
|
310
323
|
|
|
311
324
|
```bash
|
|
@@ -264,6 +264,19 @@ For YAML-driven pipelines executed end-to-end (extract → validate → transfor
|
|
|
264
264
|
- Authoring: [`docs/pipeline-guide.md`](docs/pipeline-guide.md)
|
|
265
265
|
- Runner API and internals: [`docs/run-module.md`](docs/run-module.md)
|
|
266
266
|
|
|
267
|
+
CLI quick reference for pipelines:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# List jobs or show a pipeline summary
|
|
271
|
+
etlplus list --config examples/configs/pipeline.yml --jobs
|
|
272
|
+
etlplus list --config examples/configs/pipeline.yml --summary
|
|
273
|
+
|
|
274
|
+
# Run a job
|
|
275
|
+
etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
|
|
276
|
+
|
|
277
|
+
# Deprecated shim (will be removed): etlplus pipeline
|
|
278
|
+
```
|
|
279
|
+
|
|
267
280
|
### Complete ETL Pipeline Example
|
|
268
281
|
|
|
269
282
|
```bash
|
|
@@ -378,31 +378,32 @@ jobs:
|
|
|
378
378
|
Once you have a pipeline YAML, you can run jobs either from the
|
|
379
379
|
command line or directly from Python.
|
|
380
380
|
|
|
381
|
-
### CLI: `etlplus
|
|
381
|
+
### CLI: `etlplus list` (inspect) and `etlplus run` (execute)
|
|
382
382
|
|
|
383
|
-
List jobs
|
|
383
|
+
List jobs or show a summary from a pipeline file:
|
|
384
384
|
|
|
385
385
|
```bash
|
|
386
|
-
etlplus
|
|
386
|
+
etlplus list --config examples/configs/pipeline.yml --jobs
|
|
387
|
+
etlplus list --config examples/configs/pipeline.yml --summary
|
|
387
388
|
```
|
|
388
389
|
|
|
389
390
|
Run a specific job end-to-end (extract → validate → transform → load):
|
|
390
391
|
|
|
391
392
|
```bash
|
|
392
|
-
etlplus pipeline --config examples/configs/pipeline.yml --run file_to_file_customers
|
|
393
|
-
|
|
394
|
-
# Equivalent, using the dedicated run command
|
|
395
393
|
etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
|
|
396
394
|
```
|
|
397
395
|
|
|
398
396
|
Notes:
|
|
399
397
|
|
|
400
|
-
-
|
|
398
|
+
- These commands read the same YAML schema described in this guide.
|
|
401
399
|
- Environment-variable substitution (e.g. `${GITHUB_TOKEN}`) is applied the same way as when loading
|
|
402
400
|
configs via the Python API.
|
|
403
401
|
- For more details on the orchestration implementation, see
|
|
404
402
|
[Runner internals: etlplus.run](run-module.md).
|
|
405
403
|
|
|
404
|
+
Deprecated: `etlplus pipeline` is still available as a shim but will be removed in a future release;
|
|
405
|
+
prefer `list` and `run`.
|
|
406
|
+
|
|
406
407
|
### Python: `etlplus.run.run`
|
|
407
408
|
|
|
408
409
|
To trigger a job programmatically, use the high-level runner function exposed by the package:
|
|
@@ -782,8 +782,21 @@ def list_cmd(
|
|
|
782
782
|
'--pipelines',
|
|
783
783
|
help='List ETL pipelines',
|
|
784
784
|
),
|
|
785
|
-
sources: bool = typer.Option(
|
|
786
|
-
|
|
785
|
+
sources: bool = typer.Option(
|
|
786
|
+
False,
|
|
787
|
+
'--sources',
|
|
788
|
+
help='List data sources',
|
|
789
|
+
),
|
|
790
|
+
summary: bool = typer.Option(
|
|
791
|
+
False,
|
|
792
|
+
'--summary',
|
|
793
|
+
help='Show pipeline summary (name, version, sources, targets, jobs)',
|
|
794
|
+
),
|
|
795
|
+
targets: bool = typer.Option(
|
|
796
|
+
False,
|
|
797
|
+
'--targets',
|
|
798
|
+
help='List data targets',
|
|
799
|
+
),
|
|
787
800
|
transforms: bool = typer.Option(
|
|
788
801
|
False,
|
|
789
802
|
'--transforms',
|
|
@@ -805,6 +818,8 @@ def list_cmd(
|
|
|
805
818
|
If True, list ETL pipelines.
|
|
806
819
|
sources : bool, optional
|
|
807
820
|
If True, list data sources.
|
|
821
|
+
summary : bool, optional
|
|
822
|
+
If True, show pipeline summary (name, version, sources, targets, jobs).
|
|
808
823
|
targets : bool, optional
|
|
809
824
|
If True, list data targets.
|
|
810
825
|
transforms : bool, optional
|
|
@@ -820,6 +835,7 @@ def list_cmd(
|
|
|
820
835
|
state,
|
|
821
836
|
command='list',
|
|
822
837
|
config=config,
|
|
838
|
+
summary=summary,
|
|
823
839
|
pipelines=pipelines,
|
|
824
840
|
jobs=jobs,
|
|
825
841
|
sources=sources,
|
|
@@ -953,7 +969,7 @@ def pipeline_cmd(
|
|
|
953
969
|
),
|
|
954
970
|
) -> int:
|
|
955
971
|
"""
|
|
956
|
-
|
|
972
|
+
Deprecated wrapper to inspect or run a pipeline YAML configuration.
|
|
957
973
|
|
|
958
974
|
Parameters
|
|
959
975
|
----------
|
|
@@ -117,6 +117,8 @@ def _list_sections(
|
|
|
117
117
|
Metadata output for the list command.
|
|
118
118
|
"""
|
|
119
119
|
sections: dict[str, Any] = {}
|
|
120
|
+
if getattr(args, 'jobs', False):
|
|
121
|
+
sections['jobs'] = _pipeline_summary(cfg)['jobs']
|
|
120
122
|
if getattr(args, 'pipelines', False):
|
|
121
123
|
sections['pipelines'] = [cfg.name]
|
|
122
124
|
if getattr(args, 'sources', False):
|
|
@@ -598,6 +600,12 @@ def cmd_pipeline(
|
|
|
598
600
|
int
|
|
599
601
|
Zero on success.
|
|
600
602
|
"""
|
|
603
|
+
print(
|
|
604
|
+
'DEPRECATED: use "etlplus list --summary|--jobs" or '
|
|
605
|
+
'"etlplus run --job/--pipeline" instead of "etlplus pipeline".',
|
|
606
|
+
file=sys.stderr,
|
|
607
|
+
)
|
|
608
|
+
|
|
601
609
|
cfg = load_pipeline_config(args.config, substitute=True)
|
|
602
610
|
|
|
603
611
|
list_flag = getattr(args, 'list', False) or getattr(args, 'jobs', False)
|
|
@@ -635,6 +643,10 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
635
643
|
Zero on success.
|
|
636
644
|
"""
|
|
637
645
|
cfg = load_pipeline_config(args.config, substitute=True)
|
|
646
|
+
if getattr(args, 'summary', False):
|
|
647
|
+
print_json(_pipeline_summary(cfg))
|
|
648
|
+
return 0
|
|
649
|
+
|
|
638
650
|
print_json(_list_sections(cfg, args))
|
|
639
651
|
return 0
|
|
640
652
|
|
|
@@ -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
|
|
|
@@ -333,8 +422,9 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
333
422
|
pipe_parser = subparsers.add_parser(
|
|
334
423
|
'pipeline',
|
|
335
424
|
help=(
|
|
336
|
-
'
|
|
337
|
-
|
|
425
|
+
'DEPRECATED: use "list" (for summary/jobs) or "run" (to execute); '
|
|
426
|
+
'see '
|
|
427
|
+
f'{PROJECT_URL}/blob/main/docs/pipeline-guide.md'
|
|
338
428
|
),
|
|
339
429
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
340
430
|
)
|
|
@@ -357,6 +447,11 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
357
447
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
358
448
|
)
|
|
359
449
|
_add_config_option(list_parser)
|
|
450
|
+
_add_boolean_flag(
|
|
451
|
+
list_parser,
|
|
452
|
+
name='jobs',
|
|
453
|
+
help_text='List ETL jobs',
|
|
454
|
+
)
|
|
360
455
|
_add_boolean_flag(
|
|
361
456
|
list_parser,
|
|
362
457
|
name='pipelines',
|
|
@@ -367,6 +462,13 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
367
462
|
name='sources',
|
|
368
463
|
help_text='List data sources',
|
|
369
464
|
)
|
|
465
|
+
_add_boolean_flag(
|
|
466
|
+
list_parser,
|
|
467
|
+
name='summary',
|
|
468
|
+
help_text=(
|
|
469
|
+
'Show pipeline summary (name, version, sources, targets, jobs)'
|
|
470
|
+
),
|
|
471
|
+
)
|
|
370
472
|
_add_boolean_flag(
|
|
371
473
|
list_parser,
|
|
372
474
|
name='targets',
|
|
@@ -422,6 +524,9 @@ def main(
|
|
|
422
524
|
|
|
423
525
|
Raises
|
|
424
526
|
------
|
|
527
|
+
click.exceptions.UsageError
|
|
528
|
+
Re-raises Typer/Click usage errors after printing help for unknown
|
|
529
|
+
commands.
|
|
425
530
|
SystemExit
|
|
426
531
|
Re-raises SystemExit exceptions to preserve exit codes.
|
|
427
532
|
|
|
@@ -442,6 +547,19 @@ def main(
|
|
|
442
547
|
)
|
|
443
548
|
return int(result or 0)
|
|
444
549
|
|
|
550
|
+
except click.exceptions.UsageError as exc:
|
|
551
|
+
if _is_unknown_command_error(exc):
|
|
552
|
+
typer.echo(f'Error: {exc}', err=True)
|
|
553
|
+
_emit_root_help(command)
|
|
554
|
+
return int(getattr(exc, 'exit_code', 2))
|
|
555
|
+
if _is_illegal_option_error(exc):
|
|
556
|
+
typer.echo(f'Error: {exc}', err=True)
|
|
557
|
+
if not _emit_context_help(exc.ctx):
|
|
558
|
+
_emit_root_help(command)
|
|
559
|
+
return int(getattr(exc, 'exit_code', 2))
|
|
560
|
+
|
|
561
|
+
raise
|
|
562
|
+
|
|
445
563
|
except typer.Exit as exc:
|
|
446
564
|
return int(exc.exit_code)
|
|
447
565
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etlplus
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
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
|
|
@@ -306,6 +306,19 @@ For YAML-driven pipelines executed end-to-end (extract → validate → transfor
|
|
|
306
306
|
- Authoring: [`docs/pipeline-guide.md`](docs/pipeline-guide.md)
|
|
307
307
|
- Runner API and internals: [`docs/run-module.md`](docs/run-module.md)
|
|
308
308
|
|
|
309
|
+
CLI quick reference for pipelines:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# List jobs or show a pipeline summary
|
|
313
|
+
etlplus list --config examples/configs/pipeline.yml --jobs
|
|
314
|
+
etlplus list --config examples/configs/pipeline.yml --summary
|
|
315
|
+
|
|
316
|
+
# Run a job
|
|
317
|
+
etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
|
|
318
|
+
|
|
319
|
+
# Deprecated shim (will be removed): etlplus pipeline
|
|
320
|
+
```
|
|
321
|
+
|
|
309
322
|
### Complete ETL Pipeline Example
|
|
310
323
|
|
|
311
324
|
```bash
|
|
@@ -44,12 +44,12 @@ CLI examples:
|
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
# List jobs defined in a pipeline file
|
|
47
|
-
etlplus
|
|
47
|
+
etlplus list --config examples/configs/pipeline.yml --jobs
|
|
48
48
|
|
|
49
|
-
#
|
|
50
|
-
etlplus
|
|
49
|
+
# Show a pipeline summary (name, version, sources, targets, jobs)
|
|
50
|
+
etlplus list --config examples/configs/pipeline.yml --summary
|
|
51
51
|
|
|
52
|
-
#
|
|
52
|
+
# Run a specific job end-to-end
|
|
53
53
|
etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
:mod:`tests.integration.test_i_pipeline_smoke` module.
|
|
3
3
|
|
|
4
4
|
Pipeline smoke integration test suite exercising a minimal file→file job via
|
|
5
|
-
the CLI. Parametrized to verify both empty
|
|
5
|
+
the CLI (using the deprecated-free path). Parametrized to verify both empty
|
|
6
|
+
and non-empty inputs.
|
|
6
7
|
|
|
7
8
|
Notes
|
|
8
9
|
-----
|
|
9
10
|
- Builds a transient pipeline YAML string per test run.
|
|
10
|
-
- Invokes ``etlplus
|
|
11
|
+
- Invokes ``etlplus run --job <job>`` end-to-end.
|
|
11
12
|
- Validates output file contents against input data shape.
|
|
12
13
|
"""
|
|
13
14
|
|
|
@@ -85,7 +86,7 @@ class TestPipelineSmoke:
|
|
|
85
86
|
|
|
86
87
|
code, out, err = cli_invoke(
|
|
87
88
|
(
|
|
88
|
-
'
|
|
89
|
+
'run',
|
|
89
90
|
'--config',
|
|
90
91
|
str(cfg_path),
|
|
91
92
|
'--job',
|
|
@@ -60,6 +60,7 @@ class TestCreateParser:
|
|
|
60
60
|
|
|
61
61
|
def test_extract_parser_sets_handler_and_format_flag(self) -> None:
|
|
62
62
|
"""Extract parser should bind handlers and flag explicit formats."""
|
|
63
|
+
# pylint: disable=protected-access
|
|
63
64
|
|
|
64
65
|
parser = cli_main_module.create_parser()
|
|
65
66
|
namespace = parser.parse_args(
|
|
@@ -74,7 +75,6 @@ class TestCreateParser:
|
|
|
74
75
|
|
|
75
76
|
def test_list_parser_supports_boolean_flags(self) -> None:
|
|
76
77
|
"""List parser should surface boolean flag wiring."""
|
|
77
|
-
|
|
78
78
|
parser = cli_main_module.create_parser()
|
|
79
79
|
namespace = parser.parse_args(
|
|
80
80
|
['list', '--config', 'pipelines.yml', '--targets', '--transforms'],
|
|
@@ -95,7 +95,6 @@ class TestMain:
|
|
|
95
95
|
monkeypatch: pytest.MonkeyPatch,
|
|
96
96
|
) -> None:
|
|
97
97
|
"""Test that the command return value is normalized into an ``int``."""
|
|
98
|
-
|
|
99
98
|
captured: dict[str, object] = {}
|
|
100
99
|
|
|
101
100
|
def _action(**kwargs: object) -> object:
|
|
@@ -161,7 +160,8 @@ class TestMain:
|
|
|
161
160
|
monkeypatch: pytest.MonkeyPatch,
|
|
162
161
|
) -> None:
|
|
163
162
|
"""
|
|
164
|
-
``typer.Abort``
|
|
163
|
+
Test that ``typer.Abort`` propagates as a generic failure (exit code
|
|
164
|
+
1).
|
|
165
165
|
"""
|
|
166
166
|
|
|
167
167
|
def _action(**kwargs: object) -> object: # noqa: ARG001
|
|
@@ -175,7 +175,7 @@ class TestMain:
|
|
|
175
175
|
self,
|
|
176
176
|
monkeypatch: pytest.MonkeyPatch,
|
|
177
177
|
) -> None:
|
|
178
|
-
"""``typer.Exit``
|
|
178
|
+
"""Test that ``typer.Exit`` propagates its exit code."""
|
|
179
179
|
|
|
180
180
|
def _action(**kwargs: object) -> object: # noqa: ARG001
|
|
181
181
|
raise typer.Exit(17)
|
|
@@ -188,14 +188,52 @@ class TestMain:
|
|
|
188
188
|
"""Test that no args prints help and exits with exit code 0."""
|
|
189
189
|
assert cli_main([]) == 0
|
|
190
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
|
+
|
|
191
229
|
def test_value_error_returns_exit_code_1(
|
|
192
230
|
self,
|
|
193
231
|
monkeypatch: pytest.MonkeyPatch,
|
|
194
232
|
capsys: pytest.CaptureFixture[str],
|
|
195
233
|
) -> None:
|
|
196
234
|
"""
|
|
197
|
-
Test that :class:`ValueError` from a command maps to exit code 1.
|
|
198
|
-
|
|
235
|
+
Test that :class:`ValueError` from a command maps to exit code 1.
|
|
236
|
+
"""
|
|
199
237
|
monkeypatch.setattr(
|
|
200
238
|
cli_app_module,
|
|
201
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
|