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/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
- print_json(_pipeline_summary(cfg))
164
+ cli_io.emit_json(_pipeline_summary(cfg), pretty=True)
445
165
  return 0
446
166
 
447
- print_json(_check_sections(cfg, args))
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, _ = _presentation_flags(args)
468
- explicit_format = _explicit_cli_format(args)
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 = _read_stdin_text()
472
- payload = _parse_text_payload(text, getattr(args, 'format', None))
473
- _emit_json(payload, pretty=pretty)
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
- if not _write_json_output(
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, _ = _presentation_flags(args)
513
- explicit_format = _explicit_cli_format(args)
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
- _resolve_cli_payload(
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 = _materialize_file_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
- _emit_json(payload, pretty=pretty)
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
- if not _write_json_output(
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 = _presentation_flags(args)
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
- print_json({'status': 'ok', 'result': result})
359
+ cli_io.emit_json({'status': 'ok', 'result': result}, pretty=True)
637
360
  return 0
638
361
 
639
- print_json(_pipeline_summary(cfg))
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, _ = _presentation_flags(args)
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
- _resolve_cli_payload(
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
- if not _write_json_output(
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, _ = _presentation_flags(args)
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
- _resolve_cli_payload(
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
- _write_json_output(
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
- _emit_json(result, pretty=pretty)
451
+ cli_io.emit_json(result, pretty=pretty)
729
452
 
730
453
  return 0