etlplus 0.5.5__py3-none-any.whl → 0.6.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/handlers.py CHANGED
@@ -18,8 +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
+ from ..database import load_table_spec
22
+ from ..database import render_tables
23
23
  from ..enums import FileFormat
24
24
  from ..extract import extract
25
25
  from ..file import File
@@ -0,0 +1,23 @@
1
+ """
2
+ :mod:`etlplus.database` package.
3
+
4
+ This package defines database-related utilities for ETLPlus, including:
5
+ - DDL rendering and schema management.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .ddl import load_table_spec
11
+ from .ddl import render_table_sql
12
+ from .ddl import render_tables
13
+ from .ddl import render_tables_to_string
14
+
15
+ # SECTION: EXPORTS ========================================================== #
16
+
17
+
18
+ __all__ = [
19
+ 'load_table_spec',
20
+ 'render_table_sql',
21
+ 'render_tables',
22
+ 'render_tables_to_string',
23
+ ]
@@ -11,18 +11,20 @@ can emit DDLs without shelling out to that script.
11
11
  from __future__ import annotations
12
12
 
13
13
  import importlib.resources
14
- import json
15
14
  import os
16
15
  from collections.abc import Iterable
17
16
  from collections.abc import Mapping
18
17
  from pathlib import Path
19
18
  from typing import Any
19
+ from typing import Final
20
20
 
21
21
  from jinja2 import DictLoader
22
22
  from jinja2 import Environment
23
23
  from jinja2 import FileSystemLoader
24
24
  from jinja2 import StrictUndefined
25
25
 
26
+ from ..file import File
27
+
26
28
  # SECTION: EXPORTS ========================================================== #
27
29
 
28
30
 
@@ -31,13 +33,26 @@ __all__ = [
31
33
  'load_table_spec',
32
34
  'render_table_sql',
33
35
  'render_tables',
36
+ 'render_tables_to_string',
34
37
  ]
35
38
 
36
39
 
40
+ # SECTION: INTERNAL CONSTANTS =============================================== #
41
+
42
+
43
+ _SUPPORTED_SPEC_SUFFIXES: Final[frozenset[str]] = frozenset(
44
+ {
45
+ '.json',
46
+ '.yml',
47
+ '.yaml',
48
+ },
49
+ )
50
+
51
+
37
52
  # SECTION: CONSTANTS ======================================================== #
38
53
 
39
54
 
40
- TEMPLATES = {
55
+ TEMPLATES: Final[dict[str, str]] = {
41
56
  'ddl': 'ddl.sql.j2',
42
57
  'view': 'view.sql.j2',
43
58
  }
@@ -46,12 +61,68 @@ TEMPLATES = {
46
61
  # SECTION: INTERNAL FUNCTIONS =============================================== #
47
62
 
48
63
 
49
- def _build_env(
64
+ def _load_template_text(
65
+ filename: str,
66
+ ) -> str:
67
+ """Return the bundled template text.
68
+
69
+ Parameters
70
+ ----------
71
+ filename : str
72
+ Template filename located inside the package data folder.
73
+
74
+ Returns
75
+ -------
76
+ str
77
+ Raw template contents.
78
+
79
+ Raises
80
+ ------
81
+ FileNotFoundError
82
+ If the template file cannot be located in package data.
83
+ """
84
+
85
+ try:
86
+ return (
87
+ importlib.resources.files(
88
+ 'etlplus.templates',
89
+ )
90
+ .joinpath(filename)
91
+ .read_text(encoding='utf-8')
92
+ )
93
+ except FileNotFoundError as exc: # pragma: no cover - deployment guard
94
+ raise FileNotFoundError(
95
+ f'Could not load template {filename} '
96
+ f'from etlplus.templates package data.',
97
+ ) from exc
98
+
99
+
100
+ def _resolve_template(
50
101
  *,
51
102
  template_key: str | None,
52
103
  template_path: str | None,
53
- ) -> Environment:
54
- """Return a Jinja2 environment using a built-in or file template."""
104
+ ) -> tuple[Environment, str]:
105
+ """Return environment and template name for rendering.
106
+
107
+ Parameters
108
+ ----------
109
+ template_key : str | None
110
+ Named template key bundled with the package.
111
+ template_path : str | None
112
+ Explicit template file override.
113
+
114
+ Returns
115
+ -------
116
+ tuple[Environment, str]
117
+ Pair of configured Jinja environment and the template identifier.
118
+
119
+ Raises
120
+ ------
121
+ FileNotFoundError
122
+ If the provided template path does not exist.
123
+ ValueError
124
+ If the template key is unknown.
125
+ """
55
126
  file_override = template_path or os.environ.get('TEMPLATE_NAME')
56
127
  if file_override:
57
128
  path = Path(file_override)
@@ -64,8 +135,7 @@ def _build_env(
64
135
  trim_blocks=True,
65
136
  lstrip_blocks=True,
66
137
  )
67
- env.globals['TEMPLATE_NAME'] = path.name
68
- return env
138
+ return env, path.name
69
139
 
70
140
  key = (template_key or 'ddl').strip()
71
141
  if key not in TEMPLATES:
@@ -84,51 +154,59 @@ def _build_env(
84
154
  trim_blocks=True,
85
155
  lstrip_blocks=True,
86
156
  )
87
- env.globals['TEMPLATE_NAME'] = key
88
- return env
157
+ return env, key
89
158
 
90
159
 
91
- def _load_template_text(filename: str) -> str:
92
- """Return the raw template text bundled with the package."""
160
+ # SECTION: FUNCTIONS ======================================================== #
93
161
 
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
162
 
163
+ def load_table_spec(
164
+ path: Path | str,
165
+ ) -> dict[str, Any]:
166
+ """
167
+ Load a table specification from disk.
108
168
 
