etlplus 0.7.2__tar.gz → 0.8.2__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 (151) hide show
  1. {etlplus-0.7.2/etlplus.egg-info → etlplus-0.8.2}/PKG-INFO +1 -3
  2. {etlplus-0.7.2 → etlplus-0.8.2}/README.md +0 -2
  3. {etlplus-0.7.2 → etlplus-0.8.2}/docs/pipeline-guide.md +0 -3
  4. etlplus-0.8.2/etlplus/cli/commands.py +645 -0
  5. etlplus-0.8.2/etlplus/cli/constants.py +65 -0
  6. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/cli/handlers.py +37 -358
  7. etlplus-0.8.2/etlplus/cli/io.py +343 -0
  8. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/cli/main.py +46 -131
  9. etlplus-0.8.2/etlplus/cli/options.py +115 -0
  10. etlplus-0.8.2/etlplus/cli/state.py +411 -0
  11. etlplus-0.8.2/etlplus/cli/types.py +33 -0
  12. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/ddl.py +1 -1
  13. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/types.py +0 -5
  14. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/types.py +5 -0
  15. {etlplus-0.7.2 → etlplus-0.8.2/etlplus.egg-info}/PKG-INFO +1 -3
  16. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus.egg-info/SOURCES.txt +6 -1
  17. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_pagination_strategy.py +1 -1
  18. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_app.py +15 -67
  19. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_handlers.py +99 -138
  20. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_main.py +7 -7
  21. etlplus-0.7.2/etlplus/cli/app.py +0 -1367
  22. {etlplus-0.7.2 → etlplus-0.8.2}/.coveragerc +0 -0
  23. {etlplus-0.7.2 → etlplus-0.8.2}/.editorconfig +0 -0
  24. {etlplus-0.7.2 → etlplus-0.8.2}/.gitattributes +0 -0
  25. {etlplus-0.7.2 → etlplus-0.8.2}/.github/actions/python-bootstrap/action.yml +0 -0
  26. {etlplus-0.7.2 → etlplus-0.8.2}/.github/workflows/ci.yml +0 -0
  27. {etlplus-0.7.2 → etlplus-0.8.2}/.gitignore +0 -0
  28. {etlplus-0.7.2 → etlplus-0.8.2}/.pre-commit-config.yaml +0 -0
  29. {etlplus-0.7.2 → etlplus-0.8.2}/.ruff.toml +0 -0
  30. {etlplus-0.7.2 → etlplus-0.8.2}/CODE_OF_CONDUCT.md +0 -0
  31. {etlplus-0.7.2 → etlplus-0.8.2}/CONTRIBUTING.md +0 -0
  32. {etlplus-0.7.2 → etlplus-0.8.2}/DEMO.md +0 -0
  33. {etlplus-0.7.2 → etlplus-0.8.2}/LICENSE +0 -0
  34. {etlplus-0.7.2 → etlplus-0.8.2}/MANIFEST.in +0 -0
  35. {etlplus-0.7.2 → etlplus-0.8.2}/Makefile +0 -0
  36. {etlplus-0.7.2 → etlplus-0.8.2}/REFERENCES.md +0 -0
  37. {etlplus-0.7.2 → etlplus-0.8.2}/docs/snippets/installation_version.md +0 -0
  38. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/__init__.py +0 -0
  39. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/__main__.py +0 -0
  40. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/__version__.py +0 -0
  41. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/README.md +0 -0
  42. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/__init__.py +0 -0
  43. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/auth.py +0 -0
  44. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/config.py +0 -0
  45. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/endpoint_client.py +0 -0
  46. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/errors.py +0 -0
  47. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/pagination/__init__.py +0 -0
  48. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/pagination/client.py +0 -0
  49. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/pagination/config.py +0 -0
  50. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/pagination/paginator.py +0 -0
  51. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/rate_limiting/__init__.py +0 -0
  52. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/rate_limiting/config.py +0 -0
  53. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  54. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/request_manager.py +0 -0
  55. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/retry_manager.py +0 -0
  56. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/transport.py +0 -0
  57. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/api/types.py +0 -0
  58. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/cli/__init__.py +0 -0
  59. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/__init__.py +0 -0
  60. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/connector.py +0 -0
  61. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/jobs.py +0 -0
  62. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/pipeline.py +0 -0
  63. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/profile.py +0 -0
  64. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/types.py +0 -0
  65. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/config/utils.py +0 -0
  66. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/__init__.py +0 -0
  67. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/engine.py +0 -0
  68. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/orm.py +0 -0
  69. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/database/schema.py +0 -0
  70. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/enums.py +0 -0
  71. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/extract.py +0 -0
  72. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/file.py +0 -0
  73. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/load.py +0 -0
  74. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/mixins.py +0 -0
  75. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/py.typed +0 -0
  76. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/run.py +0 -0
  77. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/run_helpers.py +0 -0
  78. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/templates/__init__.py +0 -0
  79. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/templates/ddl.sql.j2 +0 -0
  80. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/templates/view.sql.j2 +0 -0
  81. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/transform.py +0 -0
  82. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/utils.py +0 -0
  83. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/validate.py +0 -0
  84. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/validation/__init__.py +0 -0
  85. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus/validation/utils.py +0 -0
  86. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus.egg-info/dependency_links.txt +0 -0
  87. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus.egg-info/entry_points.txt +0 -0
  88. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus.egg-info/requires.txt +0 -0
  89. {etlplus-0.7.2 → etlplus-0.8.2}/etlplus.egg-info/top_level.txt +0 -0
  90. {etlplus-0.7.2 → etlplus-0.8.2}/examples/README.md +0 -0
  91. {etlplus-0.7.2 → etlplus-0.8.2}/examples/configs/ddl_spec.yml +0 -0
  92. {etlplus-0.7.2 → etlplus-0.8.2}/examples/configs/pipeline.yml +0 -0
  93. {etlplus-0.7.2 → etlplus-0.8.2}/examples/data/sample.csv +0 -0
  94. {etlplus-0.7.2 → etlplus-0.8.2}/examples/data/sample.json +0 -0
  95. {etlplus-0.7.2 → etlplus-0.8.2}/examples/data/sample.xml +0 -0
  96. {etlplus-0.7.2 → etlplus-0.8.2}/examples/data/sample.xsd +0 -0
  97. {etlplus-0.7.2 → etlplus-0.8.2}/examples/data/sample.yaml +0 -0
  98. {etlplus-0.7.2 → etlplus-0.8.2}/examples/quickstart_python.py +0 -0
  99. {etlplus-0.7.2 → etlplus-0.8.2}/pyproject.toml +0 -0
  100. {etlplus-0.7.2 → etlplus-0.8.2}/pytest.ini +0 -0
  101. {etlplus-0.7.2 → etlplus-0.8.2}/setup.cfg +0 -0
  102. {etlplus-0.7.2 → etlplus-0.8.2}/setup.py +0 -0
  103. {etlplus-0.7.2 → etlplus-0.8.2}/tests/__init__.py +0 -0
  104. {etlplus-0.7.2 → etlplus-0.8.2}/tests/conftest.py +0 -0
  105. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/conftest.py +0 -0
  106. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_cli.py +0 -0
  107. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_examples_data_parity.py +0 -0
  108. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_pipeline_smoke.py +0 -0
  109. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  110. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_run.py +0 -0
  111. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  112. {etlplus-0.7.2 → etlplus-0.8.2}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  113. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/conftest.py +0 -0
  114. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_auth.py +0 -0
  115. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_config.py +0 -0
  116. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_endpoint_client.py +0 -0
  117. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_mocks.py +0 -0
  118. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_pagination_client.py +0 -0
  119. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_pagination_config.py +0 -0
  120. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_paginator.py +0 -0
  121. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  122. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_rate_limiter.py +0 -0
  123. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_request_manager.py +0 -0
  124. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_retry_manager.py +0 -0
  125. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_transport.py +0 -0
  126. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/api/test_u_types.py +0 -0
  127. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/cli/conftest.py +0 -0
  128. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/config/test_u_config_utils.py +0 -0
  129. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/config/test_u_connector.py +0 -0
  130. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/config/test_u_jobs.py +0 -0
  131. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/config/test_u_pipeline.py +0 -0
  132. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/conftest.py +0 -0
  133. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/database/test_u_database_ddl.py +0 -0
  134. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/database/test_u_database_engine.py +0 -0
  135. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/database/test_u_database_orm.py +0 -0
  136. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/database/test_u_database_schema.py +0 -0
  137. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_enums.py +0 -0
  138. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_extract.py +0 -0
  139. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_file.py +0 -0
  140. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_load.py +0 -0
  141. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_main.py +0 -0
  142. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_mixins.py +0 -0
  143. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_run.py +0 -0
  144. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_run_helpers.py +0 -0
  145. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_transform.py +0 -0
  146. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_utils.py +0 -0
  147. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_validate.py +0 -0
  148. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/test_u_version.py +0 -0
  149. {etlplus-0.7.2 → etlplus-0.8.2}/tests/unit/validation/test_u_validation_utils.py +0 -0
  150. {etlplus-0.7.2 → etlplus-0.8.2}/tools/run_pipeline.py +0 -0
  151. {etlplus-0.7.2 → etlplus-0.8.2}/tools/update_demo_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.7.2
