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/app.py +257 -129
- etlplus/cli/handlers.py +244 -135
- etlplus/cli/main.py +205 -50
- etlplus/config/pipeline.py +11 -0
- etlplus/database/__init__.py +42 -0
- etlplus/database/ddl.py +311 -0
- etlplus/database/engine.py +146 -0
- etlplus/database/orm.py +347 -0
- etlplus/database/schema.py +273 -0
- etlplus/run.py +2 -4
- etlplus/templates/__init__.py +5 -0
- etlplus/templates/ddl.sql.j2 +128 -0
- etlplus/templates/view.sql.j2 +69 -0
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/METADATA +65 -1
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/RECORD +19 -11
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/WHEEL +0 -0
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/entry_points.txt +0 -0
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.4.6.dist-info → etlplus-0.7.0.dist-info}/top_level.txt +0 -0
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
|
|
26
|
-
from .handlers import
|
|
27
|
-
from .handlers import
|
|
28
|
-
from .handlers import
|
|
29
|
-
from .handlers import
|
|
30
|
-
from .handlers import
|
|
31
|
-
from .handlers import
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
421
|
+
load_parser.set_defaults(func=load_handler)
|
|
332
422
|
|
|
333
423
|
pipe_parser = subparsers.add_parser(
|
|
334
424
|
'pipeline',
|
|
335
425
|
help=(
|
|
336
|
-
'
|
|
337
|
-
|
|
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=
|
|
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
|
-
|
|
355
|
-
'
|
|
356
|
-
help='
|
|
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(
|
|
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
|
-
|
|
493
|
+
check_parser,
|
|
362
494
|
name='pipelines',
|
|
363
495
|
help_text='List ETL pipelines',
|
|
364
496
|
)
|
|
365
497
|
_add_boolean_flag(
|
|
366
|
-
|
|
498
|
+
check_parser,
|
|
367
499
|
name='sources',
|
|
368
500
|
help_text='List data sources',
|
|
369
501
|
)
|
|
370
502
|
_add_boolean_flag(
|
|
371
|
-
|
|
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
|
-
|
|
515
|
+
check_parser,
|
|
377
516
|
name='transforms',
|
|
378
517
|
help_text='List data transforms',
|
|
379
518
|
)
|
|
380
|
-
|
|
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=
|
|
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
|
|
etlplus/config/pipeline.py
CHANGED
|
@@ -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
|
+
]
|