109
- # SECTION: FUNCTIONS ======================================================== #
169
+ Parameters
170
+ ----------
171
+ path : Path | str
172
+ Path to the JSON or YAML specification file.
110
173
 
174
+ Returns
175
+ -------
176
+ dict[str, Any]
177
+ Parsed table specification mapping.
111
178
 
112
- def load_table_spec(path: Path | str) -> dict[str, Any]:
113
- """Load a table spec from JSON or YAML."""
179
+ Raises
180
+ ------
181
+ ImportError
182
+ If the file cannot be read due to missing dependencies.
183
+ RuntimeError
184
+ If the YAML dependency is missing for YAML specs.
185
+ TypeError
186
+ If the loaded spec is not a mapping.
187
+ ValueError
188
+ If the file suffix is not supported.
189
+ """
114
190
 
115
191
  spec_path = Path(path)
116
- text = spec_path.read_text(encoding='utf-8')
117
192
  suffix = spec_path.suffix.lower()
118
193
 
119
- if suffix == '.json':
120
- return json.loads(text)
194
+ if suffix not in _SUPPORTED_SPEC_SUFFIXES:
195
+ raise ValueError('Spec must be .json, .yml, or .yaml')
121
196
 
122
- if suffix in {'.yml', '.yaml'}:
123
- try:
124
- import yaml # type: ignore
125
- except Exception as exc: # pragma: no cover
197
+ try:
198
+ spec = File.read_file(spec_path)
199
+ except ImportError as e:
200
+ if suffix in {'.yml', '.yaml'}:
126
201
  raise RuntimeError(
127
202
  'Missing dependency: pyyaml is required for YAML specs.',
128
- ) from exc
129
- return yaml.safe_load(text)
203
+ ) from e
204
+ raise
130
205
 
131
- raise ValueError('Spec must be .json, .yml, or .yaml')
206
+ if not isinstance(spec, Mapping):
207
+ raise TypeError('Table spec must be a mapping')
208
+
209
+ return dict(spec)
132
210
 
133
211
 
134
212
  def render_table_sql(
@@ -153,16 +231,11 @@ def render_table_sql(
153
231
  -------
154
232
  str
155
233
  Rendered SQL string.
156
-
157
- Raises
158
- ------
159
- TypeError
160
- If the loaded template name is not a string.
161
234
  """
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.')
235
+ env, template_name = _resolve_template(
236
+ template_key=template,
237
+ template_path=template_path,
238
+ )
166
239
  tmpl = env.get_template(template_name)