3
+ Version: 0.8.2
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
@@ -366,8 +366,6 @@ etlplus check --config examples/configs/pipeline.yml --summary
366
366
 
367
367
  # Run a job
368
368
  etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
369
-
370
- # Deprecated shim (will be removed): etlplus pipeline
371
369
  ```
372
370
 
373
371
  ### Complete ETL Pipeline Example
@@ -321,8 +321,6 @@ etlplus check --config examples/configs/pipeline.yml --summary
321
321
 
322
322
  # Run a job
323
323
  etlplus run --config examples/configs/pipeline.yml --job file_to_file_customers
324
-
325
- # Deprecated shim (will be removed): etlplus pipeline
326
324
  ```
327
325
 
328
326
  ### Complete ETL Pipeline Example
@@ -401,9 +401,6 @@ Notes:
401
401
  - For more details on the orchestration implementation, see
402
402
  [Runner internals: etlplus.run](run-module.md).
403
403
 
404
- Deprecated: `etlplus pipeline` is still available as a shim but will be removed in a future release;
405
- prefer `check` and `run`.
406
-
407
404
  ### Python: `etlplus.run.run`
408
405
 
409
406
  To trigger a job programmatically, use the high-level runner function exposed by the package:
@@ -0,0 +1,645 @@
1
+ """
2
+ :mod:`etlplus.cli.commands` module.
3
+
4
+ Typer application and subcommands for the ``etlplus`` command-line interface
5
+ (CLI). Typer (Click) is used for CLI parsing, help text, and subcommand
6
+ dispatch. The Typer layer focuses on ergonomics (git-style subcommands,
7
+ optional inference of resource types, stdin/stdout piping, and quality-of-life
8
+ flags), while delegating business logic to the existing :func:`*_handler`
9
+ handlers.
10
+
11
+ Subcommands
12
+ -----------
13
+ - ``check``: inspect a pipeline configuration
14
+ - ``extract``: extract data from files, databases, or REST APIs
15
+ - ``load``: load data to files, databases, or REST APIs
16
+ - ``render``: render SQL DDL from table schema specs
17
+ - ``transform``: transform records
18
+ - ``validate``: validate data against rules
19
+
20
+ Notes
21
+ -----
22
+ - Use ``-`` to read from stdin or to write to stdout.
23
+ - Commands ``extract`` and ``transform`` support the command-line option
24
+ ``--source-type`` to override inferred resource types.
25
+ - Commands ``transform`` and ``load`` support the command-line option
26
+ ``--target-type`` to override inferred resource types.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Annotated
32
+
33
+ import typer
34
+
35
+ from .. import __version__
36
+ from ..utils import json_type
37
+ from . import handlers
38
+ from .constants import CLI_DESCRIPTION
39
+ from .constants import CLI_EPILOG
40
+ from .constants import DATA_CONNECTORS
41
+ from .constants import DEFAULT_FILE_FORMAT
42
+ from .constants import FILE_FORMATS
43
+ from .options import typer_format_option_kwargs
44
+ from .state import CliState
45
+ from .state import ensure_state
46
+ from .state import format_namespace_kwargs
47
+ from .state import infer_resource_type_or_exit
48
+ from .state import infer_resource_type_soft
49
+ from .state import log_inferred_resource
50
+ from .state import optional_choice
51
+ from .state import resolve_resource_type
52
+ from .state import stateful_namespace
53
+ from .state import validate_choice
54
+
55
+ # SECTION: EXPORTS ========================================================== #
56
+
57
+
58
+ __all__ = ['app']
59
+
60
+
61
+ # SECTION: TYPE ALIASES ==================================================== #
62
+
63
+
64
+ OperationsOption = Annotated[
65
+ str,
66
+ typer.Option(
67
+ '--operations',
68
+ help='Transformation operations as JSON string.',
69
+ ),
70
+ ]
71
+
72
+ PipelineConfigOption = Annotated[
73
+ str,
74
+ typer.Option(
75
+ ...,
76
+ '--config',
77
+ metavar='PATH',
78
+ help='Path to pipeline YAML configuration file.',
79
+ ),
80
+ ]
81
+
82
+ RenderConfigOption = Annotated[
83
+ str | None,
84
+ typer.Option(
85
+ '--config',
86
+ metavar='PATH',
87
+ help='Pipeline YAML that includes table_schemas for rendering.',
88
+ show_default=False,
89
+ ),
90
+ ]
91
+
92
+ RenderOutputOption = Annotated[
93
+ str | None,
94
+ typer.Option(
95
+ '--output',
96
+ '-o',
97
+ metavar='PATH',
98
+ help='Write rendered SQL to PATH (default: stdout).',
99
+ ),
100
+ ]
101
+
102
+ RenderSpecOption = Annotated[
103
+ str | None,
104
+ typer.Option(
105
+ '--spec',
106
+ metavar='PATH',
107
+ help='Standalone table spec file (.yml/.yaml/.json).',
108
+ show_default=False,
109
+ ),
110
+ ]
111
+
112
+ RenderTableOption = Annotated[
113
+ str | None,
114
+ typer.Option(
115
+ '--table',
116
+ metavar='NAME',
117
+ help='Filter to a single table name from table_schemas.',
118
+ ),
119
+ ]
120
+
121
+ RenderTemplateOption = Annotated[
122
+ str,
123
+ typer.Option(
124
+ '--template',
125
+ '-t',
126
+ metavar='KEY|PATH',
127
+ help='Template key (ddl/view) or path to a Jinja template file.',
128
+ show_default=True,
129
+ ),
130
+ ]
131
+
132
+ RenderTemplatePathOption = Annotated[
133
+ str | None,
134
+ typer.Option(
135
+ '--template-path',
136
+ metavar='PATH',
137
+ help=(
138
+ 'Explicit path to a Jinja template file (overrides template key).'
139
+ ),
140
+ ),
141
+ ]
142
+
143
+ RulesOption = Annotated[
144
+ str,
145
+ typer.Option(
146
+ '--rules',
147
+ help='Validation rules as JSON string.',
148
+ ),
149
+ ]
150
+
151
+ SourceFormatOption = Annotated[
152
+ str | None,
153
+ typer.Option(
154
+ '--source-format',
155
+ **typer_format_option_kwargs(context='source'),
156
+ ),
157
+ ]
158
+
159
+ SourceInputArg = Annotated[
160
+ str,
161
+ typer.Argument(
162
+ ...,
163
+ metavar='SOURCE',
164
+ help=(
165
+ 'Extract from SOURCE. Use --from/--source-type to override the '
166
+ 'inferred connector when needed.'
167
+ ),
168
+ ),
169
+ ]
170
+
171
+ SourceOverrideOption = Annotated[
172
+ str | None,
173
+ typer.Option(
174
+ '--source-type',
175
+ metavar='CONNECTOR',
176
+ show_default=False,
177
+ rich_help_panel='I/O overrides',
178
+ help='Override the inferred source type (file, database, api).',
179
+ ),
180
+ ]
181
+
182
+ StdinFormatOption = Annotated[
183
+ str | None,
184
+ typer.Option(
185
+ '--source-format',
186
+ **typer_format_option_kwargs(context='source'),
187
+ ),
188
+ ]
189
+
190
+ StreamingSourceArg = Annotated[
191
+ str,
192
+ typer.Argument(
193
+ ...,
194
+ metavar='SOURCE',
195
+ help=(
196
+ 'Data source to transform or validate (path, JSON payload, or '
197
+ '- for stdin).'
198
+ ),
199
+ ),
200
+ ]
201
+
202
+ TargetFormatOption = Annotated[
203
+ str | None,
204
+ typer.Option(
205
+ '--target-format',
206
+ **typer_format_option_kwargs(context='target'),
207
+ ),
208
+ ]
209
+
210
+ TargetInputArg = Annotated[
211
+ str,
212
+ typer.Argument(
213
+ ...,
214
+ metavar='TARGET',
215
+ help=(
216
+ 'Load JSON data from stdin into TARGET. Use --to/--target-type '
217
+ 'to override connector inference when needed. Source data must '
218
+ 'be piped into stdin.'
219
+ ),
220
+ ),
221
+ ]
222
+
223
+ TargetOverrideOption = Annotated[
224
+ str | None,
225
+ typer.Option(
226
+ '--target-type',
227
+ metavar='CONNECTOR',
228
+ show_default=False,
229
+ rich_help_panel='I/O overrides',
230
+ help='Override the inferred target type (file, database, api).',
231
+ ),
232
+ ]
233
+
234
+ TargetPathOption = Annotated[
235
+ str | None,
236
+ typer.Option(
237
+ '--target',
238
+ metavar='PATH',
239
+ help='Target file for transformed or validated output (- for stdout).',
240
+ ),
241
+ ]
242
+
243
+
244
+ # SECTION: TYPER APP ======================================================== #
245
+
246
+
247
+ app = typer.Typer(
248
+ name='etlplus',
249
+ help=CLI_DESCRIPTION,
250
+ epilog=CLI_EPILOG,
251
+ add_completion=True,
252
+ no_args_is_help=False,
253
+ rich_markup_mode='markdown',
254
+ )
255
+
256
+
257
+ @app.callback(invoke_without_command=True)
258
+ def _root(
259
+ ctx: typer.Context,
260
+ version: bool = typer.Option(
261
+ False,
262
+ '--version',
263
+ '-V',
264
+ is_eager=True,
265
+ help='Show the version and exit.',
266
+ ),
267
+ pretty: bool = typer.Option(
268
+ True,
269
+ '--pretty/--no-pretty',
270
+ help='Pretty-print JSON output (default: pretty).',
271
+ ),
272
+ quiet: bool = typer.Option(
273
+ False,
274
+ '--quiet',
275
+ '-q',
276
+ help='Suppress warnings and non-essential output.',
277
+ ),
278
+ verbose: bool = typer.Option(
279
+ False,
280
+ '--verbose',
281
+ '-v',
282
+ help='Emit extra diagnostics to stderr.',
283
+ ),
284
+ ) -> None:
285
+ """
286
+ Seed the Typer context with runtime flags and handle root-only options.
287
+ """
288
+ ctx.obj = CliState(pretty=pretty, quiet=quiet, verbose=verbose)
289
+
290
+ if version:
291
+ typer.echo(f'etlplus {__version__}')
292
+ raise typer.Exit(0)
293
+
294
+ if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
295
+ typer.echo(ctx.command.get_help(ctx))
296
+ raise typer.Exit(0)
297
+
298
+
299
+ @app.command('check')
300
+ def check_cmd(
301
+ ctx: typer.Context,
302
+ config: PipelineConfigOption,
303
+ jobs: bool = typer.Option(
304
+ False,
305
+ '--jobs',
306
+ help='List available job names and exit',
307
+ ),
308
+ pipelines: bool = typer.Option(
309
+ False,
310
+ '--pipelines',
311
+ help='List ETL pipelines',
312
+ ),
313
+ sources: bool = typer.Option(
314
+ False,
315
+ '--sources',
316
+ help='List data sources',
317
+ ),
318
+ summary: bool = typer.Option(
319
+ False,
320
+ '--summary',
321
+ help='Show pipeline summary (name, version, sources, targets, jobs)',
322
+ ),
323
+ targets: bool = typer.Option(
324
+ False,
325
+ '--targets',
326
+ help='List data targets',
327
+ ),
328
+ transforms: bool = typer.Option(
329
+ False,
330
+ '--transforms',
331
+ help='List data transforms',
332
+ ),
333
+ ) -> int:
334
+ """Inspect a pipeline configuration."""
335
+ state = ensure_state(ctx)
336
+ ns = stateful_namespace(
337
+ state,
338
+ command='check',
339
+ config=config,
340
+ jobs=jobs,
341
+ pipelines=pipelines,
342
+ sources=sources,
343
+ summary=summary,
344
+ targets=targets,
345
+ transforms=transforms,
346
+ )
347
+ return int(handlers.check_handler(ns))
348
+
349
+
350
+ @app.command('extract')
351
+ def extract_cmd(
352
+ ctx: typer.Context,
353
+ source: SourceInputArg,
354
+ source_format: SourceFormatOption | None = None,
355
+ source_type: SourceOverrideOption | None = None,
356
+ ) -> int:
357
+ """Extract data from files, databases, or REST APIs."""
358
+ state = ensure_state(ctx)
359
+
360
+ source_type = optional_choice(
361
+ source_type,
362
+ DATA_CONNECTORS,
363
+ label='source_type',
364
+ )
365
+ source_format = optional_choice(
366
+ source_format,
367
+ FILE_FORMATS,
368
+ label='source_format',
369
+ )
370
+
371
+ resolved_source = source
372
+ resolved_source_type = source_type or infer_resource_type_or_exit(
373
+ resolved_source,
374
+ )
375
+
376
+ log_inferred_resource(
377
+ state,
378
+ role='source',
379
+ value=resolved_source,
380
+ resource_type=resolved_source_type,
381
+ )
382
+
383
+ format_kwargs = format_namespace_kwargs(
384
+ format_value=source_format,
385
+ default=DEFAULT_FILE_FORMAT,
386
+ )
387
+ ns = stateful_namespace(
388
+ state,
389
+ command='extract',
390
+ source_type=resolved_source_type,
391
+ source=resolved_source,
392
+ **format_kwargs,
393
+ )
394
+ return int(handlers.extract_handler(ns))
395
+
396
+
397
+ @app.command('load')
398
+ def load_cmd(
399
+ ctx: typer.Context,
400
+ target: TargetInputArg,
401
+ source_format: StdinFormatOption | None = None,
402
+ target_format: TargetFormatOption | None = None,
403
+ target_type: TargetOverrideOption | None = None,
404
+ ) -> int:
405
+ """Load data into a file, database, or REST API."""
406
+ state = ensure_state(ctx)
407
+
408
+ source_format = optional_choice(
409
+ source_format,
410
+ FILE_FORMATS,
411
+ label='source_format',
412
+ )
413
+ target_type = optional_choice(
414
+ target_type,
415
+ DATA_CONNECTORS,
416
+ label='target_type',
417
+ )
418
+ target_format = optional_choice(
419
+ target_format,
420
+ FILE_FORMATS,
421
+ label='target_format',
422
+ )
423
+
424
+ resolved_target = target
425
+ resolved_target_type = target_type or infer_resource_type_or_exit(
426
+ resolved_target,
427
+ )
428
+
429
+ resolved_source_value = '-'
430
+ resolved_source_type = infer_resource_type_soft(resolved_source_value)
431
+
432
+ log_inferred_resource(
433
+ state,
434
+ role='source',
435
+ value=resolved_source_value,
436
+ resource_type=resolved_source_type,
437
+ )
438
+ log_inferred_resource(
439
+ state,
440
+ role='target',
441
+ value=resolved_target,
442
+ resource_type=resolved_target_type,
443
+ )
444
+
445
+ format_kwargs = format_namespace_kwargs(
446
+ format_value=target_format,
447
+ default=DEFAULT_FILE_FORMAT,
448
+ )
449
+ ns = stateful_namespace(
450
+ state,
451
+ command='load',
452
+ source=resolved_source_value,
453
+ source_format=source_format,
454
+ target_type=resolved_target_type,
455
+ target=resolved_target,
456
+ **format_kwargs,
457
+ )
458
+ return int(handlers.load_handler(ns))
459
+
460
+
461
+ @app.command('render')
462
+ def render_cmd(
463
+ ctx: typer.Context,
464
+ config: RenderConfigOption = None,
465
+ spec: RenderSpecOption = None,
466
+ table: RenderTableOption = None,
467
+ template: RenderTemplateOption = 'ddl',
468
+ template_path: RenderTemplatePathOption = None,
469
+ output: RenderOutputOption = None,
470
+ ) -> int:
471
+ """Render SQL DDL from table schemas defined in YAML/JSON configs."""
472
+ state = ensure_state(ctx)
473
+ ns = stateful_namespace(
474
+ state,
475
+ command='render',
476
+ config=config,
477
+ spec=spec,
478
+ table=table,
479
+ template=template,
480
+ template_path=template_path,
481
+ output=output,
482
+ )
483
+ return int(handlers.render_handler(ns))
484
+
485
+
486
+ @app.command('run')
487
+ def run_cmd(
488
+ ctx: typer.Context,
489
+ config: PipelineConfigOption,
490
+ job: str | None = typer.Option(
491
+ None,
492
+ '-j',
493
+ '--job',
494
+ help='Name of the job to run',
495
+ ),
496
+ pipeline: str | None = typer.Option(
497
+ None,
498
+ '-p',
499
+ '--pipeline',
500
+ help='Name of the pipeline to run',
501
+ ),
502
+ ) -> int:
503
+ """Execute an ETL job or pipeline from a YAML configuration."""
504
+ state = ensure_state(ctx)
505
+ ns = stateful_namespace(
506
+ state,
507
+ command='run',
508
+ config=config,
509
+ job=job,
510
+ pipeline=pipeline,
511
+ )
512
+ return int(handlers.run_handler(ns))
513
+
514
+
515
+ @app.command('transform')
516
+ def transform_cmd(
517
+ ctx: typer.Context,
518
+ operations: OperationsOption = '{}',
519
+ source: StreamingSourceArg = '-',
520
+ source_format: SourceFormatOption | None = None,
521
+ source_type: SourceOverrideOption | None = None,
522
+ target: TargetPathOption | None = None,
523
+ target_format: TargetFormatOption | None = None,
524
+ target_type: TargetOverrideOption | None = None,
525
+ ) -> int:
526
+ """Transform records using JSON-described operations."""
527
+ state = ensure_state(ctx)
528
+
529
+ source_format = optional_choice(
530
+ source_format,
531
+ FILE_FORMATS,
532
+ label='source_format',
533
+ )
534
+ source_type = optional_choice(
535
+ source_type,
536
+ DATA_CONNECTORS,
537
+ label='source_type',
538
+ )
539
+ target_format = optional_choice(
540
+ target_format,
541
+ FILE_FORMATS,
542
+ label='target_format',
543
+ )
544
+ target_format_kwargs = format_namespace_kwargs(
545
+ format_value=target_format,
546
+ default=DEFAULT_FILE_FORMAT,
547
+ )
548
+ target_type = optional_choice(
549
+ target_type,
550
+ DATA_CONNECTORS,
551
+ label='target_type',
552
+ )
553
+
554
+ resolved_source_type = source_type or infer_resource_type_soft(source)
555
+ resolved_source_value = source if source is not None else '-'
556
+ resolved_target_value = target if target is not None else '-'
557
+
558
+ if resolved_source_type is not None:
559
+ resolved_source_type = validate_choice(
560
+ resolved_source_type,
561
+ DATA_CONNECTORS,
562
+ label='source_type',
563
+ )
564
+
565
+ resolved_target_type = resolve_resource_type(
566
+ explicit_type=None,
567
+ override_type=target_type,
568
+ value=resolved_target_value,
569
+ label='target_type',
570
+ )
571
+
572
+ log_inferred_resource(
573
+ state,
574
+ role='source',
575
+ value=resolved_source_value,
576
+ resource_type=resolved_source_type,
577
+ )
578
+ log_inferred_resource(
579
+ state,
580
+ role='target',
581
+ value=resolved_target_value,
582
+ resource_type=resolved_target_type,
583
+ )
584
+
585
+ ns = stateful_namespace(
586
+ state,
587
+ command='transform',
588
+ source=resolved_source_value,
589
+ source_type=resolved_source_type,
590
+ operations=json_type(operations),
591
+ target=resolved_target_value,
592
+ source_format=source_format,
593
+ target_type=resolved_target_type,
594
+ target_format=target_format_kwargs['format'],
595
+ **target_format_kwargs,
596
+ )
597
+ return int(handlers.transform_handler(ns))
598
+
599
+
600
+ @app.command('validate')
601
+ def validate_cmd(
602
+ ctx: typer.Context,
603
+ rules: RulesOption = '{}',
604
+ source: StreamingSourceArg = '-',
605
+ source_format: SourceFormatOption | None = None,
606
+ source_type: SourceOverrideOption | None = None,
607
+ target: TargetPathOption | None = None,
608
+ ) -> int:
609
+ """Validate data against JSON-described rules."""
610
+ source_format = optional_choice(
611
+ source_format,
612
+ FILE_FORMATS,
613
+ label='source_format',
614
+ )
615
+ source_type = optional_choice(
616
+ source_type,
617
+ DATA_CONNECTORS,
618
+ label='source_type',
619
+ )
620
+ source_format_kwargs = format_namespace_kwargs(
621
+ format_value=source_format,
622
+ default=DEFAULT_FILE_FORMAT,
623
+ )
624
+
625
+ state = ensure_state(ctx)
626
+ resolved_source_type = source_type or infer_resource_type_soft(source)
627
+
628
+ log_inferred_resource(
629
+ state,
630
+ role='source',
631
+ value=source,
632
+ resource_type=resolved_source_type,
633
+ )
634
+
635
+ ns = stateful_namespace(
636
+ state,
637
+ command='validate',
638
+ source=source,
639
+ source_type=resolved_source_type,
640
+ rules=json_type(rules), # convert CLI string to dict
641
+ target=target,
642
+ source_format=source_format,
643
+ **source_format_kwargs,
644
+ )
645
+ return int(handlers.validate_handler(ns))