etlplus 0.8.2__py3-none-any.whl → 0.8.4__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 CHANGED
@@ -29,27 +29,28 @@ Notes
29
29
  from __future__ import annotations
30
30
 
31
31
  from typing import Annotated
32
+ from typing import Any
33
+ from typing import Literal
34
+ from typing import cast
32
35
 
33
36
  import typer
34
37
 
35
38
  from .. import __version__
36
- from ..utils import json_type
39
+ from ..enums import FileFormat
37
40
  from . import handlers
38
41
  from .constants import CLI_DESCRIPTION
39
42
  from .constants import CLI_EPILOG
40
43
  from .constants import DATA_CONNECTORS
41
- from .constants import DEFAULT_FILE_FORMAT
42
44
  from .constants import FILE_FORMATS
45
+ from .io import parse_json_payload
43
46
  from .options import typer_format_option_kwargs
44
47
  from .state import CliState
45
48
  from .state import ensure_state
46
- from .state import format_namespace_kwargs
47
49
  from .state import infer_resource_type_or_exit
48
50
  from .state import infer_resource_type_soft
49
51
  from .state import log_inferred_resource
50
52
  from .state import optional_choice
51
53
  from .state import resolve_resource_type
52
- from .state import stateful_namespace
53
54
  from .state import validate_choice
54
55
 
55
56
  # SECTION: EXPORTS ========================================================== #
@@ -60,7 +61,6 @@ __all__ = ['app']
60
61
 
61
62
  # SECTION: TYPE ALIASES ==================================================== #
62
63
 
