etlplus 0.8.0__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.8.0/etlplus.egg-info → etlplus-0.8.2}/PKG-INFO +1 -1
  2. etlplus-0.8.2/etlplus/cli/commands.py +645 -0
  3. etlplus-0.8.2/etlplus/cli/constants.py +65 -0
  4. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/cli/handlers.py +34 -311
  5. etlplus-0.8.2/etlplus/cli/io.py +343 -0
  6. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/cli/main.py +46 -108
  7. etlplus-0.8.2/etlplus/cli/options.py +115 -0
  8. etlplus-0.8.2/etlplus/cli/state.py +411 -0
  9. etlplus-0.8.2/etlplus/cli/types.py +33 -0
  10. {etlplus-0.8.0 → etlplus-0.8.2/etlplus.egg-info}/PKG-INFO +1 -1
  11. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus.egg-info/SOURCES.txt +6 -1
  12. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_app.py +15 -22
  13. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_handlers.py +99 -138
  14. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/cli/test_u_cli_main.py +7 -7
  15. etlplus-0.8.0/etlplus/cli/app.py +0 -1312
  16. {etlplus-0.8.0 → etlplus-0.8.2}/.coveragerc +0 -0
  17. {etlplus-0.8.0 → etlplus-0.8.2}/.editorconfig +0 -0
  18. {etlplus-0.8.0 → etlplus-0.8.2}/.gitattributes +0 -0
  19. {etlplus-0.8.0 → etlplus-0.8.2}/.github/actions/python-bootstrap/action.yml +0 -0
  20. {etlplus-0.8.0 → etlplus-0.8.2}/.github/workflows/ci.yml +0 -0
  21. {etlplus-0.8.0 → etlplus-0.8.2}/.gitignore +0 -0
  22. {etlplus-0.8.0 → etlplus-0.8.2}/.pre-commit-config.yaml +0 -0
  23. {etlplus-0.8.0 → etlplus-0.8.2}/.ruff.toml +0 -0
  24. {etlplus-0.8.0 → etlplus-0.8.2}/CODE_OF_CONDUCT.md +0 -0
  25. {etlplus-0.8.0 → etlplus-0.8.2}/CONTRIBUTING.md +0 -0
  26. {etlplus-0.8.0 → etlplus-0.8.2}/DEMO.md +0 -0
  27. {etlplus-0.8.0 → etlplus-0.8.2}/LICENSE +0 -0
  28. {etlplus-0.8.0 → etlplus-0.8.2}/MANIFEST.in +0 -0
  29. {etlplus-0.8.0 → etlplus-0.8.2}/Makefile +0 -0
  30. {etlplus-0.8.0 → etlplus-0.8.2}/README.md +0 -0
  31. {etlplus-0.8.0 → etlplus-0.8.2}/REFERENCES.md +0 -0
  32. {etlplus-0.8.0 → etlplus-0.8.2}/docs/pipeline-guide.md +0 -0
  33. {etlplus-0.8.0 → etlplus-0.8.2}/docs/snippets/installation_version.md +0 -0
  34. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/__init__.py +0 -0
  35. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/__main__.py +0 -0
  36. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/__version__.py +0 -0
  37. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/README.md +0 -0
  38. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/__init__.py +0 -0
  39. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/auth.py +0 -0
  40. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/config.py +0 -0
  41. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/endpoint_client.py +0 -0
  42. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/errors.py +0 -0
  43. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/pagination/__init__.py +0 -0
  44. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/pagination/client.py +0 -0
  45. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/pagination/config.py +0 -0
  46. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/pagination/paginator.py +0 -0
  47. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/rate_limiting/__init__.py +0 -0
  48. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/rate_limiting/config.py +0 -0
  49. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  50. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/request_manager.py +0 -0
  51. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/retry_manager.py +0 -0
  52. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/transport.py +0 -0
  53. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/api/types.py +0 -0
  54. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/cli/__init__.py +0 -0
  55. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/__init__.py +0 -0
  56. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/connector.py +0 -0
  57. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/jobs.py +0 -0
  58. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/pipeline.py +0 -0
  59. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/profile.py +0 -0
  60. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/types.py +0 -0
  61. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/config/utils.py +0 -0
  62. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/__init__.py +0 -0
  63. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/ddl.py +0 -0
  64. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/engine.py +0 -0
  65. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/orm.py +0 -0
  66. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/schema.py +0 -0
  67. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/database/types.py +0 -0
  68. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/enums.py +0 -0
  69. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/extract.py +0 -0
  70. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/file.py +0 -0
  71. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/load.py +0 -0
  72. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/mixins.py +0 -0
  73. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/py.typed +0 -0
  74. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/run.py +0 -0
  75. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/run_helpers.py +0 -0
  76. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/templates/__init__.py +0 -0
  77. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/templates/ddl.sql.j2 +0 -0
  78. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/templates/view.sql.j2 +0 -0
  79. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/transform.py +0 -0
  80. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/types.py +0 -0
  81. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/utils.py +0 -0
  82. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/validate.py +0 -0
  83. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/validation/__init__.py +0 -0
  84. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus/validation/utils.py +0 -0
  85. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus.egg-info/dependency_links.txt +0 -0
  86. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus.egg-info/entry_points.txt +0 -0
  87. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus.egg-info/requires.txt +0 -0
  88. {etlplus-0.8.0 → etlplus-0.8.2}/etlplus.egg-info/top_level.txt +0 -0
  89. {etlplus-0.8.0 → etlplus-0.8.2}/examples/README.md +0 -0
  90. {etlplus-0.8.0 → etlplus-0.8.2}/examples/configs/ddl_spec.yml +0 -0
  91. {etlplus-0.8.0 → etlplus-0.8.2}/examples/configs/pipeline.yml +0 -0
  92. {etlplus-0.8.0 → etlplus-0.8.2}/examples/data/sample.csv +0 -0
  93. {etlplus-0.8.0 → etlplus-0.8.2}/examples/data/sample.json +0 -0
  94. {etlplus-0.8.0 → etlplus-0.8.2}/examples/data/sample.xml +0 -0
  95. {etlplus-0.8.0 → etlplus-0.8.2}/examples/data/sample.xsd +0 -0
  96. {etlplus-0.8.0 → etlplus-0.8.2}/examples/data/sample.yaml +0 -0
  97. {etlplus-0.8.0 → etlplus-0.8.2}/examples/quickstart_python.py +0 -0
  98. {etlplus-0.8.0 → etlplus-0.8.2}/pyproject.toml +0 -0
  99. {etlplus-0.8.0 → etlplus-0.8.2}/pytest.ini +0 -0
  100. {etlplus-0.8.0 → etlplus-0.8.2}/setup.cfg +0 -0
  101. {etlplus-0.8.0 → etlplus-0.8.2}/setup.py +0 -0
  102. {etlplus-0.8.0 → etlplus-0.8.2}/tests/__init__.py +0 -0
  103. {etlplus-0.8.0 → etlplus-0.8.2}/tests/conftest.py +0 -0
  104. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/conftest.py +0 -0
  105. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_cli.py +0 -0
  106. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_examples_data_parity.py +0 -0
  107. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_pagination_strategy.py +0 -0
  108. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_pipeline_smoke.py +0 -0
  109. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  110. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_run.py +0 -0
  111. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  112. {etlplus-0.8.0 → etlplus-0.8.2}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  113. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/conftest.py +0 -0
  114. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_auth.py +0 -0
  115. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_config.py +0 -0
  116. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_endpoint_client.py +0 -0
  117. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_mocks.py +0 -0
  118. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_pagination_client.py +0 -0
  119. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_pagination_config.py +0 -0
  120. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_paginator.py +0 -0
  121. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  122. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_rate_limiter.py +0 -0
  123. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_request_manager.py +0 -0
  124. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_retry_manager.py +0 -0
  125. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_transport.py +0 -0
  126. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/api/test_u_types.py +0 -0
  127. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/cli/conftest.py +0 -0
  128. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/config/test_u_config_utils.py +0 -0
  129. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/config/test_u_connector.py +0 -0
  130. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/config/test_u_jobs.py +0 -0
  131. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/config/test_u_pipeline.py +0 -0
  132. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/conftest.py +0 -0
  133. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/database/test_u_database_ddl.py +0 -0
  134. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/database/test_u_database_engine.py +0 -0
  135. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/database/test_u_database_orm.py +0 -0
  136. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/database/test_u_database_schema.py +0 -0
  137. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_enums.py +0 -0
  138. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_extract.py +0 -0
  139. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_file.py +0 -0
  140. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_load.py +0 -0
  141. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_main.py +0 -0
  142. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_mixins.py +0 -0
  143. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_run.py +0 -0
  144. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_run_helpers.py +0 -0
  145. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_transform.py +0 -0
  146. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_utils.py +0 -0
  147. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_validate.py +0 -0
  148. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/test_u_version.py +0 -0
  149. {etlplus-0.8.0 → etlplus-0.8.2}/tests/unit/validation/test_u_validation_utils.py +0 -0
  150. {etlplus-0.8.0 → etlplus-0.8.2}/tools/run_pipeline.py +0 -0
  151. {etlplus-0.8.0 → 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.8.0
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
@@ -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))
@@ -0,0 +1,65 @@
1
+ """
2
+ :mod:`etlplus.cli.constants` module.
3
+
4
+ Shared constants for :mod:`etlplus.cli`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Final
10
+
11
+ from ..enums import DataConnectorType
12
+ from ..enums import FileFormat
13
+
14
+ # SECTION: EXPORTS ========================================================== #
15
+
16
+
17
+ __all__ = [
18
+ # Constants
19
+ 'CLI_DESCRIPTION',
20
+ 'CLI_EPILOG',
21
+ 'DATA_CONNECTORS',
22
+ 'DEFAULT_FILE_FORMAT',
23
+ 'FILE_FORMATS',
24
+ 'PROJECT_URL',
25
+ ]
26
+
27
+
28
+ # SECTION: CONSTANTS ======================================================== #
29
+
30
+
31
+ DATA_CONNECTORS: Final[frozenset[str]] = frozenset(DataConnectorType.choices())
32
+
33
+ FILE_FORMATS: Final[frozenset[str]] = frozenset(FileFormat.choices())
34
+ DEFAULT_FILE_FORMAT: Final[str] = 'json'
35
+
36
+ CLI_DESCRIPTION: Final[str] = '\n'.join(
37
+ [
38
+ 'ETLPlus - A Swiss Army knife for simple ETL operations.',
39
+ '',
40
+ ' Provide a subcommand and options. Examples:',
41
+ '',
42
+ ' etlplus extract in.csv > out.json',
43
+ ' etlplus validate in.json --rules "{"required": ["id"]}"',
44
+ (
45
+ ' etlplus transform --from file in.json '
46
+ '--operations "{"select": ["id"]}" --to file -o out.json'
47
+ ),
48
+ ' etlplus extract in.csv | etlplus load --to file out.json',
49
+ ' cat data.json | etlplus load --to api https://example.com/data',
50
+ '',
51
+ ' Override format inference when extensions are misleading:',
52
+ '',
53
+ ' etlplus extract data.txt --source-format csv',
54
+ ' etlplus load payload.bin --target-format json',
55
+ ],
56
+ )
57
+ CLI_EPILOG: Final[str] = '\n'.join(
58
+ [
59
+ 'Tip:',
60
+ '--source-format and --target-format override format inference '
61
+ 'based on filename extensions when needed.',
62
+ ],
63
+ )
64
+
65
+ PROJECT_URL: Final[str] = 'https://github.com/Dagitali/ETLPlus'