167
240
  return tmpl.render(spec=spec).rstrip() + '\n'
168
241
 
@@ -195,3 +268,44 @@ def render_tables(
195
268
  render_table_sql(spec, template=template, template_path=template_path)
196
269
  for spec in specs
197
270
  ]
271
+
272
+
273
+ def render_tables_to_string(
274
+ spec_paths: Iterable[Path | str],
275
+ *,
276
+ template: str | None = 'ddl',
277
+ template_path: Path | str | None = None,
278
+ ) -> str:
279
+ """
280
+ Render one or more specs and concatenate the SQL payloads.
281
+
282
+ Parameters
283
+ ----------
284
+ spec_paths : Iterable[Path | str]
285
+ Paths to table specification files.
286
+ template : str | None, optional
287
+ Template key bundled with ETLPlus. Defaults to ``'ddl'``.
288
+ template_path : Path | str | None, optional
289
+ Custom Jinja template to override the bundled templates.
290
+
291
+ Returns
292
+ -------
293
+ str
294
+ Concatenated SQL payload suitable for writing to disk or stdout.
295
+ """
296
+
297
+ resolved_template_path = (
298
+ str(template_path) if template_path is not None else None
299
+ )
300
+ rendered_sql: list[str] = []
301
+ for spec_path in spec_paths:
302
+ spec = load_table_spec(spec_path)
303
+ rendered_sql.append(
304
+ render_table_sql(
305
+ spec,
306
+ template=template,
307
+ template_path=resolved_template_path,
308
+ ),
309
+ )
310
+
311
+ return ''.join(rendered_sql)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.5.5
3
+ Version: 0.6.1
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -1,7 +1,6 @@
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
5
4
  etlplus/enums.py,sha256=V_j18Ud2BCXpFsBk2pZGrvCVrvAMJ7uja1z9fppFGso,10175
6
5
  etlplus/extract.py,sha256=f44JdHhNTACxgn44USx05paKTwq7LQY-V4wANCW9hVM,6173
7
6
  etlplus/file.py,sha256=RxIAsGDN4f_vNA2B5-ct88JNd_ISAyYbooIRE5DstS8,17972
@@ -33,7 +32,7 @@ etlplus/api/rate_limiting/config.py,sha256=2b4wIynblN-1EyMqI4aXa71SljzSjXYh5N1Nn
33
32
  etlplus/api/rate_limiting/rate_limiter.py,sha256=Uxozqd_Ej5Lsj-M-mLT2WexChgWh7x35_YP10yqYPQA,7159
34
33
  etlplus/cli/__init__.py,sha256=J97-Rv931IL1_b4AXnB7Fbbd7HKnHBpx18NQfC_kE6c,299
35
34
  etlplus/cli/app.py,sha256=SYPO-NDwXgymJrACw39jZ_NJrSKAs0O8anuWR5o42WM,35893
36
- etlplus/cli/handlers.py,sha256=ZPoV9N48mtpUMf4S_czAKpo4ZqLFVb4tcTq0B36v-84,18941
35
+ etlplus/cli/handlers.py,sha256=nFMvqHQhJ8kJZPisDCiUHeOhjlqAO6hJvRjXiJTcU74,18951
37
36
  etlplus/cli/main.py,sha256=ijYOy72SEsxrTEBan5yADW8CZyr0yddVF8HeMgFw6Zg,16576
