etlplus 0.8.0__py3-none-any.whl → 0.8.2__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/cli/commands.py +645 -0
- etlplus/cli/constants.py +65 -0
- etlplus/cli/handlers.py +34 -311
- etlplus/cli/io.py +343 -0
- etlplus/cli/main.py +46 -108
- etlplus/cli/options.py +115 -0
- etlplus/cli/state.py +411 -0
- etlplus/cli/types.py +33 -0
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/METADATA +1 -1
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/RECORD +14 -9
- etlplus/cli/app.py +0 -1312
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/WHEEL +0 -0
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/entry_points.txt +0 -0
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.8.0.dist-info → etlplus-0.8.2.dist-info}/top_level.txt +0 -0
etlplus/cli/handlers.py
CHANGED
|
@@ -7,9 +7,6 @@ Command handler functions for the ``etlplus`` command-line interface (CLI).
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
-
import csv
|
|
11
|
-
import io
|
|
12
|
-
import json
|
|
13
10
|
import os
|
|
14
11
|
import sys
|
|
15
12
|
from pathlib import Path
|
|
@@ -20,17 +17,14 @@ from ..config import PipelineConfig
|
|
|
20
17
|
from ..config import load_pipeline_config
|
|
21
18
|
from ..database import load_table_spec
|
|
22
19
|
from ..database import render_tables
|
|
23
|
-
from ..enums import FileFormat
|
|
24
20
|
from ..extract import extract
|
|
25
|
-
from ..file import File
|
|
26
21
|
from ..load import load
|
|
27
22
|
from ..run import run
|
|
28
23
|
from ..transform import transform
|
|
29
24
|
from ..types import JSONData
|
|
30
25
|
from ..types import TemplateKey
|
|
31
|
-
from ..utils import json_type
|
|
32
|
-
from ..utils import print_json
|
|
33
26
|
from ..validate import validate
|
|
27
|
+
from . import io as cli_io
|
|
34
28
|
|
|
35
29
|
# SECTION: EXPORTS ========================================================== #
|
|
36
30
|
|
|
@@ -81,73 +75,6 @@ def _collect_table_specs(
|
|
|
81
75
|
return specs
|
|
82
76
|
|
|
83
77
|
|
|
84
|
-
def _emit_json(
|
|
85
|
-
data: Any,
|
|
86
|
-
*,
|
|
87
|
-
pretty: bool,
|
|
88
|
-
) -> None:
|
|
89
|
-
"""
|
|
90
|
-
Emit JSON to stdout honoring the pretty/compact preference.
|
|
91
|
-
|
|
92
|
-
Parameters
|
|
93
|
-
----------
|
|
94
|
-
data : Any
|
|
95
|
-
Arbitrary JSON-serializable payload.
|
|
96
|
-
pretty : bool
|
|
97
|
-
When ``True`` pretty-print via :func:`print_json`; otherwise emit a
|
|
98
|
-
compact JSON string.
|
|
99
|
-
"""
|
|
100
|
-
if pretty:
|
|
101
|
-
print_json(data)
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
dumped = json.dumps(
|
|
105
|
-
data,
|
|
106
|
-
ensure_ascii=False,
|
|
107
|
-
separators=(',', ':'),
|
|
108
|
-
)
|
|
109
|
-
print(dumped)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _explicit_cli_format(
|
|
113
|
-
args: argparse.Namespace,
|
|
114
|
-
) -> str | None:
|
|
115
|
-
"""Return the explicit CLI format hint when provided."""
|
|
116
|
-
|
|
117
|
-
if not getattr(args, '_format_explicit', False):
|
|
118
|
-
return None
|
|
119
|
-
for attr in ('format', 'target_format', 'source_format'):
|
|
120
|
-
value = getattr(args, attr, None)
|
|
121
|
-
if value is None:
|
|
122
|
-
continue
|
|
123
|
-
normalized = value.strip().lower()
|
|
124
|
-
if normalized:
|
|
125
|
-
return normalized
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _infer_payload_format(
|
|
130
|
-
text: str,
|
|
131
|
-
) -> str:
|
|
132
|
-
"""
|
|
133
|
-
Infer JSON vs CSV from payload text.
|
|
134
|
-
|
|
135
|
-
Parameters
|
|
136
|
-
----------
|
|
137
|
-
text : str
|
|
138
|
-
Incoming payload as plain text.
|
|
139
|
-
|
|
140
|
-
Returns
|
|
141
|
-
-------
|
|
142
|
-
str
|
|
143
|
-
``'json'`` when the text starts with ``{``/``[``, else ``'csv'``.
|
|
144
|
-
"""
|
|
145
|
-
stripped = text.lstrip()
|
|
146
|
-
if stripped.startswith('{') or stripped.startswith('['):
|
|
147
|
-
return 'json'
|
|
148
|
-
return 'csv'
|
|
149
|
-
|
|
150
|
-
|
|
151
78
|
def _check_sections(
|
|
152
79
|
cfg: PipelineConfig,
|
|
153
80
|
args: argparse.Namespace,
|
|
@@ -185,88 +112,6 @@ def _check_sections(
|
|
|
185
112
|
return sections
|
|
186
113
|
|
|
187
114
|
|
|
188
|
-
def _materialize_file_payload(
|
|
189
|
-
source: object,
|
|
190
|
-
*,
|
|
191
|
-
format_hint: str | None,
|
|
192
|
-
format_explicit: bool,
|
|
193
|
-
) -> JSONData | object:
|
|
194
|
-
"""
|
|
195
|
-
Return structured payloads when ``source`` references a file.
|
|
196
|
-
|
|
197
|
-
Parameters
|
|
198
|
-
----------
|
|
199
|
-
source : object
|
|
200
|
-
Input source of data, possibly a file path.
|
|
201
|
-
format_hint : str | None
|
|
202
|
-
Explicit format hint: 'json', 'csv', or None to infer.
|
|
203
|
-
format_explicit : bool
|
|
204
|
-
Whether an explicit format hint was provided.
|
|
205
|
-
|
|
206
|
-
Returns
|
|
207
|
-
-------
|
|
208
|
-
JSONData | object
|
|
209
|
-
Parsed JSON data when ``source`` is a file; otherwise the original
|
|
210
|
-
``source`` object.
|
|
211
|
-
"""
|
|
212
|
-
if isinstance(source, (dict, list)):
|
|
213
|
-
return cast(JSONData, source)
|
|
214
|
-
if not isinstance(source, (str, os.PathLike)):
|
|
215
|
-
return source
|
|
216
|
-
|
|
217
|
-
path = Path(source)
|
|
218
|
-
|
|
219
|
-
normalized_hint = (format_hint or '').strip().lower()
|
|
220
|
-
fmt: FileFormat | None = None
|
|
221
|
-
|
|
222
|
-
if format_explicit and normalized_hint:
|
|
223
|
-
try:
|
|
224
|
-
fmt = FileFormat(normalized_hint)
|
|
225
|
-
except ValueError:
|
|
226
|
-
fmt = None
|
|
227
|
-
elif not format_explicit:
|
|
228
|
-
suffix = path.suffix.lower().lstrip('.')
|
|
229
|
-
if suffix:
|
|
230
|
-
try:
|
|
231
|
-
fmt = FileFormat(suffix)
|
|
232
|
-
except ValueError:
|
|
233
|
-
fmt = None
|
|
234
|
-
|
|
235
|
-
if fmt is None:
|
|
236
|
-
return source
|
|
237
|
-
if fmt == FileFormat.CSV:
|
|
238
|
-
return _read_csv_rows(path)
|
|
239
|
-
return File(path, fmt).read()
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _parse_text_payload(
|
|
243
|
-
text: str,
|
|
244
|
-
fmt: str | None,
|
|
245
|
-
) -> JSONData | str:
|
|
246
|
-
"""
|
|
247
|
-
Parse JSON/CSV text into a Python payload.
|
|
248
|
-
|
|
249
|
-
Parameters
|
|
250
|
-
----------
|
|
251
|
-
text : str
|
|
252
|
-
The input text payload.
|
|
253
|
-
fmt : str | None
|
|
254
|
-
Explicit format hint: 'json', 'csv', or None to infer.
|
|
255
|
-
|
|
256
|
-
Returns
|
|
257
|
-
-------
|
|
258
|
-
JSONData | str
|
|
259
|
-
The parsed payload as JSON data or raw text.
|
|
260
|
-
"""
|
|
261
|
-
effective = (fmt or '').strip().lower() or _infer_payload_format(text)
|
|
262
|
-
if effective == 'json':
|
|
263
|
-
return cast(JSONData, json_type(text))
|
|
264
|
-
if effective == 'csv':
|
|
265
|
-
reader = csv.DictReader(io.StringIO(text))
|
|
266
|
-
return [dict(row) for row in reader]
|
|
267
|
-
return text
|
|
268
|
-
|
|
269
|
-
|
|
270
115
|
def _pipeline_summary(
|
|
271
116
|
cfg: PipelineConfig,
|
|
272
117
|
) -> dict[str, Any]:
|
|
@@ -295,131 +140,6 @@ def _pipeline_summary(
|
|
|
295
140
|
}
|
|
296
141
|
|
|
297
142
|
|
|
298
|
-
def _presentation_flags(
|
|
299
|
-
args: argparse.Namespace,
|
|
300
|
-
) -> tuple[bool, bool]:
|
|
301
|
-
"""
|
|
302
|
-
Return presentation toggles from the parsed namespace.
|
|
303
|
-
|
|
304
|
-
Parameters
|
|
305
|
-
----------
|
|
306
|
-
args : argparse.Namespace
|
|
307
|
-
Namespace produced by the CLI parser.
|
|
308
|
-
|
|
309
|
-
Returns
|
|
310
|
-
-------
|
|
311
|
-
tuple[bool, bool]
|
|
312
|
-
Pair of ``(pretty, quiet)`` flags with safe defaults.
|
|
313
|
-
"""
|
|
314
|
-
return getattr(args, 'pretty', True), getattr(args, 'quiet', False)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _read_csv_rows(
|
|
318
|
-
path: Path,
|
|
319
|
-
) -> list[dict[str, str]]:
|
|
320
|
-
"""
|
|
321
|
-
Read CSV rows into dictionaries.
|
|
322
|
-
|
|
323
|
-
Parameters
|
|
324
|
-
----------
|
|
325
|
-
path : Path
|
|
326
|
-
Path to a CSV file.
|
|
327
|
-
|
|
328
|
-
Returns
|
|
329
|
-
-------
|
|
330
|
-
list[dict[str, str]]
|
|
331
|
-
List of dictionaries, each representing a row in the CSV file.
|
|
332
|
-
"""
|
|
333
|
-
with path.open(newline='', encoding='utf-8') as handle:
|
|
334
|
-
reader = csv.DictReader(handle)
|
|
335
|
-
return [dict(row) for row in reader]
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def _read_stdin_text() -> str:
|
|
339
|
-
"""
|
|
340
|
-
Return every character from ``stdin`` as a single string.
|
|
341
|
-
|
|
342
|
-
Returns
|
|
343
|
-
-------
|
|
344
|
-
str
|
|
345
|
-
Entire ``stdin`` contents.
|
|
346
|
-
"""
|
|
347
|
-
return sys.stdin.read()
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def _resolve_cli_payload(
|
|
351
|
-
source: object,
|
|
352
|
-
*,
|
|
353
|
-
format_hint: str | None,
|
|
354
|
-
format_explicit: bool,
|
|
355
|
-
hydrate_files: bool = True,
|
|
356
|
-
) -> JSONData | object:
|
|
357
|
-
"""
|
|
358
|
-
Normalize CLI-provided payloads, honoring stdin and inline data.
|
|
359
|
-
|
|
360
|
-
Parameters
|
|
361
|
-
----------
|
|
362
|
-
source : object
|
|
363
|
-
Raw CLI value (path, inline payload, or ``'-'`` for stdin).
|
|
364
|
-
format_hint : str | None
|
|
365
|
-
Explicit format hint supplied by the CLI option.
|
|
366
|
-
format_explicit : bool
|
|
367
|
-
Flag indicating whether the format hint was explicitly provided.
|
|
368
|
-
hydrate_files : bool, optional
|
|
369
|
-
When ``True`` (default) materialize file paths into structured data.
|
|
370
|
-
When ``False``, keep the original path so downstream code can stream
|
|
371
|
-
from disk directly.
|
|
372
|
-
|
|
373
|
-
Returns
|
|
374
|
-
-------
|
|
375
|
-
JSONData | object
|
|
376
|
-
Parsed payload or the original source value when hydration is
|
|
377
|
-
disabled.
|
|
378
|
-
"""
|
|
379
|
-
if isinstance(source, (os.PathLike, str)) and str(source) == '-':
|
|
380
|
-
text = _read_stdin_text()
|
|
381
|
-
return _parse_text_payload(text, format_hint)
|
|
382
|
-
|
|
383
|
-
if not hydrate_files:
|
|
384
|
-
return source
|
|
385
|
-
|
|
386
|
-
return _materialize_file_payload(
|
|
387
|
-
source,
|
|
388
|
-
format_hint=format_hint,
|
|
389
|
-
format_explicit=format_explicit,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def _write_json_output(
|
|
394
|
-
data: Any,
|
|
395
|
-
output_path: str | None,
|
|
396
|
-
*,
|
|
397
|
-
success_message: str,
|
|
398
|
-
) -> bool:
|
|
399
|
-
"""
|
|
400
|
-
Optionally persist JSON data to disk.
|
|
401
|
-
|
|
402
|
-
Parameters
|
|
403
|
-
----------
|
|
404
|
-
data : Any
|
|
405
|
-
Data to write.
|
|
406
|
-
output_path : str | None
|
|
407
|
-
Path to write the output to. None to print to stdout.
|
|
408
|
-
success_message : str
|
|
409
|
-
Message to print upon successful write.
|
|
410
|
-
|
|
411
|
-
Returns
|
|
412
|
-
-------
|
|
413
|
-
bool
|
|
414
|
-
True if output was written to a file, False if printed to stdout.
|
|
415
|
-
"""
|
|
416
|
-
if not output_path or output_path == '-':
|
|
417
|
-
return False
|
|
418
|
-
File(Path(output_path), FileFormat.JSON).write_json(data)
|
|
419
|
-
print(f'{success_message} {output_path}')
|
|
420
|
-
return True
|
|
421
|
-
|
|
422
|
-
|
|
423
143
|
# SECTION: FUNCTIONS ======================================================== #
|
|
424
144
|
|
|
425
145
|
|
|
@@ -441,10 +161,10 @@ def check_handler(
|
|
|
441
161
|
"""
|
|
442
162
|
cfg = load_pipeline_config(args.config, substitute=True)
|
|
443
163
|
if getattr(args, 'summary', False):
|
|
444
|
-
|
|
164
|
+
cli_io.emit_json(_pipeline_summary(cfg), pretty=True)
|
|
445
165
|
return 0
|
|
446
166
|
|
|
447
|
-
|
|
167
|
+
cli_io.emit_json(_check_sections(cfg, args), pretty=True)
|
|
448
168
|
return 0
|
|
449
169
|
|
|
450
170
|
|
|
@@ -464,13 +184,16 @@ def extract_handler(
|
|
|
464
184
|
int
|
|
465
185
|
Zero on success.
|
|
466
186
|
"""
|
|
467
|
-
pretty, _ =
|
|
468
|
-
explicit_format =
|
|
187
|
+
pretty, _ = cli_io.presentation_flags(args)
|
|
188
|
+
explicit_format = cli_io.explicit_cli_format(args)
|
|
469
189
|
|
|
470
190
|
if args.source == '-':
|
|
471
|
-
text =
|
|
472
|
-
payload =
|
|
473
|
-
|
|
191
|
+
text = cli_io.read_stdin_text()
|
|
192
|
+
payload = cli_io.parse_text_payload(
|
|
193
|
+
text,
|
|
194
|
+
getattr(args, 'format', None),
|
|
195
|
+
)
|
|
196
|
+
cli_io.emit_json(payload, pretty=pretty)
|
|
474
197
|
|
|
475
198
|
return 0
|
|
476
199
|
|
|
@@ -483,12 +206,12 @@ def extract_handler(
|
|
|
483
206
|
if output_path is None:
|
|
484
207
|
output_path = getattr(args, 'output', None)
|
|
485
208
|
|
|
486
|
-
|
|
209
|
+
cli_io.emit_or_write(
|
|
487
210
|
result,
|
|
488
211
|
output_path,
|
|
212
|
+
pretty=pretty,
|
|
489
213
|
success_message='Data extracted and saved to',
|
|
490
|
-
)
|
|
491
|
-
_emit_json(result, pretty=pretty)
|
|
214
|
+
)
|
|
492
215
|
|
|
493
216
|
return 0
|
|
494
217
|
|
|
@@ -509,14 +232,14 @@ def load_handler(
|
|
|
509
232
|
int
|
|
510
233
|
Zero on success.
|
|
511
234
|
"""
|
|
512
|
-
pretty, _ =
|
|
513
|
-
explicit_format =
|
|
235
|
+
pretty, _ = cli_io.presentation_flags(args)
|
|
236
|
+
explicit_format = cli_io.explicit_cli_format(args)
|
|
514
237
|
|
|
515
238
|
# Allow piping into load.
|
|
516
239
|
source_format = getattr(args, 'source_format', None)
|
|
517
240
|
source_value = cast(
|
|
518
241
|
str | Path | os.PathLike[str] | dict[str, Any] | list[dict[str, Any]],
|
|
519
|
-
|
|
242
|
+
cli_io.resolve_cli_payload(
|
|
520
243
|
args.source,
|
|
521
244
|
format_hint=source_format,
|
|
522
245
|
format_explicit=source_format is not None,
|
|
@@ -526,12 +249,12 @@ def load_handler(
|
|
|
526
249
|
|
|
527
250
|
# Allow piping out of load for file targets.
|
|
528
251
|
if args.target_type == 'file' and args.target == '-':
|
|
529
|
-
payload =
|
|
252
|
+
payload = cli_io.materialize_file_payload(
|
|
530
253
|
source_value,
|
|
531
254
|
format_hint=source_format,
|
|
532
255
|
format_explicit=source_format is not None,
|
|
533
256
|
)
|
|
534
|
-
|
|
257
|
+
cli_io.emit_json(payload, pretty=pretty)
|
|
535
258
|
return 0
|
|
536
259
|
|
|
537
260
|
result = load(
|
|
@@ -542,12 +265,12 @@ def load_handler(
|
|
|
542
265
|
)
|
|
543
266
|
|
|
544
267
|
output_path = getattr(args, 'output', None)
|
|
545
|
-
|
|
268
|
+
cli_io.emit_or_write(
|
|
546
269
|
result,
|
|
547
270
|
output_path,
|
|
271
|
+
pretty=pretty,
|
|
548
272
|
success_message='Load result saved to',
|
|
549
|
-
)
|
|
550
|
-
_emit_json(result, pretty=pretty)
|
|
273
|
+
)
|
|
551
274
|
|
|
552
275
|
return 0
|
|
553
276
|
|
|
@@ -556,7 +279,7 @@ def render_handler(
|
|
|
556
279
|
args: argparse.Namespace,
|
|
557
280
|
) -> int:
|
|
558
281
|
"""Render SQL DDL statements from table schema specs."""
|
|
559
|
-
_, quiet =
|
|
282
|
+
_, quiet = cli_io.presentation_flags(args)
|
|
560
283
|
|
|
561
284
|
template_value: TemplateKey = getattr(args, 'template', 'ddl') or 'ddl'
|
|
562
285
|
template_path = getattr(args, 'template_path', None)
|
|
@@ -633,10 +356,10 @@ def run_handler(
|
|
|
633
356
|
job_name = getattr(args, 'job', None) or getattr(args, 'pipeline', None)
|
|
634
357
|
if job_name:
|
|
635
358
|
result = run(job=job_name, config_path=args.config)
|
|
636
|
-
|
|
359
|
+
cli_io.emit_json({'status': 'ok', 'result': result}, pretty=True)
|
|
637
360
|
return 0
|
|
638
361
|
|
|
639
|
-
|
|
362
|
+
cli_io.emit_json(_pipeline_summary(cfg), pretty=True)
|
|
640
363
|
return 0
|
|
641
364
|
|
|
642
365
|
|
|
@@ -656,13 +379,13 @@ def transform_handler(
|
|
|
656
379
|
int
|
|
657
380
|
Zero on success.
|
|
658
381
|
"""
|
|
659
|
-
pretty, _ =
|
|
382
|
+
pretty, _ = cli_io.presentation_flags(args)
|
|
660
383
|
format_hint: str | None = getattr(args, 'source_format', None)
|
|
661
384
|
format_explicit: bool = format_hint is not None
|
|
662
385
|
|
|
663
386
|
payload = cast(
|
|
664
387
|
JSONData | str,
|
|
665
|
-
|
|
388
|
+
cli_io.resolve_cli_payload(
|
|
666
389
|
args.source,
|
|
667
390
|
format_hint=format_hint,
|
|
668
391
|
format_explicit=format_explicit,
|
|
@@ -671,12 +394,12 @@ def transform_handler(
|
|
|
671
394
|
|
|
672
395
|
data = transform(payload, args.operations)
|
|
673
396
|
|
|
674
|
-
|
|
397
|
+
cli_io.emit_or_write(
|
|
675
398
|
data,
|
|
676
399
|
getattr(args, 'target', None),
|
|
400
|
+
pretty=pretty,
|
|
677
401
|
success_message='Data transformed and saved to',
|
|
678
|
-
)
|
|
679
|
-
_emit_json(data, pretty=pretty)
|
|
402
|
+
)
|
|
680
403
|
|
|
681
404
|
return 0
|
|
682
405
|
|
|
@@ -697,12 +420,12 @@ def validate_handler(
|
|
|
697
420
|
int
|
|
698
421
|
Zero on success.
|
|
699
422
|
"""
|
|
700
|
-
pretty, _ =
|
|
423
|
+
pretty, _ = cli_io.presentation_flags(args)
|
|
701
424
|
format_explicit: bool = getattr(args, '_format_explicit', False)
|
|
702
425
|
format_hint: str | None = getattr(args, 'source_format', None)
|
|
703
426
|
payload = cast(
|
|
704
427
|
JSONData | str,
|
|
705
|
-
|
|
428
|
+
cli_io.resolve_cli_payload(
|
|
706
429
|
args.source,
|
|
707
430
|
format_hint=format_hint,
|
|
708
431
|
format_explicit=format_explicit,
|
|
@@ -714,7 +437,7 @@ def validate_handler(
|
|
|
714
437
|
if target_path:
|
|
715
438
|
validated_data = result.get('data')
|
|
716
439
|
if validated_data is not None:
|
|
717
|
-
|
|
440
|
+
cli_io.write_json_output(
|
|
718
441
|
validated_data,
|
|
719
442
|
target_path,
|
|
720
443
|
success_message='Validation result saved to',
|
|
@@ -725,6 +448,6 @@ def validate_handler(
|
|
|
725
448
|
file=sys.stderr,
|
|
726
449
|
)
|
|
727
450
|
else:
|
|
728
|
-
|
|
451
|
+
cli_io.emit_json(result, pretty=pretty)
|
|
729
452
|
|
|
730
453
|
return 0
|