etlplus 0.4.6__py3-none-any.whl → 0.7.0__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/main.py CHANGED
@@ -10,10 +10,12 @@ This module exposes :func:`main` for the console script as well as
10
10
  from __future__ import annotations
11
11
 
12
12
  import argparse
13
+ import contextlib
13
14
  import sys
14
15
  from collections.abc import Sequence
15
16
  from typing import Literal
16
17
 
18
+ import click
17
19
  import typer
18
20
 
19
21
  from .. import __version__
@@ -22,13 +24,14 @@ from ..enums import FileFormat
22
24
  from ..utils import json_type
23
25
  from .app import PROJECT_URL
24
26
  from .app import app
25
- from .handlers import cmd_extract
26
- from .handlers import cmd_list
27
- from .handlers import cmd_load
28
- from .handlers import cmd_pipeline
29
- from .handlers import cmd_run
30
- from .handlers import cmd_transform
31
- from .handlers import cmd_validate
27
+ from .handlers import check_handler
28
+ from .handlers import extract_handler
29
+ from .handlers import load_handler
30
+ from .handlers import pipeline_handler
31
+ from .handlers import render_handler
32
+ from .handlers import run_handler
33
+ from .handlers import transform_handler
34
+ from .handlers import validate_handler
32
35
 
33
36
  # SECTION: EXPORTS ========================================================== #
34
37
 
@@ -68,6 +71,32 @@ class _FormatAction(argparse.Action):
68
71
  # SECTION: INTERNAL FUNCTIONS =============================================== #
69
72
 
70
73
 
74
+ def _add_boolean_flag(
75
+ parser: argparse.ArgumentParser,
76
+ *,
77
+ name: str,
78
+ help_text: str,
79
+ ) -> None:
80
+ """Add a toggle that also supports the ``--no-`` prefix via 3.13.
81
+
82
+ Parameters
83
+ ----------
84
+ parser : argparse.ArgumentParser
85
+ Parser receiving the flag.
86
+ name : str
87
+ Primary flag name without leading dashes.
88
+ help_text : str
89
+ Help text rendered in ``--help`` output.
90
+ """
91
+
92
+ parser.add_argument(
93
+ f'--{name}',
94
+ action=argparse.BooleanOptionalAction,
95
+ default=False,
96
+ help=help_text,
97
+ )
98
+
99
+
71
100
  def _add_config_option(
72
101
  parser: argparse.ArgumentParser,
73
102
  *,
@@ -129,32 +158,6 @@ def _add_format_options(
129
158
  )
130
159
 
131
160
 
132
- def _add_boolean_flag(
133
- parser: argparse.ArgumentParser,
134
- *,
135
- name: str,
136
- help_text: str,
137
- ) -> None:
138
- """Add a toggle that also supports the ``--no-`` prefix via 3.13.
139
-
140
- Parameters
141
- ----------
142
- parser : argparse.ArgumentParser
143
- Parser receiving the flag.
144
- name : str
145
- Primary flag name without leading dashes.
146
- help_text : str
147
- Help text rendered in ``--help`` output.
148
- """
149
-
150
- parser.add_argument(
151
- f'--{name}',
152
- action=argparse.BooleanOptionalAction,
153
- default=False,
154
- help=help_text,
155
- )
156
-
157
-
158
161
  def _cli_description() -> str:
159
162
  return '\n'.join(
160
163
  [
@@ -188,6 +191,93 @@ def _cli_epilog() -> str:
188
191
  )
189
192
 
190
193
 
194
+ def _emit_context_help(
195
+ ctx: click.Context | None,
196
+ ) -> bool:
197
+ """
198
+ Mirror Click help output for the provided context onto stderr.
199
+
200
+ Parameters
201
+ ----------
202
+ ctx : click.Context | None
203
+ The Click context to emit help for.
204
+
205
+ Returns
206
+ -------
207
+ bool
208
+ ``True`` when help was emitted, ``False`` when ``ctx`` was ``None``.
209
+ """
210
+ if ctx is None:
211
+ return False
212
+
213
+ with contextlib.redirect_stdout(sys.stderr):
214
+ ctx.get_help()
215
+ return True
216
+
217
+
218
+ def _emit_root_help(
219
+ command: click.Command,
220
+ ) -> None:
221
+ """
222
+ Print the root ``etlplus`` help text to stderr.
223
+
224
+ Parameters
225
+ ----------
226
+ command : click.Command
227
+ The root Typer/Click command.
228
+ """
229
+ ctx = command.make_context('etlplus', [], resilient_parsing=True)
230
+ try:
231
+ _emit_context_help(ctx)
232
+ finally:
233
+ ctx.close()
234
+
235
+
236
+ def _is_illegal_option_error(
237
+ exc: click.exceptions.UsageError,
238
+ ) -> bool:
239
+ """
240
+ Return ``True`` when usage errors stem from invalid options.
241
+
242
+ Parameters
243
+ ----------
244
+ exc : click.exceptions.UsageError
245
+ The usage error to inspect.
246
+
247
+ Returns
248
+ -------
249
+ bool
250
+ ``True`` when the error indicates illegal options.
251
+ """
252
+ return isinstance(
253
+ exc,
254
+ (
255
+ click.exceptions.BadOptionUsage,
256
+ click.exceptions.NoSuchOption,
257
+ ),
258
+ )
259
+
260
+
261
+ def _is_unknown_command_error(
262
+ exc: click.exceptions.UsageError,
263
+ ) -> bool:
264
+ """
265
+ Return ``True`` when a :class:`UsageError` indicates bad subcommand.
266
+
267
+ Parameters
268
+ ----------
269
+ exc : click.exceptions.UsageError
270
+ The usage error to inspect.
271
+
272
+ Returns
273
+ -------
274
+ bool
275
+ ``True`` when the error indicates an unknown command.
276
+ """
277
+ message = getattr(exc, 'message', None) or str(exc)
278
+ return message.startswith('No such command ')
279
+
280
+
191
281
  # SECTION: FUNCTIONS ======================================================== #
192
282
 
193
283
 
@@ -239,7 +329,7 @@ def create_parser() -> argparse.ArgumentParser:
239
329
  ),
240
330
  )
