etlplus 0.5.2__py3-none-any.whl → 0.9.1__py3-none-any.whl
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/api/README.md +24 -26
- etlplus/cli/commands.py +924 -0
- etlplus/cli/constants.py +71 -0
- etlplus/cli/handlers.py +369 -484
- etlplus/cli/io.py +336 -0
- etlplus/cli/main.py +16 -418
- etlplus/cli/options.py +49 -0
- etlplus/cli/state.py +336 -0
- etlplus/cli/types.py +33 -0
- etlplus/database/__init__.py +44 -0
- etlplus/database/ddl.py +319 -0
- etlplus/database/engine.py +151 -0
- etlplus/database/orm.py +354 -0
- etlplus/database/schema.py +274 -0
- etlplus/database/types.py +33 -0
- etlplus/enums.py +51 -1
- etlplus/load.py +1 -1
- etlplus/run.py +2 -4
- etlplus/types.py +5 -0
- etlplus/utils.py +1 -32
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/METADATA +84 -40
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/RECORD +26 -16
- etlplus/cli/app.py +0 -1367
- etlplus/ddl.py +0 -197
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/WHEEL +0 -0
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/entry_points.txt +0 -0
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.5.2.dist-info → etlplus-0.9.1.dist-info}/top_level.txt +0 -0
etlplus/cli/commands.py
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
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
|
+
from typing import Any
|
|
33
|
+
from typing import Literal
|
|
34
|
+
from typing import cast
|
|
35
|
+
|
|
36
|
+
import typer
|
|
37
|
+
|
|
38
|
+
from .. import __version__
|
|
39
|
+
from ..enums import FileFormat
|
|
40
|
+
from . import handlers
|
|
41
|
+
from .constants import CLI_DESCRIPTION
|
|
42
|
+
from .constants import CLI_EPILOG
|
|
43
|
+
from .constants import DATA_CONNECTORS
|
|
44
|
+
from .constants import FILE_FORMATS
|
|
45
|
+
from .io import parse_json_payload
|
|
46
|
+
from .options import typer_format_option_kwargs
|
|
47
|
+
from .state import CliState
|
|
48
|
+
from .state import ensure_state
|
|
49
|
+
from .state import infer_resource_type_or_exit
|
|
50
|
+
from .state import infer_resource_type_soft
|
|
51
|
+
from .state import log_inferred_resource
|
|
52
|
+
from .state import optional_choice
|
|
53
|
+
from .state import resolve_resource_type
|
|
54
|
+
from .state import validate_choice
|
|
55
|
+
|
|
56
|
+
# SECTION: EXPORTS ========================================================== #
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ['app']
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# SECTION: TYPE ALIASES ==================================================== #
|
|
63
|
+
|
|
64
|
+
OperationsOption = Annotated[
|
|
65
|
+
str,
|
|
66
|
+
typer.Option(
|
|
67
|
+
'--operations',
|
|
68
|
+
help='Transformation operations as JSON string.',
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
OutputOption = Annotated[
|
|
73
|
+
str | None,
|
|
74
|
+
typer.Option(
|
|
75
|
+
'--output',
|
|
76
|
+
'-o',
|
|
77
|
+
metavar='PATH',
|
|
78
|
+
help='Write output to file PATH (default: STDOUT).',
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
PipelineConfigOption = Annotated[
|
|
83
|
+
str,
|
|
84
|
+
typer.Option(
|
|
85
|
+
...,
|
|
86
|
+
'--config',
|
|
87
|
+
metavar='PATH',
|
|
88
|
+
help='Path to pipeline YAML configuration file.',
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
RenderConfigOption = Annotated[
|
|
93
|
+
str | None,
|
|
94
|
+
typer.Option(
|
|
95
|
+
'--config',
|
|
96
|
+
metavar='PATH',
|
|
97
|
+
help='Pipeline YAML that includes table_schemas for rendering.',
|
|
98
|
+
show_default=False,
|
|
99
|
+
),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
RenderOutputOption = Annotated[
|
|
103
|
+
str | None,
|
|
104
|
+
typer.Option(
|
|
105
|
+
'--output',
|
|
106
|
+
'-o',
|
|
107
|
+
metavar='PATH',
|
|
108
|
+
help='Write rendered SQL to PATH (default: STDOUT).',
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
RenderSpecOption = Annotated[
|
|
113
|
+
str | None,
|
|
114
|
+
typer.Option(
|
|
115
|
+
'--spec',
|
|
116
|
+
metavar='PATH',
|
|
117
|
+
help='Standalone table spec file (.yml/.yaml/.json).',
|
|
118
|
+
show_default=False,
|
|
119
|
+
),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
RenderTableOption = Annotated[
|
|
123
|
+
str | None,
|
|
124
|
+
typer.Option(
|
|
125
|
+
'--table',
|
|
126
|
+
metavar='NAME',
|
|
127
|
+
help='Filter to a single table name from table_schemas.',
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
RenderTemplateOption = Annotated[
|
|
132
|
+
Literal['ddl', 'view'] | None,
|
|
133
|
+
typer.Option(
|
|
134
|
+
'--template',
|
|
135
|
+
'-t',
|
|
136
|
+
metavar='KEY',
|
|
137
|
+
help='Template key (ddl/view).',
|
|
138
|
+
show_default=True,
|
|
139
|
+
),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
RenderTemplatePathOption = Annotated[
|
|
143
|
+
str | None,
|
|
144
|
+
typer.Option(
|
|
145
|
+
'--template-path',
|
|
146
|
+
metavar='PATH',
|
|
147
|
+
help=(
|
|
148
|
+
'Explicit path to a Jinja template file (overrides template key).'
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
RulesOption = Annotated[
|
|
154
|
+
str,
|
|
155
|
+
typer.Option(
|
|
156
|
+
'--rules',
|
|
157
|
+
help='Validation rules as JSON string.',
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
SourceArg = Annotated[
|
|
162
|
+
str,
|
|
163
|
+
typer.Argument(
|
|
164
|
+
...,
|
|
165
|
+
metavar='SOURCE',
|
|
166
|
+
help=(
|
|
167
|
+
'Extract data from SOURCE (JSON payload, file/folder path, '
|
|
168
|
+
'URI/URL, or - for STDIN). Use --source-format to override the '
|
|
169
|
+
'inferred data format and --source-type to override the inferred '
|
|
170
|
+
'data connector.'
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
SourceFormatOption = Annotated[
|
|
176
|
+
FileFormat | None,
|
|
177
|
+
typer.Option(
|
|
178
|
+
'--source-format',
|
|
179
|
+
**typer_format_option_kwargs(context='source'),
|
|
180
|
+
),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
SourceTypeOption = Annotated[
|
|
184
|
+
str | None,
|
|
185
|
+
typer.Option(
|
|
186
|
+
'--source-type',
|
|
187
|
+
metavar='CONNECTOR',
|
|
188
|
+
show_default=False,
|
|
189
|
+
rich_help_panel='I/O overrides',
|
|
190
|
+
help=(
|
|
191
|
+
'Override the inferred source type (api, database, file, folder).'
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
TargetArg = Annotated[
|
|
197
|
+
str,
|
|
198
|
+
typer.Argument(
|
|
199
|
+
...,
|
|
200
|
+
metavar='TARGET',
|
|
201
|
+
help=(
|
|
202
|
+
'Load data into TARGET (file/folder path, URI/URL, or - for '
|
|
203
|
+
'STDOUT). Use --target-format to override the inferred data '
|
|
204
|
+
'format and --target-type to override the inferred data connector.'
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
TargetFormatOption = Annotated[
|
|
210
|
+
FileFormat | None,
|
|
211
|
+
typer.Option(
|
|
212
|
+
'--target-format',
|
|
213
|
+
**typer_format_option_kwargs(context='target'),
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
TargetTypeOption = Annotated[
|
|
218
|
+
str | None,
|
|
219
|
+
typer.Option(
|
|
220
|
+
'--target-type',
|
|
221
|
+
metavar='CONNECTOR',
|
|
222
|
+
show_default=False,
|
|
223
|
+
rich_help_panel='I/O overrides',
|
|
224
|
+
help=(
|
|
225
|
+
'Override the inferred target type (api, database, file, folder).'
|
|
226
|
+
),
|
|
227
|
+
),
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_json_option(
|
|
235
|
+
value: str,
|
|
236
|
+
flag: str,
|
|
237
|
+
) -> Any:
|
|
238
|
+
"""
|
|
239
|
+
Parse JSON option values and surface a helpful CLI error.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
value : str
|
|
244
|
+
The JSON string to parse.
|
|
245
|
+
flag : str
|
|
246
|
+
The CLI flag name for error messages.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
Any
|
|
251
|
+
The parsed JSON value.
|
|
252
|
+
|
|
253
|
+
Raises
|
|
254
|
+
------
|
|
255
|
+
typer.BadParameter
|
|
256
|
+
When the JSON is invalid.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
return parse_json_payload(value)
|
|
260
|
+
except ValueError as e:
|
|
261
|
+
raise typer.BadParameter(
|
|
262
|
+
f'Invalid JSON for {flag}: {e}',
|
|
263
|
+
) from e
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# SECTION: TYPER APP ======================================================== #
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
app = typer.Typer(
|
|
270
|
+
name='etlplus',
|
|
271
|
+
help=CLI_DESCRIPTION,
|
|
272
|
+
epilog=CLI_EPILOG,
|
|
273
|
+
add_completion=True,
|
|
274
|
+
no_args_is_help=False,
|
|
275
|
+
rich_markup_mode='markdown',
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@app.callback(invoke_without_command=True)
|
|
280
|
+
def _root(
|
|
281
|
+
ctx: typer.Context,
|
|
282
|
+
version: bool = typer.Option(
|
|
283
|
+
False,
|
|
284
|
+
'--version',
|
|
285
|
+
'-V',
|
|
286
|
+
is_eager=True,
|
|
287
|
+
help='Show the version and exit.',
|
|
288
|
+
),
|
|
289
|
+
pretty: bool = typer.Option(
|
|
290
|
+
True,
|
|
291
|
+
'--pretty/--no-pretty',
|
|
292
|
+
help='Pretty-print JSON output (default: pretty).',
|
|
293
|
+
),
|
|
294
|
+
quiet: bool = typer.Option(
|
|
295
|
+
False,
|
|
296
|
+
'--quiet',
|
|
297
|
+
'-q',
|
|
298
|
+
help='Suppress warnings and non-essential output.',
|
|
299
|
+
),
|
|
300
|
+
verbose: bool = typer.Option(
|
|
301
|
+
False,
|
|
302
|
+
'--verbose',
|
|
303
|
+
'-v',
|
|
304
|
+
help='Emit extra diagnostics to STDERR.',
|
|
305
|
+
),
|
|
306
|
+
) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Seed the Typer context with runtime flags and handle root-only options.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
ctx : typer.Context
|
|
313
|
+
The Typer command context.
|
|
314
|
+
version : bool, optional
|
|
315
|
+
Show the version and exit. Default is ``False``.
|
|
316
|
+
pretty : bool, optional
|
|
317
|
+
Whether to pretty-print JSON output. Default is ``True``.
|
|
318
|
+
quiet : bool, optional
|
|
319
|
+
Whether to suppress warnings and non-essential output. Default is
|
|
320
|
+
``False``.
|
|
321
|
+
verbose : bool, optional
|
|
322
|
+
Whether to emit extra diagnostics to STDERR. Default is ``False``.
|
|
323
|
+
|
|
324
|
+
Raises
|
|
325
|
+
------
|
|
326
|
+
typer.Exit
|
|
327
|
+
When ``--version`` is provided or no subcommand is invoked.
|
|
328
|
+
"""
|
|
329
|
+
ctx.obj = CliState(pretty=pretty, quiet=quiet, verbose=verbose)
|
|
330
|
+
|
|
331
|
+
if version:
|
|
332
|
+
typer.echo(f'etlplus {__version__}')
|
|
333
|
+
raise typer.Exit(0)
|
|
334
|
+
|
|
335
|
+
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
336
|
+
typer.echo(ctx.command.get_help(ctx))
|
|
337
|
+
raise typer.Exit(0)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@app.command('check')
|
|
341
|
+
def check_cmd(
|
|
342
|
+
ctx: typer.Context,
|
|
343
|
+
config: PipelineConfigOption,
|
|
344
|
+
jobs: bool = typer.Option(
|
|
345
|
+
False,
|
|
346
|
+
'--jobs',
|
|
347
|
+
help='List available job names and exit',
|
|
348
|
+
),
|
|
349
|
+
pipelines: bool = typer.Option(
|
|
350
|
+
False,
|
|
351
|
+
'--pipelines',
|
|
352
|
+
help='List ETL pipelines',
|
|
353
|
+
),
|
|
354
|
+
sources: bool = typer.Option(
|
|
355
|
+
False,
|
|
356
|
+
'--sources',
|
|
357
|
+
help='List data sources',
|
|
358
|
+
),
|
|
359
|
+
summary: bool = typer.Option(
|
|
360
|
+
False,
|
|
361
|
+
'--summary',
|
|
362
|
+
help='Show pipeline summary (name, version, sources, targets, jobs)',
|
|
363
|
+
),
|
|
364
|
+
targets: bool = typer.Option(
|
|
365
|
+
False,
|
|
366
|
+
'--targets',
|
|
367
|
+
help='List data targets',
|
|
368
|
+
),
|
|
369
|
+
transforms: bool = typer.Option(
|
|
370
|
+
False,
|
|
371
|
+
'--transforms',
|
|
372
|
+
help='List data transforms',
|
|
373
|
+
),
|
|
374
|
+
) -> int:
|
|
375
|
+
"""
|
|
376
|
+
Inspect a pipeline configuration.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
ctx : typer.Context
|
|
381
|
+
The Typer context.
|
|
382
|
+
config : PipelineConfigOption
|
|
383
|
+
Path to pipeline YAML configuration file.
|
|
384
|
+
jobs : bool, optional
|
|
385
|
+
List available job names and exit. Default is ``False``.
|
|
386
|
+
pipelines : bool, optional
|
|
387
|
+
List ETL pipelines. Default is ``False``.
|
|
388
|
+
sources : bool, optional
|
|
389
|
+
List data sources. Default is ``False``.
|
|
390
|
+
summary : bool, optional
|
|
391
|
+
Show pipeline summary (name, version, sources, targets, jobs). Default
|
|
392
|
+
is ``False``.
|
|
393
|
+
targets : bool, optional
|
|
394
|
+
List data targets. Default is ``False``.
|
|
395
|
+
transforms : bool, optional
|
|
396
|
+
List data transforms. Default is ``False``.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
int
|
|
401
|
+
Exit code.
|
|
402
|
+
|
|
403
|
+
Raises
|
|
404
|
+
------
|
|
405
|
+
typer.Exit
|
|
406
|
+
When argument order is invalid or required arguments are missing.
|
|
407
|
+
"""
|
|
408
|
+
# Argument order enforcement.
|
|
409
|
+
if not config:
|
|
410
|
+
typer.echo("Error: Missing required option '--config'.", err=True)
|
|
411
|
+
raise typer.Exit(2)
|
|
412
|
+
|
|
413
|
+
state = ensure_state(ctx)
|
|
414
|
+
return int(
|
|
415
|
+
handlers.check_handler(
|
|
416
|
+
config=config,
|
|
417
|
+
jobs=jobs,
|
|
418
|
+
pipelines=pipelines,
|
|
419
|
+
sources=sources,
|
|
420
|
+
summary=summary,
|
|
421
|
+
targets=targets,
|
|
422
|
+
transforms=transforms,
|
|
423
|
+
pretty=state.pretty,
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@app.command('extract')
|
|
429
|
+
def extract_cmd(
|
|
430
|
+
ctx: typer.Context,
|
|
431
|
+
source: SourceArg = '-',
|
|
432
|
+
source_format: SourceFormatOption = None,
|
|
433
|
+
source_type: SourceTypeOption = None,
|
|
434
|
+
) -> int:
|
|
435
|
+
"""
|
|
436
|
+
Extract data from files, databases, or REST APIs.
|
|
437
|
+
|
|
438
|
+
Parameters
|
|
439
|
+
----------
|
|
440
|
+
ctx : typer.Context
|
|
441
|
+
The Typer context.
|
|
442
|
+
source : SourceArg, optional
|
|
443
|
+
Source (JSON payload, file/folder path, URL/URI, or - for STDIN)
|
|
444
|
+
from which to extract data. Default is ``-``.
|
|
445
|
+
source_format : SourceFormatOption, optional
|
|
446
|
+
Data source format. Overrides the inferred format (``csv``, ``json``,
|
|
447
|
+
etc.) based on filename extension or STDIN content. Default is
|
|
448
|
+
``None``.
|
|
449
|
+
source_type : SourceTypeOption, optional
|
|
450
|
+
Data source type. Overrides the inferred type (``api``, ``database``,
|
|
451
|
+
``file``, ``folder``) based on URI/URL schema. Default is ``None``.
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
int
|
|
456
|
+
Exit code.
|
|
457
|
+
|
|
458
|
+
Raises
|
|
459
|
+
------
|
|
460
|
+
typer.Exit
|
|
461
|
+
When argument order is invalid or required arguments are missing.
|
|
462
|
+
"""
|
|
463
|
+
state = ensure_state(ctx)
|
|
464
|
+
|
|
465
|
+
# Argument order enforcement
|
|
466
|
+
if source.startswith('--'):
|
|
467
|
+
typer.echo(
|
|
468
|
+
f"Error: Option '{source}' must follow the 'SOURCE' argument.",
|
|
469
|
+
err=True,
|
|
470
|
+
)
|
|
471
|
+
raise typer.Exit(2)
|
|
472
|
+
if not source:
|
|
473
|
+
typer.echo("Error: Missing required argument 'SOURCE'.", err=True)
|
|
474
|
+
raise typer.Exit(2)
|
|
475
|
+
|
|
476
|
+
source_type = optional_choice(
|
|
477
|
+
source_type,
|
|
478
|
+
DATA_CONNECTORS,
|
|
479
|
+
label='source_type',
|
|
480
|
+
)
|
|
481
|
+
source_format = cast(
|
|
482
|
+
SourceFormatOption,
|
|
483
|
+
optional_choice(
|
|
484
|
+
source_format,
|
|
485
|
+
FILE_FORMATS,
|
|
486
|
+
label='source_format',
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
resolved_source_type = source_type or infer_resource_type_or_exit(source)
|
|
491
|
+
|
|
492
|
+
log_inferred_resource(
|
|
493
|
+
state,
|
|
494
|
+
role='source',
|
|
495
|
+
value=source,
|
|
496
|
+
resource_type=resolved_source_type,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return int(
|
|
500
|
+
handlers.extract_handler(
|
|
501
|
+
source_type=resolved_source_type,
|
|
502
|
+
source=source,
|
|
503
|
+
format_hint=source_format,
|
|
504
|
+
format_explicit=source_format is not None,
|
|
505
|
+
pretty=state.pretty,
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@app.command('load')
|
|
511
|
+
def load_cmd(
|
|
512
|
+
ctx: typer.Context,
|
|
513
|
+
source_format: SourceFormatOption = None,
|
|
514
|
+
target: TargetArg = '-',
|
|
515
|
+
target_format: TargetFormatOption = None,
|
|
516
|
+
target_type: TargetTypeOption = None,
|
|
517
|
+
) -> int:
|
|
518
|
+
"""
|
|
519
|
+
Load data into a file, database, or REST API.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
ctx : typer.Context
|
|
524
|
+
The Typer context.
|
|
525
|
+
source_format : SourceFormatOption, optional
|
|
526
|
+
Data source format. Overrides the inferred format (``csv``, ``json``,
|
|
527
|
+
etc.) based on filename extension or STDIN content. Default is
|
|
528
|
+
``None``.
|
|
529
|
+
target : TargetArg, optional
|
|
530
|
+
Target (file/folder path, URL/URI, or - for STDOUT) into which to load
|
|
531
|
+
data. Default is ``-``.
|
|
532
|
+
target_format : TargetFormatOption, optional
|
|
533
|
+
Target data format. Overrides the inferred format (``csv``, ``json``,
|
|
534
|
+
etc.) based on filename extension. Default is ``None``.
|
|
535
|
+
target_type : TargetTypeOption, optional
|
|
536
|
+
Data target type. Overrides the inferred type (``api``, ``database``,
|
|
537
|
+
``file``, ``folder``) based on URI/URL schema. Default is ``None``.
|
|
538
|
+
|
|
539
|
+
Returns
|
|
540
|
+
-------
|
|
541
|
+
int
|
|
542
|
+
Exit code.
|
|
543
|
+
|
|
544
|
+
Raises
|
|
545
|
+
------
|
|
546
|
+
typer.Exit
|
|
547
|
+
When argument order is invalid or required arguments are missing.
|
|
548
|
+
"""
|
|
549
|
+
# Argument order enforcement
|
|
550
|
+
if target.startswith('--'):
|
|
551
|
+
typer.echo(
|
|
552
|
+
f"Error: Option '{target}' must follow the 'TARGET' argument.",
|
|
553
|
+
err=True,
|
|
554
|
+
)
|
|
555
|
+
raise typer.Exit(2)
|
|
556
|
+
if not target:
|
|
557
|
+
typer.echo("Error: Missing required argument 'TARGET'.", err=True)
|
|
558
|
+
raise typer.Exit(2)
|
|
559
|
+
|
|
560
|
+
state = ensure_state(ctx)
|
|
561
|
+
|
|
562
|
+
source_format = cast(
|
|
563
|
+
SourceFormatOption,
|
|
564
|
+
optional_choice(
|
|
565
|
+
source_format,
|
|
566
|
+
FILE_FORMATS,
|
|
567
|
+
label='source_format',
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
target_type = optional_choice(
|
|
571
|
+
target_type,
|
|
572
|
+
DATA_CONNECTORS,
|
|
573
|
+
label='target_type',
|
|
574
|
+
)
|
|
575
|
+
target_format = cast(
|
|
576
|
+
TargetFormatOption,
|
|
577
|
+
optional_choice(
|
|
578
|
+
target_format,
|
|
579
|
+
FILE_FORMATS,
|
|
580
|
+
label='target_format',
|
|
581
|
+
),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
resolved_target = target
|
|
585
|
+
resolved_target_type = target_type or infer_resource_type_or_exit(
|
|
586
|
+
resolved_target,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
resolved_source_value = '-'
|
|
590
|
+
resolved_source_type = infer_resource_type_soft(resolved_source_value)
|
|
591
|
+
|
|
592
|
+
log_inferred_resource(
|
|
593
|
+
state,
|
|
594
|
+
role='source',
|
|
595
|
+
value=resolved_source_value,
|
|
596
|
+
resource_type=resolved_source_type,
|
|
597
|
+
)
|
|
598
|
+
log_inferred_resource(
|
|
599
|
+
state,
|
|
600
|
+
role='target',
|
|
601
|
+
value=resolved_target,
|
|
602
|
+
resource_type=resolved_target_type,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return int(
|
|
606
|
+
handlers.load_handler(
|
|
607
|
+
source=resolved_source_value,
|
|
608
|
+
target_type=resolved_target_type,
|
|
609
|
+
target=resolved_target,
|
|
610
|
+
source_format=source_format,
|
|
611
|
+
target_format=target_format,
|
|
612
|
+
format_explicit=target_format is not None,
|
|
613
|
+
output=None,
|
|
614
|
+
pretty=state.pretty,
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
@app.command('render')
|
|
620
|
+
def render_cmd(
|
|
621
|
+
ctx: typer.Context,
|
|
622
|
+
config: RenderConfigOption = None,
|
|
623
|
+
spec: RenderSpecOption = None,
|
|
624
|
+
table: RenderTableOption = None,
|
|
625
|
+
template: RenderTemplateOption = 'ddl',
|
|
626
|
+
template_path: RenderTemplatePathOption = None,
|
|
627
|
+
output: OutputOption = None,
|
|
628
|
+
) -> int:
|
|
629
|
+
"""
|
|
630
|
+
Render SQL DDL from table schemas defined in YAML/JSON configs.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
ctx : typer.Context
|
|
635
|
+
The Typer context.
|
|
636
|
+
config : RenderConfigOption
|
|
637
|
+
Pipeline YAML that includes table_schemas for rendering.
|
|
638
|
+
spec : RenderSpecOption, optional
|
|
639
|
+
Standalone table spec file (.yml/.yaml/.json).
|
|
640
|
+
table : RenderTableOption, optional
|
|
641
|
+
Filter to a single table name from table_schemas.
|
|
642
|
+
template : RenderTemplateOption
|
|
643
|
+
Template key (ddl/view) or path to a Jinja template file.
|
|
644
|
+
template_path : RenderTemplatePathOption, optional
|
|
645
|
+
Explicit path to a Jinja template file (overrides template key).
|
|
646
|
+
output : OutputOption, optional
|
|
647
|
+
Path of file to which to write rendered SQL (default: STDOUT).
|
|
648
|
+
|
|
649
|
+
Returns
|
|
650
|
+
-------
|
|
651
|
+
int
|
|
652
|
+
Exit code.
|
|
653
|
+
|
|
654
|
+
Raises
|
|
655
|
+
------
|
|
656
|
+
typer.Exit
|
|
657
|
+
When argument order is invalid or required arguments are missing.
|
|
658
|
+
"""
|
|
659
|
+
# Argument order enforcement
|
|
660
|
+
if not (config or spec):
|
|
661
|
+
typer.echo(
|
|
662
|
+
"Error: Missing required option '--config' or '--spec'.",
|
|
663
|
+
err=True,
|
|
664
|
+
)
|
|
665
|
+
raise typer.Exit(2)
|
|
666
|
+
|
|
667
|
+
state = ensure_state(ctx)
|
|
668
|
+
return int(
|
|
669
|
+
handlers.render_handler(
|
|
670
|
+
config=config,
|
|
671
|
+
spec=spec,
|
|
672
|
+
table=table,
|
|
673
|
+
template=template,
|
|
674
|
+
template_path=template_path,
|
|
675
|
+
output=output,
|
|
676
|
+
pretty=state.pretty,
|
|
677
|
+
quiet=state.quiet,
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@app.command('run')
|
|
683
|
+
def run_cmd(
|
|
684
|
+
ctx: typer.Context,
|
|
685
|
+
config: PipelineConfigOption,
|
|
686
|
+
job: str | None = typer.Option(
|
|
687
|
+
None,
|
|
688
|
+
'-j',
|
|
689
|
+
'--job',
|
|
690
|
+
help='Name of the job to run',
|
|
691
|
+
),
|
|
692
|
+
pipeline: str | None = typer.Option(
|
|
693
|
+
None,
|
|
694
|
+
'-p',
|
|
695
|
+
'--pipeline',
|
|
696
|
+
help='Name of the pipeline to run',
|
|
697
|
+
),
|
|
698
|
+
) -> int:
|
|
699
|
+
"""
|
|
700
|
+
Execute an ETL job or pipeline from a YAML configuration.
|
|
701
|
+
|
|
702
|
+
Parameters
|
|
703
|
+
----------
|
|
704
|
+
ctx : typer.Context
|
|
705
|
+
The Typer context.
|
|
706
|
+
config : PipelineConfigOption
|
|
707
|
+
Path to pipeline YAML configuration file.
|
|
708
|
+
job : str | None, optional
|
|
709
|
+
Name of the job to run. Default is ``None``.
|
|
710
|
+
pipeline : str | None, optional
|
|
711
|
+
Name of the pipeline to run. Default is ``None``.
|
|
712
|
+
|
|
713
|
+
Returns
|
|
714
|
+
-------
|
|
715
|
+
int
|
|
716
|
+
Exit code.
|
|
717
|
+
|
|
718
|
+
Raises
|
|
719
|
+
------
|
|
720
|
+
typer.Exit
|
|
721
|
+
When argument order is invalid or required arguments are missing.
|
|
722
|
+
"""
|
|
723
|
+
# Argument order enforcement
|
|
724
|
+
if not config:
|
|
725
|
+
typer.echo("Error: Missing required option '--config'.", err=True)
|
|
726
|
+
raise typer.Exit(2)
|
|
727
|
+
|
|
728
|
+
state = ensure_state(ctx)
|
|
729
|
+
return int(
|
|
730
|
+
handlers.run_handler(
|
|
731
|
+
config=config,
|
|
732
|
+
job=job,
|
|
733
|
+
pipeline=pipeline,
|
|
734
|
+
pretty=state.pretty,
|
|
735
|
+
),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@app.command('transform')
|
|
740
|
+
def transform_cmd(
|
|
741
|
+
ctx: typer.Context,
|
|
742
|
+
operations: OperationsOption = '{}',
|
|
743
|
+
source: SourceArg = '-',
|
|
744
|
+
source_format: SourceFormatOption = None,
|
|
745
|
+
source_type: SourceTypeOption = None,
|
|
746
|
+
target: TargetArg = '-',
|
|
747
|
+
target_format: TargetFormatOption = None,
|
|
748
|
+
target_type: TargetTypeOption = None,
|
|
749
|
+
) -> int:
|
|
750
|
+
"""
|
|
751
|
+
Transform records using JSON-described operations.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
ctx : typer.Context
|
|
756
|
+
The Typer context.
|
|
757
|
+
operations : OperationsOption, optional
|
|
758
|
+
Transformation operations as JSON string. Default is ``{}``.
|
|
759
|
+
source : SourceArg, optional
|
|
760
|
+
Source (JSON payload, file/folder path, URL/URI, or - for STDIN) from
|
|
761
|
+
which to extract data. Default is ``-``.
|
|
762
|
+
source_format : SourceFormatOption, optional
|
|
763
|
+
Data source format. Overrides the inferred format (``csv``, ``json``,
|
|
764
|
+
etc.) based on filename extension or STDIN content. Default is
|
|
765
|
+
``None``.
|
|
766
|
+
source_type : SourceTypeOption, optional
|
|
767
|
+
Data source type. Overrides the inferred type (``api``, ``database``,
|
|
768
|
+
``file``, ``folder``) based on URI/URL schema. Default is ``None``.
|
|
769
|
+
target : TargetArg, optional
|
|
770
|
+
Target (file/folder path, URL/URI, or - for STDOUT) into which to load
|
|
771
|
+
data. Default is ``-``.
|
|
772
|
+
target_format : TargetFormatOption, optional
|
|
773
|
+
Target data format. Overrides the inferred format (``csv``, ``json``,
|
|
774
|
+
etc.) based on filename extension. Default is ``None``.
|
|
775
|
+
target_type : TargetTypeOption, optional
|
|
776
|
+
Data target type. Overrides the inferred type (``api``, ``database``,
|
|
777
|
+
``file``, ``folder``) based on URI/URL schema. Default is ``None``.
|
|
778
|
+
|
|
779
|
+
Returns
|
|
780
|
+
-------
|
|
781
|
+
int
|
|
782
|
+
Exit code.
|
|
783
|
+
"""
|
|
784
|
+
state = ensure_state(ctx)
|
|
785
|
+
|
|
786
|
+
source_format = cast(
|
|
787
|
+
SourceFormatOption,
|
|
788
|
+
optional_choice(
|
|
789
|
+
source_format,
|
|
790
|
+
FILE_FORMATS,
|
|
791
|
+
label='source_format',
|
|
792
|
+
),
|
|
793
|
+
)
|
|
794
|
+
source_type = optional_choice(
|
|
795
|
+
source_type,
|
|
796
|
+
DATA_CONNECTORS,
|
|
797
|
+
label='source_type',
|
|
798
|
+
)
|
|
799
|
+
target_format = cast(
|
|
800
|
+
TargetFormatOption,
|
|
801
|
+
optional_choice(
|
|
802
|
+
target_format,
|
|
803
|
+
FILE_FORMATS,
|
|
804
|
+
label='target_format',
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
target_type = optional_choice(
|
|
808
|
+
target_type,
|
|
809
|
+
DATA_CONNECTORS,
|
|
810
|
+
label='target_type',
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
resolved_source_type = source_type or infer_resource_type_soft(source)
|
|
814
|
+
resolved_source_value = source if source is not None else '-'
|
|
815
|
+
resolved_target_value = target if target is not None else '-'
|
|
816
|
+
|
|
817
|
+
if resolved_source_type is not None:
|
|
818
|
+
resolved_source_type = validate_choice(
|
|
819
|
+
resolved_source_type,
|
|
820
|
+
DATA_CONNECTORS,
|
|
821
|
+
label='source_type',
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
resolved_target_type = resolve_resource_type(
|
|
825
|
+
explicit_type=None,
|
|
826
|
+
override_type=target_type,
|
|
827
|
+
value=resolved_target_value,
|
|
828
|
+
label='target_type',
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
log_inferred_resource(
|
|
832
|
+
state,
|
|
833
|
+
role='source',
|
|
834
|
+
value=resolved_source_value,
|
|
835
|
+
resource_type=resolved_source_type,
|
|
836
|
+
)
|
|
837
|
+
log_inferred_resource(
|
|
838
|
+
state,
|
|
839
|
+
role='target',
|
|
840
|
+
value=resolved_target_value,
|
|
841
|
+
resource_type=resolved_target_type,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
return int(
|
|
845
|
+
handlers.transform_handler(
|
|
846
|
+
source=resolved_source_value,
|
|
847
|
+
operations=_parse_json_option(operations, '--operations'),
|
|
848
|
+
target=resolved_target_value,
|
|
849
|
+
source_format=source_format,
|
|
850
|
+
target_format=target_format,
|
|
851
|
+
format_explicit=target_format is not None,
|
|
852
|
+
pretty=state.pretty,
|
|
853
|
+
),
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
@app.command('validate')
|
|
858
|
+
def validate_cmd(
|
|
859
|
+
ctx: typer.Context,
|
|
860
|
+
rules: RulesOption = '{}',
|
|
861
|
+
source: SourceArg = '-',
|
|
862
|
+
source_format: SourceFormatOption = None,
|
|
863
|
+
source_type: SourceTypeOption = None,
|
|
864
|
+
output: OutputOption = '-',
|
|
865
|
+
) -> int:
|
|
866
|
+
"""
|
|
867
|
+
Validate data against JSON-described rules.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
ctx : typer.Context
|
|
872
|
+
The Typer context.
|
|
873
|
+
rules : RulesOption
|
|
874
|
+
Validation rules as JSON string.
|
|
875
|
+
source : SourceArg
|
|
876
|
+
Data source to validate (path, JSON payload, or - for STDIN).
|
|
877
|
+
source_format : SourceFormatOption, optional
|
|
878
|
+
Data source format. Overrides the inferred format (``csv``, ``json``,
|
|
879
|
+
etc.) based on filename extension or STDIN content. Default is
|
|
880
|
+
``None``.
|
|
881
|
+
source_type : SourceTypeOption, optional
|
|
882
|
+
Data source type. Overrides the inferred type (``api``, ``database``,
|
|
883
|
+
``file``, ``folder``) based on URI/URL schema. Default is ``None``.
|
|
884
|
+
output : OutputOption, optional
|
|
885
|
+
Output file for validated output (- for STDOUT). Default is ``None``.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
int
|
|
890
|
+
Exit code.
|
|
891
|
+
"""
|
|
892
|
+
source_format = cast(
|
|
893
|
+
SourceFormatOption,
|
|
894
|
+
optional_choice(
|
|
895
|
+
source_format,
|
|
896
|
+
FILE_FORMATS,
|
|
897
|
+
label='source_format',
|
|
898
|
+
),
|
|
899
|
+
)
|
|
900
|
+
source_type = optional_choice(
|
|
901
|
+
source_type,
|
|
902
|
+
DATA_CONNECTORS,
|
|
903
|
+
label='source_type',
|
|
904
|
+
)
|
|
905
|
+
state = ensure_state(ctx)
|
|
906
|
+
resolved_source_type = source_type or infer_resource_type_soft(source)
|
|
907
|
+
|
|
908
|
+
log_inferred_resource(
|
|
909
|
+
state,
|
|
910
|
+
role='source',
|
|
911
|
+
value=source,
|
|
912
|
+
resource_type=resolved_source_type,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
return int(
|
|
916
|
+
handlers.validate_handler(
|
|
917
|
+
source=source,
|
|
918
|
+
rules=_parse_json_option(rules, '--rules'),
|
|
919
|
+
source_format=source_format,
|
|
920
|
+
target=output,
|
|
921
|
+
format_explicit=source_format is not None,
|
|
922
|
+
pretty=state.pretty,
|
|
923
|
+
),
|
|
924
|
+
)
|