63
-
64
64
  OperationsOption = Annotated[
65
65
  str,
66
66
  typer.Option(
@@ -119,12 +119,12 @@ RenderTableOption = Annotated[
119
119
  ]
120
120
 
121
121
  RenderTemplateOption = Annotated[
122
- str,
122
+ Literal['ddl', 'view'] | None,
123
123
  typer.Option(
124
124
  '--template',
125
125
  '-t',
126
- metavar='KEY|PATH',
127
- help='Template key (ddl/view) or path to a Jinja template file.',
126
+ metavar='KEY',
127
+ help='Template key (ddl/view).',
128
128
  show_default=True,
129
129
  ),
130
130
  ]
@@ -149,7 +149,7 @@ RulesOption = Annotated[
149
149
  ]
150
150
 
151
151
  SourceFormatOption = Annotated[
152
- str | None,
152
+ FileFormat | None,
153
153
  typer.Option(
154
154
  '--source-format',
155
155
  **typer_format_option_kwargs(context='source'),
@@ -180,7 +180,7 @@ SourceOverrideOption = Annotated[
180
180
  ]
181
181
 
182
182
  StdinFormatOption = Annotated[
183
- str | None,
183
+ FileFormat | None,
184
184
  typer.Option(
185
185
  '--source-format',
186
186
  **typer_format_option_kwargs(context='source'),
@@ -200,7 +200,7 @@ StreamingSourceArg = Annotated[
200
200
  ]
201
201
 
202
202
  TargetFormatOption = Annotated[
203
- str | None,
203
+ FileFormat | None,
204
204
  typer.Option(
205
205
  '--target-format',
206
206
  **typer_format_option_kwargs(context='target'),
@@ -241,6 +241,41 @@ TargetPathOption = Annotated[
241
241
  ]
242
242
 
243
243
 
244
+ # SECTION: INTERNAL FUNCTIONS =============================================== #
245
+
246
+
247
+ def _parse_json_option(
248
+ value: str,
249
+ flag: str,
250
+ ) -> Any:
251
+ """
252
+ Parse JSON option values and surface a helpful CLI error.
253
+
254
+ Parameters
255
+ ----------
256
+ value : str
257
+ The JSON string to parse.
258
+ flag : str
259
+ The CLI flag name for error messages.
260
+
261
+ Returns
262
+ -------
263
+ Any
264
+ The parsed JSON value.
265
+
266
+ Raises
267
+ ------
268
+ typer.BadParameter
269
+ When the JSON is invalid.
270
+ """
271
+ try:
272
+ return parse_json_payload(value)
273
+ except ValueError as e:
274
+ raise typer.BadParameter(
275
+ f'Invalid JSON for {flag}: {e}',
276
+ ) from e
277
+
278
+
244
279
  # SECTION: TYPER APP ======================================================== #
245
280
 
246
281
 
@@ -284,6 +319,25 @@ def _root(
284
319
  ) -> None:
285
320
  """
286
321
  Seed the Typer context with runtime flags and handle root-only options.
322
+
323
+ Parameters
324
+ ----------
325
+ ctx : typer.Context
326
+ The Typer command context.
327
+ version : bool, optional
328
+ Show the version and exit. Default is ``False``.
329
+ pretty : bool, optional
330
+ Whether to pretty-print JSON output. Default is ``True``.
331
+ quiet : bool, optional
332
+ Whether to suppress warnings and non-essential output. Default is
333
+ ``False``.
334
+ verbose : bool, optional
335
+ Whether to emit extra diagnostics to stderr. Default is ``False``.
336
+
337
+ Raises
338
+ ------
339
+ typer.Exit
340
+ When ``--version`` is provided or no subcommand is invoked.
287
341
  """
288
342
  ctx.obj = CliState(pretty=pretty, quiet=quiet, verbose=verbose)
289
343
 
@@ -331,20 +385,47 @@ def check_cmd(
331
385
  help='List data transforms',
332
386
  ),
333
387
  ) -> int:
334
- """Inspect a pipeline configuration."""
388
+ """
389
+ Inspect a pipeline configuration.
390
+
391
+ Parameters
392
+ ----------
393
+ ctx : typer.Context
394
+ The Typer context.
395
+ config : PipelineConfigOption
396
+ Path to pipeline YAML configuration file.
397
+ jobs : bool, optional
398
+ List available job names and exit. Default is ``False``.
399
+ pipelines : bool, optional
400
+ List ETL pipelines. Default is ``False``.
401
+ sources : bool, optional
402
+ List data sources. Default is ``False``.
403
+ summary : bool, optional
404
+ Show pipeline summary (name, version, sources, targets, jobs). Default
405
+ is ``False``.
406
+ targets : bool, optional
407
+ List data targets. Default is ``False``.
408
+ transforms : bool, optional
409
+ List data transforms. Default is ``False``.
410
+
411
+ Returns
412
+ -------
413
+ int
414
+ Exit code.
415
+ """
335
416
  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,
417
+ return int(
418
+ handlers.check_handler(
419
+ config=config,
420
+ jobs=jobs,
421
+ pipelines=pipelines,
422
+ sources=sources,
423
+ summary=summary,
424
+ targets=targets,
425
+ transforms=transforms,
426
+ pretty=state.pretty,
427
+ ),
346
428
  )
347
- return int(handlers.check_handler(ns))
348
429
 
349
430
 
350
431
  @app.command('extract')
@@ -354,7 +435,28 @@ def extract_cmd(
354
435
  source_format: SourceFormatOption | None = None,
355
436
  source_type: SourceOverrideOption | None = None,
356
437
  ) -> int:
357
- """Extract data from files, databases, or REST APIs."""
438
+ """
439
+ Extract data from files, databases, or REST APIs.
440
+
441
+ Parameters
442
+ ----------
443
+ ctx : typer.Context
444
+ The Typer context.
445
+ source : SourceInputArg
446
+ Extract from SOURCE. Use --from/--source-type to override the inferred
447
+ connector when needed.
448
+ source_format : SourceFormatOption | None, optional
449
+ Format of the source. Overrides filename-based inference when provided.
450
+ Default is ``None``.
451
+ source_type : SourceOverrideOption | None, optional
452
+ Override the inferred source type (file, database, api). Default is
453
+ ``None``.
454
+
455
+ Returns
456
+ -------
457
+ int
458
+ Exit code.
459
+ """
358
460
  state = ensure_state(ctx)
359
461
 
360
462
  source_type = optional_choice(
@@ -362,10 +464,13 @@ def extract_cmd(
362
464
  DATA_CONNECTORS,
363
465
  label='source_type',
364
466
  )
365
- source_format = optional_choice(
366
- source_format,
367
- FILE_FORMATS,
368
- label='source_format',
467
+ source_format = cast(
468
+ SourceFormatOption,
469
+ optional_choice(
470
+ source_format,
471
+ FILE_FORMATS,
472
+ label='source_format',
473
+ ),
369
474
  )
370
475
 
371
476
  resolved_source = source
@@ -380,45 +485,73 @@ def extract_cmd(
380
485
  resource_type=resolved_source_type,
381
486
  )
382
487
 
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,
488
+ return int(
489
+ handlers.extract_handler(
490
+ source_type=resolved_source_type,
491
+ source=resolved_source,
492
+ format_hint=source_format,
493
+ format_explicit=source_format is not None,
494
+ pretty=state.pretty,
495
+ ),
393
496
  )
394
- return int(handlers.extract_handler(ns))
395
497
 
396
498
 
397
499
  @app.command('load')
398
500
  def load_cmd(
399
501
  ctx: typer.Context,
400
502
  target: TargetInputArg,
401
- source_format: StdinFormatOption | None = None,
402
- target_format: TargetFormatOption | None = None,
403
- target_type: TargetOverrideOption | None = None,
503
+ source_format: StdinFormatOption = None,
504
+ target_format: TargetFormatOption = None,
505
+ target_type: TargetOverrideOption = None,
404
506
  ) -> int:
405
- """Load data into a file, database, or REST API."""
507
+ """
508
+ Load data into a file, database, or REST API.
509
+
510
+ Parameters
511
+ ----------
512
+ ctx : typer.Context
513
+ The Typer context.
514
+ target : TargetInputArg
515
+ Load JSON data from stdin into TARGET. Use --to/--target-type to
516
+ override connector inference when needed. Source data must be piped
517
+ into stdin.
518
+ source_format : StdinFormatOption, optional
519
+ Format of the source. Overrides filename-based inference when provided.
520
+ Default is ``None``.
521
+ target_format : TargetFormatOption, optional
522
+ Format of the target. Overrides filename-based inference when provided.
523
+ Default is ``None``.
524
+ target_type : TargetOverrideOption, optional
525
+ Override the inferred target type (file, database, api). Default is
526
+ ``None``.
527
+
528
+ Returns
529
+ -------
530
+ int
531
+ Exit code.
532
+ """
406
533
  state = ensure_state(ctx)
407
534
 
408
- source_format = optional_choice(
409
- source_format,
410
- FILE_FORMATS,
411
- label='source_format',
535
+ source_format = cast(
536
+ StdinFormatOption,
537
+ optional_choice(
538
+ source_format,
539
+ FILE_FORMATS,
540
+ label='source_format',
541
+ ),
412
542
  )
413
543
  target_type = optional_choice(
414
544
  target_type,
415
545
  DATA_CONNECTORS,
416
546
  label='target_type',
417
547
  )
418
- target_format = optional_choice(
419
- target_format,
420
- FILE_FORMATS,
421
- label='target_format',
548
+ target_format = cast(
549
+ TargetFormatOption,
550
+ optional_choice(
551
+ target_format,
552
+ FILE_FORMATS,
553
+ label='target_format',
554
+ ),
422
555
  )
423
556
 
424
557
  resolved_target = target
@@ -442,20 +575,18 @@ def load_cmd(
442
575
  resource_type=resolved_target_type,
443
576
  )
444
577
 
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,
578
+ return int(
579
+ handlers.load_handler(
580
+ source=resolved_source_value,
581
+ target_type=resolved_target_type,
582
+ target=resolved_target,
583
+ source_format=source_format,
584
+ target_format=target_format,
585
+ format_explicit=target_format is not None,
586
+ output=None,
587
+ pretty=state.pretty,
588
+ ),
457
589
  )
458
- return int(handlers.load_handler(ns))
459
590
 
460
591
 
461
592
  @app.command('render')
@@ -468,19 +599,44 @@ def render_cmd(
468
599
  template_path: RenderTemplatePathOption = None,
469
600
  output: RenderOutputOption = None,
470
601
  ) -> int:
471
- """Render SQL DDL from table schemas defined in YAML/JSON configs."""
602
+ """
603
+ Render SQL DDL from table schemas defined in YAML/JSON configs.
604
+
605
+ Parameters
606
+ ----------
607
+ ctx : typer.Context
608
+ The Typer context.
609
+ config : RenderConfigOption
610
+ Pipeline YAML that includes table_schemas for rendering.
611
+ spec : RenderSpecOption, optional
612
+ Standalone table spec file (.yml/.yaml/.json).
613
+ table : RenderTableOption, optional
614
+ Filter to a single table name from table_schemas.
615
+ template : RenderTemplateOption
616
+ Template key (ddl/view) or path to a Jinja template file.
617
+ template_path : RenderTemplatePathOption, optional
618
+ Explicit path to a Jinja template file (overrides template key).
619
+ output : RenderOutputOption, optional
620
+ Write rendered SQL to PATH (default: stdout).
621
+
622
+ Returns
623
+ -------
624
+ int
625
+ Exit code.
626
+ """
472
627
  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,
628
+ return int(
629
+ handlers.render_handler(
630
+ config=config,
631
+ spec=spec,
632
+ table=table,
633
+ template=template,
634
+ template_path=template_path,
635
+ output=output,
636
+ pretty=state.pretty,
637
+ quiet=state.quiet,
638
+ ),
482
639
  )
483
- return int(handlers.render_handler(ns))
484
640
 
485
641
 
486
642
  @app.command('run')
@@ -500,16 +656,34 @@ def run_cmd(
500
656
  help='Name of the pipeline to run',
501
657
  ),
502
658
  ) -> int:
503
- """Execute an ETL job or pipeline from a YAML configuration."""
659
+ """
660
+ Execute an ETL job or pipeline from a YAML configuration.
661
+
662
+ Parameters
663
+ ----------
664
+ ctx : typer.Context
665
+ The Typer context.
666
+ config : PipelineConfigOption
667
+ Path to pipeline YAML configuration file.
668
+ job : str | None, optional
669
+ Name of the job to run. Default is ``None``.
670
+ pipeline : str | None, optional
671
+ Name of the pipeline to run. Default is ``None``.
672
+
673
+ Returns
674
+ -------
675
+ int
676
+ Exit code.
677
+ """
504
678
  state = ensure_state(ctx)
505
- ns = stateful_namespace(
506
- state,
507
- command='run',
508
- config=config,
509
- job=job,
510
- pipeline=pipeline,
679
+ return int(
680
+ handlers.run_handler(
681
+ config=config,
682
+ job=job,
683
+ pipeline=pipeline,
684
+ pretty=state.pretty,
685
+ ),
511
686
  )
512
- return int(handlers.run_handler(ns))
513
687
 
514
688
 
515
689
  @app.command('transform')
@@ -517,33 +691,65 @@ def transform_cmd(
517
691
  ctx: typer.Context,
518
692
  operations: OperationsOption = '{}',
519
693
  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,
694
+ source_format: SourceFormatOption = None,
695
+ source_type: SourceOverrideOption = None,
696
+ target: TargetPathOption = None,
697
+ target_format: TargetFormatOption = None,
698
+ target_type: TargetOverrideOption = None,
525
699
  ) -> int:
526
- """Transform records using JSON-described operations."""
700
+ """
701
+ Transform records using JSON-described operations.
702
+
703
+ Parameters
704
+ ----------
705
+ ctx : typer.Context
706
+ The Typer context.
707
+ operations : OperationsOption
708
+ Transformation operations as JSON string.
709
+ source : StreamingSourceArg
710
+ Data source to transform (path, JSON payload, or - for stdin).
711
+ source_format : SourceFormatOption, optional
712
+ Format of the source. Overrides filename-based inference when provided.
713
+ Default is ``None``.
714
+ source_type : SourceOverrideOption, optional
715
+ Override the inferred source type (file, database, api). Default is
716
+ ``None``.
717
+ target : TargetPathOption, optional
718
+ Target file for transformed output (- for stdout). Default is ``None``.
719
+ target_format : TargetFormatOption, optional
720
+ Format of the target. Overrides filename-based inference when provided.
721
+ Default is ``None``.
722
+ target_type : TargetOverrideOption, optional
723
+ Override the inferred target type (file, database, api). Default is
724
+ ``None``.
725
+
726
+ Returns
727
+ -------
728
+ int
729
+ Exit code.
730
+ """
527
731
  state = ensure_state(ctx)
528
732
 
529
- source_format = optional_choice(
530
- source_format,
531
- FILE_FORMATS,
532
- label='source_format',
733
+ source_format = cast(
734
+ SourceFormatOption,
735
+ optional_choice(
736
+ source_format,
737
+ FILE_FORMATS,
738
+ label='source_format',
739
+ ),
533
740
  )
534
741
  source_type = optional_choice(
535
742
  source_type,
536
743
  DATA_CONNECTORS,
537
744
  label='source_type',
538
745
  )
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,
746
+ target_format = cast(
747
+ TargetFormatOption,
748
+ optional_choice(
749
+ target_format,
750
+ FILE_FORMATS,
751
+ label='target_format',
752
+ ),
547
753
  )
548
754
  target_type = optional_choice(
549
755
  target_type,
@@ -582,19 +788,17 @@ def transform_cmd(
582
788
  resource_type=resolved_target_type,
583
789
  )
584
790
 
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,
791
+ return int(
792
+ handlers.transform_handler(
793
+ source=resolved_source_value,
794
+ operations=_parse_json_option(operations, '--operations'),
795
+ target=resolved_target_value,
796
+ source_format=source_format,
797
+ target_format=target_format,
798
+ format_explicit=target_format is not None,
799
+ pretty=state.pretty,
800
+ ),
596
801
  )
597
- return int(handlers.transform_handler(ns))
598
802
 
599
803
 
600
804
  @app.command('validate')
@@ -602,26 +806,48 @@ def validate_cmd(
602
806
  ctx: typer.Context,
603
807
  rules: RulesOption = '{}',
604
808
  source: StreamingSourceArg = '-',
605
- source_format: SourceFormatOption | None = None,
606
- source_type: SourceOverrideOption | None = None,
607
- target: TargetPathOption | None = None,
809
+ source_format: SourceFormatOption = None,
810
+ source_type: SourceOverrideOption = None,
811
+ target: TargetPathOption = None,
608
812
  ) -> int:
609
- """Validate data against JSON-described rules."""
610
- source_format = optional_choice(
611
- source_format,
612
- FILE_FORMATS,
613
- label='source_format',
813
+ """
814
+ Validate data against JSON-described rules.
815
+
816
+ Parameters
817
+ ----------
818
+ ctx : typer.Context
819
+ The Typer context.
820
+ rules : RulesOption
821
+ Validation rules as JSON string.
822
+ source : StreamingSourceArg
823
+ Data source to validate (path, JSON payload, or - for stdin).
824
+ source_format : SourceFormatOption, optional
825
+ Format of the source. Overrides filename-based inference when provided.
826
+ Default is ``None``.
827
+ source_type : SourceOverrideOption, optional
828
+ Override the inferred source type (file, database, api). Default is
829
+ ``None``.
830
+ target : TargetPathOption, optional
831
+ Target file for validated output (- for stdout). Default is ``None``.
832
+
833
+ Returns
834
+ -------
835
+ int
836
+ Exit code.
837
+ """
838
+ source_format = cast(
839
+ SourceFormatOption,
840
+ optional_choice(
841
+ source_format,
842
+ FILE_FORMATS,
843
+ label='source_format',
844
+ ),
614
845
  )
615
846
  source_type = optional_choice(
616
847
  source_type,
617
848
  DATA_CONNECTORS,
618
849
  label='source_type',
619
850
  )
620
- source_format_kwargs = format_namespace_kwargs(
621
- format_value=source_format,
622
- default=DEFAULT_FILE_FORMAT,
623
- )
624
-
625
851
  state = ensure_state(ctx)
626
852
  resolved_source_type = source_type or infer_resource_type_soft(source)
627
853
 
@@ -632,14 +858,13 @@ def validate_cmd(
632
858
  resource_type=resolved_source_type,
633
859
  )
634
860
 
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,
861
+ return int(
862
+ handlers.validate_handler(
863
+ source=source,
864
+ rules=_parse_json_option(rules, '--rules'),
865
+ source_format=source_format,
866
+ target=target,
867
+ format_explicit=source_format is not None,
868
+ pretty=state.pretty,
869
+ ),
644
870
  )
645
- return int(handlers.validate_handler(ns))