241
331
  _add_format_options(extract_parser, context='source')
242
- extract_parser.set_defaults(func=cmd_extract)
332
+ extract_parser.set_defaults(func=extract_handler)
243
333
 
244
334
  validate_parser = subparsers.add_parser(
245
335
  'validate',
@@ -256,7 +346,7 @@ def create_parser() -> argparse.ArgumentParser:
256
346
  default={},
257
347
  help='Validation rules as JSON string',
258
348
  )
259
- validate_parser.set_defaults(func=cmd_validate)
349
+ validate_parser.set_defaults(func=validate_handler)
260
350
 
261
351
  transform_parser = subparsers.add_parser(
262
352
  'transform',
@@ -304,7 +394,7 @@ def create_parser() -> argparse.ArgumentParser:
304
394
  'File targets infer format from the extension.'
305
395
  ),
306
396
  )
307
- transform_parser.set_defaults(func=cmd_transform)
397
+ transform_parser.set_defaults(func=transform_handler)
308
398
 
309
399
  load_parser = subparsers.add_parser(
310
400
  'load',
@@ -328,13 +418,14 @@ def create_parser() -> argparse.ArgumentParser:
328
418
  ),
329
419
  )
330
420
  _add_format_options(load_parser, context='target')
331
- load_parser.set_defaults(func=cmd_load)
421
+ load_parser.set_defaults(func=load_handler)
332
422
 
