etlplus 0.4.9__py3-none-any.whl → 0.5.1__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 +112 -0
- etlplus/cli/handlers.py +114 -20
- etlplus/cli/main.py +37 -0
- etlplus/config/pipeline.py +11 -0
- etlplus/ddl.py +197 -0
- etlplus/templates/__init__.py +5 -0
- etlplus/templates/ddl.sql.j2 +128 -0
- etlplus/templates/view.sql.j2 +69 -0
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/METADATA +1 -1
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/RECORD +14 -10
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/WHEEL +0 -0
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/entry_points.txt +0 -0
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.4.9.dist-info → etlplus-0.5.1.dist-info}/top_level.txt +0 -0
etlplus/cli/app.py
CHANGED
|
@@ -25,6 +25,7 @@ Subcommands
|
|
|
25
25
|
- ``validate``: validate data against rules
|
|
26
26
|
- ``transform``: transform records
|
|
27
27
|
- ``load``: load data to files, databases, or REST APIs
|
|
28
|
+
- ``render``: render SQL DDL from table schema specs
|
|
28
29
|
|
|
29
30
|
Notes
|
|
30
31
|
-----
|
|
@@ -60,6 +61,7 @@ from .handlers import cmd_extract
|
|
|
60
61
|
from .handlers import cmd_list
|
|
61
62
|
from .handlers import cmd_load
|
|
62
63
|
from .handlers import cmd_pipeline
|
|
64
|
+
from .handlers import cmd_render
|
|
63
65
|
from .handlers import cmd_run
|
|
64
66
|
from .handlers import cmd_transform
|
|
65
67
|
from .handlers import cmd_validate
|
|
@@ -258,6 +260,67 @@ PipelineConfigOption = Annotated[
|
|
|
258
260
|
),
|
|
259
261
|
]
|
|
260
262
|
|
|
263
|
+
RenderConfigOption = Annotated[
|
|
264
|
+
str | None,
|
|
265
|
+
typer.Option(
|
|
266
|
+
'--config',
|
|
267
|
+
metavar='PATH',
|
|
268
|
+
help='Pipeline YAML that includes table_schemas for rendering.',
|
|
269
|
+
show_default=False,
|
|
270
|
+
),
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
RenderOutputOption = Annotated[
|
|
274
|
+
str | None,
|
|
275
|
+
typer.Option(
|
|
276
|
+
'--output',
|
|
277
|
+
'-o',
|
|
278
|
+
metavar='PATH',
|
|
279
|
+
help='Write rendered SQL to PATH (default: stdout).',
|
|
280
|
+
),
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
RenderSpecOption = Annotated[
|
|
284
|
+
str | None,
|
|
285
|
+
typer.Option(
|
|
286
|
+
'--spec',
|
|
287
|
+
metavar='PATH',
|
|
288
|
+
help='Standalone table spec file (.yml/.yaml/.json).',
|
|
289
|
+
show_default=False,
|
|
290
|
+
),
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
RenderTableOption = Annotated[
|
|
294
|
+
str | None,
|
|
295
|
+
typer.Option(
|
|
296
|
+
'--table',
|
|
297
|
+
metavar='NAME',
|
|
298
|
+
help='Filter to a single table name from table_schemas.',
|
|
299
|
+
),
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
RenderTemplateOption = Annotated[
|
|
303
|
+
str,
|
|
304
|
+
typer.Option(
|
|
305
|
+
'--template',
|
|
306
|
+
'-t',
|
|
307
|
+
metavar='KEY|PATH',
|
|
308
|
+
help='Template key (ddl/view) or path to a Jinja template file.',
|
|
309
|
+
show_default=True,
|
|
310
|
+
),
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
RenderTemplatePathOption = Annotated[
|
|
314
|
+
str | None,
|
|
315
|
+
typer.Option(
|
|
316
|
+
'--template-path',
|
|
317
|
+
metavar='PATH',
|
|
318
|
+
help=(
|
|
319
|
+
'Explicit path to a Jinja template file (overrides template key).'
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
]
|
|
323
|
+
|
|
261
324
|
|
|
262
325
|
# SECTION: DATA CLASSES ===================================================== #
|
|
263
326
|
|
|
@@ -1001,6 +1064,55 @@ def pipeline_cmd(
|
|
|
1001
1064
|
return int(cmd_pipeline(ns))
|
|
1002
1065
|
|
|
1003
1066
|
|
|
1067
|
+
@app.command('render')
|
|
1068
|
+
def render_cmd(
|
|
1069
|
+
ctx: typer.Context,
|
|
1070
|
+
config: RenderConfigOption = None,
|
|
1071
|
+
spec: RenderSpecOption = None,
|
|
1072
|
+
table: RenderTableOption = None,
|
|
1073
|
+
template: RenderTemplateOption = 'ddl',
|
|
1074
|
+
template_path: RenderTemplatePathOption = None,
|
|
1075
|
+
output: RenderOutputOption = None,
|
|
1076
|
+
) -> int:
|
|
1077
|
+
"""
|
|
1078
|
+
Render SQL DDL from table schemas defined in YAML/JSON configs.
|
|
1079
|
+
|
|
1080
|
+
Parameters
|
|
1081
|
+
----------
|
|
1082
|
+
ctx : typer.Context
|
|
1083
|
+
Typer execution context provided to the command.
|
|
1084
|
+
config : RenderConfigOption, optional
|
|
1085
|
+
Pipeline YAML containing ``table_schemas`` entries.
|
|
1086
|
+
spec : RenderSpecOption, optional
|
|
1087
|
+
Standalone table spec file (.yml/.yaml/.json).
|
|
1088
|
+
table : RenderTableOption, optional
|
|
1089
|
+
Filter to a single table name within the available specs.
|
|
1090
|
+
template : RenderTemplateOption, optional
|
|
1091
|
+
Built-in template key or template file path.
|
|
1092
|
+
template_path : RenderTemplatePathOption, optional
|
|
1093
|
+
Explicit template file path to render with.
|
|
1094
|
+
output : RenderOutputOption, optional
|
|
1095
|
+
Path to write SQL to (stdout when omitted).
|
|
1096
|
+
|
|
1097
|
+
Returns
|
|
1098
|
+
-------
|
|
1099
|
+
int
|
|
1100
|
+
Zero on success.
|
|
1101
|
+
"""
|
|
1102
|
+
state = _ensure_state(ctx)
|
|
1103
|
+
ns = _stateful_namespace(
|
|
1104
|
+
state,
|
|
1105
|
+
command='render',
|
|
1106
|
+
config=config,
|
|
1107
|
+
spec=spec,
|
|
1108
|
+
table=table,
|
|
1109
|
+
template=template,
|
|
1110
|
+
template_path=template_path,
|
|
1111
|
+
output=output,
|
|
1112
|
+
)
|
|
1113
|
+
return int(cmd_render(ns))
|
|
1114
|
+
|
|
1115
|
+
|
|
1004
1116
|
@app.command('run')
|
|
1005
1117
|
def run_cmd(
|
|
1006
1118
|
ctx: typer.Context,
|
etlplus/cli/handlers.py
CHANGED
|
@@ -18,6 +18,8 @@ from typing import cast
|
|
|
18
18
|
|
|
19
19
|
from ..config import PipelineConfig
|
|
20
20
|
from ..config import load_pipeline_config
|
|
21
|
+
from ..ddl import load_table_spec
|
|
22
|
+
from ..ddl import render_tables
|
|
21
23
|
from ..enums import FileFormat
|
|
22
24
|
from ..extract import extract
|
|
23
25
|
from ..file import File
|
|
@@ -38,6 +40,7 @@ __all__ = [
|
|
|
38
40
|
'cmd_list',
|
|
39
41
|
'cmd_load',
|
|
40
42
|
'cmd_pipeline',
|
|
43
|
+
'cmd_render',
|
|
41
44
|
'cmd_run',
|
|
42
45
|
'cmd_transform',
|
|
43
46
|
'cmd_validate',
|
|
@@ -47,6 +50,37 @@ __all__ = [
|
|
|
47
50
|
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
48
51
|
|
|
49
52
|
|
|
53
|
+
def _collect_table_specs(
|
|
54
|
+
config_path: str | None,
|
|
55
|
+
spec_path: str | None,
|
|
56
|
+
) -> list[dict[str, Any]]:
|
|
57
|
+
"""
|
|
58
|
+
Load table schemas from a pipeline config and/or standalone spec.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
config_path : str | None
|
|
63
|
+
Path to a pipeline YAML config file.
|
|
64
|
+
spec_path : str | None
|
|
65
|
+
Path to a standalone table spec file.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
list[dict[str, Any]]
|
|
70
|
+
Collected table specification mappings.
|
|
71
|
+
"""
|
|
72
|
+
specs: list[dict[str, Any]] = []
|
|
73
|
+
|
|
74
|
+
if spec_path:
|
|
75
|
+
specs.append(load_table_spec(Path(spec_path)))
|
|
76
|
+
|
|
77
|
+
if config_path:
|
|
78
|
+
cfg = load_pipeline_config(config_path, substitute=True)
|
|
79
|
+
specs.extend(getattr(cfg, 'table_schemas', []))
|
|
80
|
+
|
|
81
|
+
return specs
|
|
82
|
+
|
|
83
|
+
|
|
50
84
|
def _emit_json(
|
|
51
85
|
data: Any,
|
|
52
86
|
*,
|
|
@@ -75,6 +109,23 @@ def _emit_json(
|
|
|
75
109
|
print(dumped)
|
|
76
110
|
|
|
77
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
|
+
|
|
78
129
|
def _infer_payload_format(
|
|
79
130
|
text: str,
|
|
80
131
|
) -> str:
|
|
@@ -134,23 +185,6 @@ def _list_sections(
|
|
|
134
185
|
return sections
|
|
135
186
|
|
|
136
187
|
|
|
137
|
-
def _explicit_cli_format(
|
|
138
|
-
args: argparse.Namespace,
|
|
139
|
-
) -> str | None:
|
|
140
|
-
"""Return the explicit CLI format hint when provided."""
|
|
141
|
-
|
|
142
|
-
if not getattr(args, '_format_explicit', False):
|
|
143
|
-
return None
|
|
144
|
-
for attr in ('format', 'target_format', 'source_format'):
|
|
145
|
-
value = getattr(args, attr, None)
|
|
146
|
-
if value is None:
|
|
147
|
-
continue
|
|
148
|
-
normalized = value.strip().lower()
|
|
149
|
-
if normalized:
|
|
150
|
-
return normalized
|
|
151
|
-
return None
|
|
152
|
-
|
|
153
|
-
|
|
154
188
|
def _materialize_file_payload(
|
|
155
189
|
source: object,
|
|
156
190
|
*,
|
|
@@ -224,7 +258,6 @@ def _parse_text_payload(
|
|
|
224
258
|
JSONData | str
|
|
225
259
|
The parsed payload as JSON data or raw text.
|
|
226
260
|
"""
|
|
227
|
-
|
|
228
261
|
effective = (fmt or '').strip().lower() or _infer_payload_format(text)
|
|
229
262
|
if effective == 'json':
|
|
230
263
|
return cast(JSONData, json_type(text))
|
|
@@ -265,7 +298,8 @@ def _pipeline_summary(
|
|
|
265
298
|
def _presentation_flags(
|
|
266
299
|
args: argparse.Namespace,
|
|
267
300
|
) -> tuple[bool, bool]:
|
|
268
|
-
"""
|
|
301
|
+
"""
|
|
302
|
+
Return presentation toggles from the parsed namespace.
|
|
269
303
|
|
|
270
304
|
Parameters
|
|
271
305
|
----------
|
|
@@ -342,7 +376,6 @@ def _resolve_cli_payload(
|
|
|
342
376
|
Parsed payload or the original source value when hydration is
|
|
343
377
|
disabled.
|
|
344
378
|
"""
|
|
345
|
-
|
|
346
379
|
if isinstance(source, (os.PathLike, str)) and str(source) == '-':
|
|
347
380
|
text = _read_stdin_text()
|
|
348
381
|
return _parse_text_payload(text, format_hint)
|
|
@@ -628,6 +661,67 @@ def cmd_pipeline(
|
|
|
628
661
|
return 0
|
|
629
662
|
|
|
630
663
|
|
|
664
|
+
def cmd_render(
|
|
665
|
+
args: argparse.Namespace,
|
|
666
|
+
) -> int:
|
|
667
|
+
"""Render SQL DDL statements from table schema specs."""
|
|
668
|
+
|
|
669
|
+
_pretty, quiet = _presentation_flags(args)
|
|
670
|
+
|
|
671
|
+
template_value = getattr(args, 'template', 'ddl') or 'ddl'
|
|
672
|
+
template_path = getattr(args, 'template_path', None)
|
|
673
|
+
table_filter = getattr(args, 'table', None)
|
|
674
|
+
spec_path = getattr(args, 'spec', None)
|
|
675
|
+
config_path = getattr(args, 'config', None)
|
|
676
|
+
|
|
677
|
+
# If the provided template points to a file, treat it as a path override.
|
|
678
|
+
file_override = template_path
|
|
679
|
+
template_key = template_value
|
|
680
|
+
if template_path is None:
|
|
681
|
+
candidate_path = Path(template_value)
|
|
682
|
+
if candidate_path.exists():
|
|
683
|
+
file_override = str(candidate_path)
|
|
684
|
+
template_key = None
|
|
685
|
+
|
|
686
|
+
specs = _collect_table_specs(config_path, spec_path)
|
|
687
|
+
if table_filter:
|
|
688
|
+
specs = [
|
|
689
|
+
spec
|
|
690
|
+
for spec in specs
|
|
691
|
+
if str(spec.get('table')) == table_filter
|
|
692
|
+
or str(spec.get('name', '')) == table_filter
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
if not specs:
|
|
696
|
+
target_desc = table_filter or 'table_schemas'
|
|
697
|
+
print(
|
|
698
|
+
'No table schemas found for '
|
|
699
|
+
f'{target_desc}. Provide --spec or a pipeline --config with '
|
|
700
|
+
'table_schemas.',
|
|
701
|
+
file=sys.stderr,
|
|
702
|
+
)
|
|
703
|
+
return 1
|
|
704
|
+
|
|
705
|
+
rendered_chunks = render_tables(
|
|
706
|
+
specs,
|
|
707
|
+
template=template_key,
|
|
708
|
+
template_path=file_override,
|
|
709
|
+
)
|
|
710
|
+
sql_text = (
|
|
711
|
+
'\n'.join(chunk.rstrip() for chunk in rendered_chunks).rstrip() + '\n'
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
output_path = getattr(args, 'output', None)
|
|
715
|
+
if output_path and output_path != '-':
|
|
716
|
+
Path(output_path).write_text(sql_text, encoding='utf-8')
|
|
717
|
+
if not quiet:
|
|
718
|
+
print(f'Rendered {len(specs)} schema(s) to {output_path}')
|
|
719
|
+
return 0
|
|
720
|
+
|
|
721
|
+
print(sql_text)
|
|
722
|
+
return 0
|
|
723
|
+
|
|
724
|
+
|
|
631
725
|
def cmd_list(args: argparse.Namespace) -> int:
|
|
632
726
|
"""
|
|
633
727
|
Print requested pipeline sections from a YAML configuration.
|
etlplus/cli/main.py
CHANGED
|
@@ -28,6 +28,7 @@ from .handlers import cmd_extract
|
|
|
28
28
|
from .handlers import cmd_list
|
|
29
29
|
from .handlers import cmd_load
|
|
30
30
|
from .handlers import cmd_pipeline
|
|
31
|
+
from .handlers import cmd_render
|
|
31
32
|
from .handlers import cmd_run
|
|
32
33
|
from .handlers import cmd_transform
|
|
33
34
|
from .handlers import cmd_validate
|
|
@@ -441,6 +442,42 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
441
442
|
)
|
|
442
443
|
pipe_parser.set_defaults(func=cmd_pipeline)
|
|
443
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=cmd_render)
|
|
480
|
+
|
|
444
481
|
list_parser = subparsers.add_parser(
|
|
445
482
|
'list',
|
|
446
483
|
help='List ETL pipeline metadata',
|
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
|
)
|
etlplus/ddl.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.ddl` module.
|
|
3
|
+
|
|
4
|
+
DDL rendering utilities for pipeline table schemas.
|
|
5
|
+
|
|
6
|
+
Exposes helpers to load YAML/JSON table specs and render them into SQL via
|
|
7
|
+
Jinja templates. Mirrors the behavior of ``tools/render_ddl.py`` so the CLI
|
|
8
|
+
can emit DDLs without shelling out to that script.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.resources
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from jinja2 import DictLoader
|
|
22
|
+
from jinja2 import Environment
|
|
23
|
+
from jinja2 import FileSystemLoader
|
|
24
|
+
from jinja2 import StrictUndefined
|
|
25
|
+
|
|
26
|
+
# SECTION: EXPORTS ========================================================== #
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
'TEMPLATES',
|
|
31
|
+
'load_table_spec',
|
|
32
|
+
'render_table_sql',
|
|
33
|
+
'render_tables',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# SECTION: CONSTANTS ======================================================== #
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
TEMPLATES = {
|
|
41
|
+
'ddl': 'ddl.sql.j2',
|
|
42
|
+
'view': 'view.sql.j2',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_env(
|
|
50
|
+
*,
|
|
51
|
+
template_key: str | None,
|
|
52
|
+
template_path: str | None,
|
|
53
|
+
) -> Environment:
|
|
54
|
+
"""Return a Jinja2 environment using a built-in or file template."""
|
|
55
|
+
file_override = template_path or os.environ.get('TEMPLATE_NAME')
|
|
56
|
+
if file_override:
|
|
57
|
+
path = Path(file_override)
|
|
58
|
+
if not path.exists():
|
|
59
|
+
raise FileNotFoundError(f'Template file not found: {path}')
|
|
60
|
+
loader = FileSystemLoader(str(path.parent))
|
|
61
|
+
env = Environment(
|
|
62
|
+
loader=loader,
|
|
63
|
+
undefined=StrictUndefined,
|
|
64
|
+
trim_blocks=True,
|
|
65
|
+
lstrip_blocks=True,
|
|
66
|
+
)
|
|
67
|
+
env.globals['TEMPLATE_NAME'] = path.name
|
|
68
|
+
return env
|
|
69
|
+
|
|
70
|
+
key = (template_key or 'ddl').strip()
|
|
71
|
+
if key not in TEMPLATES:
|
|
72
|
+
choices = ', '.join(sorted(TEMPLATES))
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f'Unknown template key "{key}". Choose from: {choices}',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Load template from package data
|
|
78
|
+
template_filename = TEMPLATES[key]
|
|
79
|
+
template_source = _load_template_text(template_filename)
|
|
80
|
+
|
|
81
|
+
env = Environment(
|
|
82
|
+
loader=DictLoader({key: template_source}),
|
|
83
|
+
undefined=StrictUndefined,
|
|
84
|
+
trim_blocks=True,
|
|
85
|
+
lstrip_blocks=True,
|
|
86
|
+
)
|
|
87
|
+
env.globals['TEMPLATE_NAME'] = key
|
|
88
|
+
return env
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _load_template_text(filename: str) -> str:
|
|
92
|
+
"""Return the raw template text bundled with the package."""
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
return (
|
|
96
|
+
importlib.resources.files(
|
|
97
|
+
'etlplus.templates',
|
|
98
|
+
)
|
|
99
|
+
.joinpath(filename)
|
|
100
|
+
.read_text(encoding='utf-8')
|
|
101
|
+
)
|
|
102
|
+
except FileNotFoundError as exc: # pragma: no cover - deployment guard
|
|
103
|
+
raise FileNotFoundError(
|
|
104
|
+
f'Could not load template {filename} '
|
|
105
|
+
f'from etlplus.templates package data.',
|
|
106
|
+
) from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# SECTION: FUNCTIONS ======================================================== #
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_table_spec(path: Path | str) -> dict[str, Any]:
|
|
113
|
+
"""Load a table spec from JSON or YAML."""
|
|
114
|
+
|
|
115
|
+
spec_path = Path(path)
|
|
116
|
+
text = spec_path.read_text(encoding='utf-8')
|
|
117
|
+
suffix = spec_path.suffix.lower()
|
|
118
|
+
|
|
119
|
+
if suffix == '.json':
|
|
120
|
+
return json.loads(text)
|
|
121
|
+
|
|
122
|
+
if suffix in {'.yml', '.yaml'}:
|
|
123
|
+
try:
|
|
124
|
+
import yaml # type: ignore
|
|
125
|
+
except Exception as exc: # pragma: no cover
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
'Missing dependency: pyyaml is required for YAML specs.',
|
|
128
|
+
) from exc
|
|
129
|
+
return yaml.safe_load(text)
|
|
130
|
+
|
|
131
|
+
raise ValueError('Spec must be .json, .yml, or .yaml')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_table_sql(
|
|
135
|
+
spec: Mapping[str, Any],
|
|
136
|
+
*,
|
|
137
|
+
template: str | None = 'ddl',
|
|
138
|
+
template_path: str | None = None,
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Render a single table spec into SQL text.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
spec : Mapping[str, Any]
|
|
146
|
+
Table specification mapping.
|
|
147
|
+
template : str | None, optional
|
|
148
|
+
Template key to use (default: 'ddl').
|
|
149
|
+
template_path : str | None, optional
|
|
150
|
+
Path to a custom template file (overrides ``template``).
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
str
|
|
155
|
+
Rendered SQL string.
|
|
156
|
+
|
|
157
|
+
Raises
|
|
158
|
+
------
|
|
159
|
+
TypeError
|
|
160
|
+
If the loaded template name is not a string.
|
|
161
|
+
"""
|
|
162
|
+
env = _build_env(template_key=template, template_path=template_path)
|
|
163
|
+
template_name = env.globals.get('TEMPLATE_NAME')
|
|
164
|
+
if not isinstance(template_name, str):
|
|
165
|
+
raise TypeError('TEMPLATE_NAME must be a string.')
|
|
166
|
+
tmpl = env.get_template(template_name)
|
|
167
|
+
return tmpl.render(spec=spec).rstrip() + '\n'
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_tables(
|
|
171
|
+
specs: Iterable[Mapping[str, Any]],
|
|
172
|
+
*,
|
|
173
|
+
template: str | None = 'ddl',
|
|
174
|
+
template_path: str | None = None,
|
|
175
|
+
) -> list[str]:
|
|
176
|
+
"""
|
|
177
|
+
Render multiple table specs into a list of SQL payloads.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
specs : Iterable[Mapping[str, Any]]
|
|
182
|
+
Table specification mappings.
|
|
183
|
+
template : str | None, optional
|
|
184
|
+
Template key to use (default: 'ddl').
|
|
185
|
+
template_path : str | None, optional
|
|
186
|
+
Path to a custom template file (overrides ``template``).
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
list[str]
|
|
191
|
+
Rendered SQL strings for each table spec.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
render_table_sql(spec, template=template, template_path=template_path)
|
|
196
|
+
for spec in specs
|
|
197
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{# ---------- helpers ---------- #}
|
|
2
|
+
{% macro qid(name) -%}[{{ name | replace(']', ']]') }}]{%- endmacro %}
|
|
3
|
+
{% macro qname(schema, name) -%}{{ qid(schema) }}.{{ qid(name) }}{%- endmacro %}
|
|
4
|
+
{% macro df_name(table, col) -%}DF_{{ table }}_{{ col }}{%- endmacro %}
|
|
5
|
+
{% macro ck_name(table, col) -%}CK_{{ table }}_{{ col }}{%- endmacro %}
|
|
6
|
+
|
|
7
|
+
{# ---------- create schema (optional) ---------- #}
|
|
8
|
+
{% if spec.create_schema | default(true) %}
|
|
9
|
+
IF
|
|
10
|
+
NOT EXISTS (
|
|
11
|
+
SELECT 1
|
|
12
|
+
FROM sys.schemas
|
|
13
|
+
WHERE name = N'{{ spec.schema }}'
|
|
14
|
+
)
|
|
15
|
+
EXEC ('CREATE SCHEMA {{ qid(spec.schema) }}');
|
|
16
|
+
GO
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
{# ---------- create table guarded ---------- #}
|
|
20
|
+
{% set fqtn = qname(spec.schema, spec.table) %}
|
|
21
|
+
IF OBJECT_ID(N'{{ spec.schema }}.{{ spec.table }}', N'U') IS NULL
|
|
22
|
+
BEGIN
|
|
23
|
+
CREATE TABLE {{ fqtn }} (
|
|
24
|
+
{% set ns = namespace(maxlen=0) %}
|
|
25
|
+
{% for c in spec.columns %}
|
|
26
|
+
{% set nlen = (c.name|string)|length %}
|
|
27
|
+
{% if nlen > ns.maxlen %}{% set ns.maxlen = nlen %}{% endif %}
|
|
28
|
+
{% endfor %}
|
|
29
|
+
{% set indent_cols = ' ' %}
|
|
30
|
+
{% for c in spec.columns %}
|
|
31
|
+
{# Map-safe lookups so StrictUndefined never bites on missing keys #}
|
|
32
|
+
{%- set ctype = c.get('type') -%}
|
|
33
|
+
{%- set ident = c.get('identity') -%}
|
|
34
|
+
{%- set computed = c.get('computed') -%}
|
|
35
|
+
{%- set nullable = c.get('nullable', true) -%}
|
|
36
|
+
{%- set defv = c.get('default') -%}
|
|
37
|
+
{%- set checkv = c.get('check') -%}
|
|
38
|
+
{{- indent_cols -}}{{ qid(c.name) }} {{ ctype }}
|
|
39
|
+
{%- if computed %} AS ({{ computed }}) PERSISTED{%- endif -%}
|
|
40
|
+
{%- if nullable %} NULL{%- else %} NOT NULL{%- endif -%}
|
|
41
|
+
{%- if ident %} IDENTITY({{ ident.get('seed', 1) }}, {{ ident.get('increment', 1) }}){%- endif -%}
|
|
42
|
+
{%- if defv %} CONSTRAINT {{ qid(df_name(spec.table, c.name)) }} DEFAULT ({{ defv }}){%- endif -%}
|
|
43
|
+
{%- if checkv %} CONSTRAINT {{ qid(ck_name(spec.table, c.name)) }} CHECK ({{ checkv }}){%- endif -%}
|
|
44
|
+
{{ "," if not loop.last }}
|
|
45
|
+
{% endfor %}
|
|
46
|
+
|
|
47
|
+
{%- if spec.primary_key is defined and spec.primary_key %}
|
|
48
|
+
, CONSTRAINT {{ qid(spec.primary_key.name
|
|
49
|
+
| default('PK_' ~ spec.table)) }}
|
|
50
|
+
PRIMARY KEY CLUSTERED (
|
|
51
|
+
{%- for col in spec.primary_key.columns -%}
|
|
52
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
53
|
+
{%- endfor -%}
|
|
54
|
+
)
|
|
55
|
+
{%- endif %}
|
|
56
|
+
|
|
57
|
+
{%- for uq in spec.unique_constraints | default([]) %}
|
|
58
|
+
, CONSTRAINT {{ qid(uq.name) }} UNIQUE (
|
|
59
|
+
{%- for col in uq.columns -%}
|
|
60
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
61
|
+
{%- endfor -%}
|
|
62
|
+
)
|
|
63
|
+
{%- endfor %}
|
|
64
|
+
);
|
|
65
|
+
END
|
|
66
|
+
GO
|
|
67
|
+
|
|
68
|
+
{# ---------- indexes ---------- #}
|
|
69
|
+
{% for ix in spec.indexes | default([]) %}
|
|
70
|
+
{%- set ix_unique = ix.get('unique', false) -%}
|
|
71
|
+
{%- set ix_include = ix.get('include') -%}
|
|
72
|
+
{%- set ix_where = ix.get('where') -%}
|
|
73
|
+
IF
|
|
74
|
+
NOT EXISTS (
|
|
75
|
+
SELECT 1
|
|
76
|
+
FROM sys.indexes
|
|
77
|
+
WHERE name = N'{{ ix.name }}'
|
|
78
|
+
AND object_id = OBJECT_ID(N'{{ spec.schema }}.{{ spec.table }}')
|
|
79
|
+
)
|
|
80
|
+
BEGIN
|
|
81
|
+
CREATE{% if ix_unique %} UNIQUE{% endif %} INDEX {{ qid(ix.name) }}
|
|
82
|
+
ON {{ fqtn }} (
|
|
83
|
+
{%- for col in ix.columns -%}
|
|
84
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
85
|
+
{%- endfor -%}
|
|
86
|
+
)
|
|
87
|
+
{%- if ix_include %}
|
|
88
|
+
INCLUDE (
|
|
89
|
+
{%- for col in ix_include -%}
|
|
90
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
91
|
+
{%- endfor -%}
|
|
92
|
+
)
|
|
93
|
+
{%- endif -%}
|
|
94
|
+
{%- if ix_where %} WHERE {{ ix_where }}{% endif %};
|
|
95
|
+
END
|
|
96
|
+
GO
|
|
97
|
+
{% endfor %}
|
|
98
|
+
|
|
99
|
+
{# ---------- foreign keys ---------- #}
|
|
100
|
+
{% for fk in spec.foreign_keys | default([]) %}
|
|
101
|
+
IF
|
|
102
|
+
NOT EXISTS (
|
|
103
|
+
SELECT 1
|
|
104
|
+
FROM sys.foreign_keys
|
|
105
|
+
WHERE
|
|
106
|
+
name = N'{{ fk.name }}'
|
|
107
|
+
AND parent_object_id = OBJECT_ID(N'{{ spec.schema }}.{{ spec.table }}'
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
BEGIN
|
|
111
|
+
ALTER TABLE {{ fqtn }} WITH CHECK
|
|
112
|
+
ADD CONSTRAINT {{ qid(fk.name) }} FOREIGN KEY (
|
|
113
|
+
{%- for col in fk.columns -%}
|
|
114
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
115
|
+
{%- endfor -%}
|
|
116
|
+
) REFERENCES
|
|
117
|
+
{{ qname(fk.ref_schema | default('dbo'), fk.ref_table) }} (
|
|
118
|
+
{%- for col in fk.ref_columns -%}
|
|
119
|
+
{{ qid(col) }}{{ ", " if not loop.last }}
|
|
120
|
+
{%- endfor -%}
|
|
121
|
+
)
|
|
122
|
+
{%- set on_upd = fk.get('on_update') -%}
|
|
123
|
+
{%- set on_del = fk.get('on_delete') -%}
|
|
124
|
+
{%- if on_upd %} ON UPDATE {{ on_upd }}{% endif -%}
|
|
125
|
+
{%- if on_del %} ON DELETE {{ on_del }}{% endif %};
|
|
126
|
+
END
|
|
127
|
+
GO
|
|
128
|
+
{% endfor %}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{# -- Helpers -- #}
|
|
2
|
+
{% macro qid(name) -%}
|
|
3
|
+
[{{ name | replace(']', ']]') }}]
|
|
4
|
+
{%- endmacro %}
|
|
5
|
+
|
|
6
|
+
{% macro qname(schema, name) -%}
|
|
7
|
+
{{ qid(schema) }}.{{ qid(name) }}
|
|
8
|
+
{%- endmacro %}
|
|
9
|
+
|
|
10
|
+
{# Convert any expression to nvarchar(MAX) with N'' for NULLs #}
|
|
11
|
+
{% macro to_str(expr) -%}
|
|
12
|
+
ISNULL(CONVERT(NVARCHAR(MAX), {{ expr }}), N'')
|
|
13
|
+
{%- endmacro %}
|
|
14
|
+
|
|
15
|
+
{# Normalize truthy values to 'TRUE' / 'FALSE' #}
|
|
16
|
+
{% macro truthy(expr) -%}
|
|
17
|
+
IIF(CONVERT(VARCHAR(5), {{ expr }}) IN ('TRUE', '1', 'YES'), 'TRUE', 'FALSE')
|
|
18
|
+
{%- endmacro %}
|
|
19
|
+
|
|
20
|
+
{# -- Create schema (optional) -- #}
|
|
21
|
+
{% if spec.create_schema | default(true) %}
|
|
22
|
+
IF
|
|
23
|
+
NOT EXISTS (
|
|
24
|
+
SELECT 1
|
|
25
|
+
FROM sys.schemas
|
|
26
|
+
WHERE name = N'{{ spec.schema }}'
|
|
27
|
+
)
|
|
28
|
+
EXEC ('CREATE SCHEMA {{ qid(spec.schema) }}');
|
|
29
|
+
GO
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
{# -- SET options (match your source view) -- #}
|
|
33
|
+
SET QUOTED_IDENTIFIER ON
|
|
34
|
+
SET ANSI_NULLS ON
|
|
35
|
+
GO
|
|
36
|
+
|
|
37
|
+
{# -- Derive names with fallbacks so table-style specs also work -- #}
|
|
38
|
+
{% set view_name = spec.view if spec.view is defined else ('vw_' ~ spec.table) %}
|
|
39
|
+
{% set src_schema = (spec.from_schema if spec.from_schema is defined else (spec.schema if spec.schema is defined else 'dbo')) %}
|
|
40
|
+
{% set src_table = (
|
|
41
|
+
spec.from_table if spec.from_table is defined else
|
|
42
|
+
(spec.source_table if spec.source_table is defined else spec.table)
|
|
43
|
+
) %}
|
|
44
|
+
{% set indent_cols = ' ' %}
|
|
45
|
+
|
|
46
|
+
{# -- Create view -- #}
|
|
47
|
+
CREATE VIEW {{ qname(spec.schema | default('dbo'), view_name) }} AS
|
|
48
|
+
SELECT
|
|
49
|
+
{% if spec.projections is defined and spec.projections %}
|
|
50
|
+
{% for p in spec.projections %}
|
|
51
|
+
{%- if p.type is defined and (p.type | lower)[:8] == 'nvarchar' -%}
|
|
52
|
+
{{- indent_cols -}}{{ to_str(p.expr) }} AS {{ qid(p.alias) }}
|
|
53
|
+
{%- else -%}
|
|
54
|
+
{{- indent_cols -}}{{ p.expr }}
|
|
55
|
+
{%- endif -%}
|
|
56
|
+
{{ "," if not loop.last }}
|
|
57
|
+
{% endfor %}
|
|
58
|
+
{% else %}
|
|
59
|
+
{% for c in spec.columns | default([]) %}
|
|
60
|
+
{%- if c.type is defined and (c.type | lower)[:8] == 'nvarchar' -%}
|
|
61
|
+
{{- indent_cols -}}{{ to_str(qid(c.name)) }} AS {{ qid(c.name) }}
|
|
62
|
+
{%- else -%}
|
|
63
|
+
{{- indent_cols -}}{{ qid(c.name) }}
|
|
64
|
+
{%- endif -%}
|
|
65
|
+
{{ "," if not loop.last }}
|
|
66
|
+
{% endfor %}
|
|
67
|
+
{%- endif %}
|
|
68
|
+
FROM {{ qname(src_schema, src_table) }};
|
|
69
|
+
GO
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
etlplus/__init__.py,sha256=M2gScnyir6WOMAh_EuoQIiAzdcTls0_5hbd_Q6of8I0,1021
|
|
2
2
|
etlplus/__main__.py,sha256=btoROneNiigyfBU7BSzPKZ1R9gzBMpxcpsbPwmuHwTM,479
|
|
3
3
|
etlplus/__version__.py,sha256=1E0GMK_yUWCMQFKxXjTvyMwofi0qT2k4CDNiHWiymWE,327
|
|
4
|
+
etlplus/ddl.py,sha256=uYkiMTx1uDlUypnXCYy0K5ARnHRMHFVzzg8PizBQRLg,5306
|
|
4
5
|
etlplus/enums.py,sha256=V_j18Ud2BCXpFsBk2pZGrvCVrvAMJ7uja1z9fppFGso,10175
|
|
5
6
|
etlplus/extract.py,sha256=f44JdHhNTACxgn44USx05paKTwq7LQY-V4wANCW9hVM,6173
|
|
6
7
|
etlplus/file.py,sha256=RxIAsGDN4f_vNA2B5-ct88JNd_ISAyYbooIRE5DstS8,17972
|
|
@@ -31,21 +32,24 @@ etlplus/api/rate_limiting/__init__.py,sha256=ZySB1dZettEDnWvI1EHf_TZ9L08M_kKsNR-
|
|
|
31
32
|
etlplus/api/rate_limiting/config.py,sha256=2b4wIynblN-1EyMqI4aXa71SljzSjXYh5N1Nngr3jOg,9406
|
|
32
33
|
etlplus/api/rate_limiting/rate_limiter.py,sha256=Uxozqd_Ej5Lsj-M-mLT2WexChgWh7x35_YP10yqYPQA,7159
|
|
33
34
|
etlplus/cli/__init__.py,sha256=J97-Rv931IL1_b4AXnB7Fbbd7HKnHBpx18NQfC_kE6c,299
|
|
34
|
-
etlplus/cli/app.py,sha256=
|
|
35
|
-
etlplus/cli/handlers.py,sha256=
|
|
36
|
-
etlplus/cli/main.py,sha256=
|
|
35
|
+
etlplus/cli/app.py,sha256=buGIIoSIu5cxbYTdPcA_iaxJaPG-eHj-LPD9OgZ0h9w,35824
|
|
36
|
+
etlplus/cli/handlers.py,sha256=O7Mh9nowdMCzaV36KASWZVC4fNMEg9xnVZXE7NHW6P8,18873
|
|
37
|
+
etlplus/cli/main.py,sha256=5qWAKqlRtnb4VEpBfGT45q-LBxi_2hSMnw23jNyYA_Q,16497
|
|
37
38
|
etlplus/config/__init__.py,sha256=VZWzOg7d2YR9NT6UwKTv44yf2FRUMjTHynkm1Dl5Qzo,1486
|
|
38
39
|
etlplus/config/connector.py,sha256=0-TIwevHbKRHVmucvyGpPd-3tB1dKHB-dj0yJ6kq5eY,9809
|
|
39
40
|
etlplus/config/jobs.py,sha256=hmzRCqt0OvCEZZR4ONKrd3lvSv0OmayjLc4yOBk3ug8,7399
|
|
40
|
-
etlplus/config/pipeline.py,sha256=
|
|
41
|
+
etlplus/config/pipeline.py,sha256=Va4MQY6KEyKqHGMKPmh09ZcGpx95br-iNUjpkqtzVbw,9500
|
|
41
42
|
etlplus/config/profile.py,sha256=Ss2zedQGjkaGSpvBLTD4SZaWViMJ7TJPLB8Q2_BTpPg,1898
|
|
42
43
|
etlplus/config/types.py,sha256=a0epJ3z16HQ5bY3Ctf8s_cQPa3f0HHcwdOcjCP2xoG4,4954
|
|
43
44
|
etlplus/config/utils.py,sha256=4SUHMkt5bKBhMhiJm-DrnmE2Q4TfOgdNCKz8PJDS27o,3443
|
|
45
|
+
etlplus/templates/__init__.py,sha256=tsniN7XJYs3NwYxJ6c2HD5upHP3CDkLx-bQCMt97UOM,106
|
|
46
|
+
etlplus/templates/ddl.sql.j2,sha256=s8fMWvcb4eaJVXkifuib1aQPljtZ8buuyB_uA-ZdU3Q,4734
|
|
47
|
+
etlplus/templates/view.sql.j2,sha256=Iy8DHfhq5yyvrUKDxqp_aHIEXY4Tm6j4wT7YDEFWAhk,2180
|
|
44
48
|
etlplus/validation/__init__.py,sha256=Pe5Xg1_EA4uiNZGYu5WTF3j7odjmyxnAJ8rcioaplSQ,1254
|
|
45
49
|
etlplus/validation/utils.py,sha256=Mtqg449VIke0ziy_wd2r6yrwJzQkA1iulZC87FzXMjo,10201
|
|
46
|
-
etlplus-0.
|
|
47
|
-
etlplus-0.
|
|
48
|
-
etlplus-0.
|
|
49
|
-
etlplus-0.
|
|
50
|
-
etlplus-0.
|
|
51
|
-
etlplus-0.
|
|
50
|
+
etlplus-0.5.1.dist-info/licenses/LICENSE,sha256=MuNO63i6kWmgnV2pbP2SLqP54mk1BGmu7CmbtxMmT-U,1069
|
|
51
|
+
etlplus-0.5.1.dist-info/METADATA,sha256=_jyHbbKnTHnDCxcFjzKjUbPlgJVTHfonDBExSFPGzZU,17635
|
|
52
|
+
etlplus-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
etlplus-0.5.1.dist-info/entry_points.txt,sha256=6w-2-jzuPa55spzK34h-UKh2JTEShh38adFRONNP9QE,45
|
|
54
|
+
etlplus-0.5.1.dist-info/top_level.txt,sha256=aWWF-udn_sLGuHTM6W6MLh99ArS9ROkUWO8Mi8y1_2U,8
|
|
55
|
+
etlplus-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|