38
37
  etlplus/config/__init__.py,sha256=VZWzOg7d2YR9NT6UwKTv44yf2FRUMjTHynkm1Dl5Qzo,1486
39
38
  etlplus/config/connector.py,sha256=0-TIwevHbKRHVmucvyGpPd-3tB1dKHB-dj0yJ6kq5eY,9809
@@ -42,14 +41,16 @@ etlplus/config/pipeline.py,sha256=Va4MQY6KEyKqHGMKPmh09ZcGpx95br-iNUjpkqtzVbw,95
42
41
  etlplus/config/profile.py,sha256=Ss2zedQGjkaGSpvBLTD4SZaWViMJ7TJPLB8Q2_BTpPg,1898
43
42
  etlplus/config/types.py,sha256=a0epJ3z16HQ5bY3Ctf8s_cQPa3f0HHcwdOcjCP2xoG4,4954
44
43
  etlplus/config/utils.py,sha256=4SUHMkt5bKBhMhiJm-DrnmE2Q4TfOgdNCKz8PJDS27o,3443
44
+ etlplus/database/__init__.py,sha256=MNaqpiPNTMbwbHxdh865GXS3q4H4dkAn2YLl3GFQU8E,525
45
+ etlplus/database/ddl.py,sha256=Hvg1PwwaIMU3y8emMWml4CzvQGvvg6KZfsHo3-EWJjg,7738
45
46
  etlplus/templates/__init__.py,sha256=tsniN7XJYs3NwYxJ6c2HD5upHP3CDkLx-bQCMt97UOM,106
46
47
  etlplus/templates/ddl.sql.j2,sha256=s8fMWvcb4eaJVXkifuib1aQPljtZ8buuyB_uA-ZdU3Q,4734
47
48
  etlplus/templates/view.sql.j2,sha256=Iy8DHfhq5yyvrUKDxqp_aHIEXY4Tm6j4wT7YDEFWAhk,2180
48
49
  etlplus/validation/__init__.py,sha256=Pe5Xg1_EA4uiNZGYu5WTF3j7odjmyxnAJ8rcioaplSQ,1254
49
50
  etlplus/validation/utils.py,sha256=Mtqg449VIke0ziy_wd2r6yrwJzQkA1iulZC87FzXMjo,10201
50
- etlplus-0.5.5.dist-info/licenses/LICENSE,sha256=MuNO63i6kWmgnV2pbP2SLqP54mk1BGmu7CmbtxMmT-U,1069
51
- etlplus-0.5.5.dist-info/METADATA,sha256=I6_4VqX4PBWV2T5KYctjcoL9aCwp6Iixc9IZhgLUKFQ,19288
52
- etlplus-0.5.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- etlplus-0.5.5.dist-info/entry_points.txt,sha256=6w-2-jzuPa55spzK34h-UKh2JTEShh38adFRONNP9QE,45
54
- etlplus-0.5.5.dist-info/top_level.txt,sha256=aWWF-udn_sLGuHTM6W6MLh99ArS9ROkUWO8Mi8y1_2U,8
55
- etlplus-0.5.5.dist-info/RECORD,,
51
+ etlplus-0.6.1.dist-info/licenses/LICENSE,sha256=MuNO63i6kWmgnV2pbP2SLqP54mk1BGmu7CmbtxMmT-U,1069
52
+ etlplus-0.6.1.dist-info/METADATA,sha256=NX1ADs4_MZ3J9QLGj7bPRyXc43HJJxfOzfcmIg3VYHc,19288
53
+ etlplus-0.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
+ etlplus-0.6.1.dist-info/entry_points.txt,sha256=6w-2-jzuPa55spzK34h-UKh2JTEShh38adFRONNP9QE,45
55
+ etlplus-0.6.1.dist-info/top_level.txt,sha256=aWWF-udn_sLGuHTM6W6MLh99ArS9ROkUWO8Mi8y1_2U,8
56
+ etlplus-0.6.1.dist-info/RECORD,,