333
423
  pipe_parser = subparsers.add_parser(
334
424
  'pipeline',
335
425
  help=(
336
- 'Inspect or run pipeline YAML (see '
337
- f'{PROJECT_URL}/blob/main/docs/pipeline-guide.md)'
426
+ 'DEPRECATED: use "list" (for summary/jobs) or "run" (to execute); '
427
+ 'see '
428
+ f'{PROJECT_URL}/blob/main/docs/pipeline-guide.md'
338
429
  ),
339
430
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
340
431
  )
@@ -349,35 +440,83 @@ def create_parser() -> argparse.ArgumentParser:
349
440
  metavar='JOB',
350
441
  help='Run a specific job by name',
351
442
  )
352
- pipe_parser.set_defaults(func=cmd_pipeline)
443
+ pipe_parser.set_defaults(func=pipeline_handler)
444
+
445
+ render_parser = subparsers.add_parser(
446
+ 'render',
447
+ help='Render SQL DDL from table schema specs',
448
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
449
+ )
450
+ render_parser.add_argument(
451
+ '--config',
452
+ help='Pipeline YAML containing table_schemas',
453
+ )
454
+ render_parser.add_argument(
455
+ '-o',
456
+ '--output',
457
+ help='Write SQL to this path (stdout when omitted)',
458
+ )
459
+ render_parser.add_argument(
460
+ '--spec',
461
+ help='Standalone table spec file (.yml/.yaml/.json)',
462
+ )
463
+ render_parser.add_argument(
464
+ '--table',
465
+ help='Render only the table matching this name',
466
+ )
467
+ render_parser.add_argument(
468
+ '--template',
469
+ default='ddl',
470
+ help='Template key (ddl/view) or path to a Jinja template file',
471
+ )
472
+ render_parser.add_argument(
473
+ '--template-path',
474
+ dest='template_path',
475
+ help=(
476
+ 'Explicit path to a Jinja template file (overrides template key).'
477
+ ),
478
+ )
479
+ render_parser.set_defaults(func=render_handler)
353
480
 
354
- list_parser = subparsers.add_parser(
355
- 'list',
356
- help='List ETL pipeline metadata',
481
+ check_parser = subparsers.add_parser(
482
+ 'check',
483
+ help='Inspect ETL pipeline metadata',
357
484
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
358
485
  )
359
- _add_config_option(list_parser)
486
+ _add_config_option(check_parser)
487
+ _add_boolean_flag(
488
+ check_parser,
489
+ name='jobs',
490
+ help_text='List ETL jobs',
491
+ )
360
492
  _add_boolean_flag(
361
- list_parser,
493
+ check_parser,
362
494
  name='pipelines',
363
495
  help_text='List ETL pipelines',
364
496
  )
365
497
  _add_boolean_flag(
366
- list_parser,
498
+ check_parser,
367
499
  name='sources',
368
500
  help_text='List data sources',
369
501
  )
370
502
  _add_boolean_flag(
371
- list_parser,
503
+ check_parser,
504
+ name='summary',
505
+ help_text=(
506
+ 'Show pipeline summary (name, version, sources, targets, jobs)'
507
+ ),
508
+ )
509
+ _add_boolean_flag(
510
+ check_parser,
372
511
  name='targets',
373
512
  help_text='List data targets',
374
513
  )
375
514
  _add_boolean_flag(
376
- list_parser,
515
+ check_parser,
377
516
  name='transforms',
378
517
  help_text='List data transforms',
379
518
  )
380
- list_parser.set_defaults(func=cmd_list)
519
+ check_parser.set_defaults(func=check_handler)
381
520
 
382
521
  run_parser = subparsers.add_parser(
383
522
  'run',
@@ -398,7 +537,7 @@ def create_parser() -> argparse.ArgumentParser:
398
537
  '--pipeline',
399
538
  help='Name of the pipeline to run',
400
539
  )
401
- run_parser.set_defaults(func=cmd_run)
540
+ run_parser.set_defaults(func=run_handler)
402
541
 
403
542
  return parser
404
543
 
@@ -422,6 +561,9 @@ def main(
422
561
 
423
562
  Raises
424
563
  ------
564
+ click.exceptions.UsageError
565
+ Re-raises Typer/Click usage errors after printing help for unknown
566
+ commands.
425
567
  SystemExit
426
568
  Re-raises SystemExit exceptions to preserve exit codes.
427
569
 
@@ -442,6 +584,19 @@ def main(
442
584
  )
443
585
  return int(result or 0)
444
586
 
587
+ except click.exceptions.UsageError as exc:
588
+ if _is_unknown_command_error(exc):
589
+ typer.echo(f'Error: {exc}', err=True)
590
+ _emit_root_help(command)
591
+ return int(getattr(exc, 'exit_code', 2))
592
+ if _is_illegal_option_error(exc):
593
+ typer.echo(f'Error: {exc}', err=True)
594
+ if not _emit_context_help(exc.ctx):
595
+ _emit_root_help(command)
596
+ return int(getattr(exc, 'exit_code', 2))
597
+
598
+ raise
599
+
445
600
  except typer.Exit as exc:
446
601
  return int(exc.exit_code)
447
602
 
@@ -190,6 +190,8 @@ class PipelineConfig:
190
190
  Target connectors, parsed tolerantly.
191
191
  jobs : list[JobConfig]
192
192
  Job orchestration definitions.
193
+ table_schemas : list[dict[str, Any]]
194
+ Optional DDL-style table specifications used by the render command.
193
195
  """
194
196
 
195
197
  # -- Attributes -- #
@@ -208,6 +210,7 @@ class PipelineConfig:
208
210
  transforms: dict[str, dict[str, Any]] = field(default_factory=dict)
209
211
  targets: list[Connector] = field(default_factory=list)
210
212
  jobs: list[JobConfig] = field(default_factory=list)
213
+ table_schemas: list[dict[str, Any]] = field(default_factory=list)
211
214
 
212
215
  # -- Class Methods -- #
213
216
 
@@ -312,6 +315,13 @@ class PipelineConfig:
312
315
  # Jobs
313
316
  jobs = _build_jobs(raw)
314
317
 
318
+ # Table schemas (optional, tolerant pass-through structures).
319
+ table_schemas: list[dict[str, Any]] = []
320
+ for entry in raw.get('table_schemas', []) or []:
321
+ spec = maybe_mapping(entry)
322
+ if spec is not None:
323
+ table_schemas.append(dict(spec))
324
+
315
325
  return cls(
316
326
  name=name,
317
327
  version=version,
@@ -325,4 +335,5 @@ class PipelineConfig:
325
335
  transforms=transforms,
326
336
  targets=targets,
327
337
  jobs=jobs,
338
+ table_schemas=table_schemas,
328
339
  )
@@ -0,0 +1,42 @@
1
+ """
2
+ :mod:`etlplus.database` package.
3
+
4
+ Database utilities for:
5
+ - DDL rendering and schema management.
6
+ - Schema parsing from configuration files.
7
+ - Dynamic ORM generation.
8
+ - Database engine/session management.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .ddl import load_table_spec
14
+ from .ddl import render_table_sql
15
+ from .ddl import render_tables
16
+ from .ddl import render_tables_to_string
17
+ from .engine import engine
18
+ from .engine import load_database_url_from_config
19
+ from .engine import make_engine
20
+ from .engine import session
21
+ from .orm import build_models
22
+ from .orm import load_and_build_models
23
+ from .schema import load_table_specs
24
+
25
+ # SECTION: EXPORTS ========================================================== #
26
+
27
+
28
+ __all__ = [
29
+ # Functions
30
+ 'build_models',
31
+ 'load_and_build_models',
32
+ 'load_database_url_from_config',
33
+ 'load_table_spec',
34
+ 'load_table_specs',
35
+ 'make_engine',
36
+ 'render_table_sql',
37
+ 'render_tables',
38
+ 'render_tables_to_string',
39
+ # Singletons
40
+ 'engine',
41
+ 'session',
42
+ ]