etlplus 0.5.5__tar.gz → 0.7.0__tar.gz
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-0.5.5/etlplus.egg-info → etlplus-0.7.0}/PKG-INFO +4 -1
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/cli/handlers.py +2 -2
- etlplus-0.7.0/etlplus/database/__init__.py +42 -0
- {etlplus-0.5.5/etlplus → etlplus-0.7.0/etlplus/database}/ddl.py +161 -47
- etlplus-0.7.0/etlplus/database/engine.py +146 -0
- etlplus-0.7.0/etlplus/database/orm.py +347 -0
- etlplus-0.7.0/etlplus/database/schema.py +273 -0
- {etlplus-0.5.5 → etlplus-0.7.0/etlplus.egg-info}/PKG-INFO +4 -1
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus.egg-info/SOURCES.txt +9 -1
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus.egg-info/requires.txt +3 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/pyproject.toml +3 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/setup.py +3 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/cli/test_u_cli_app.py +1 -1
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/cli/test_u_cli_handlers.py +1 -1
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/cli/test_u_cli_main.py +1 -1
- etlplus-0.7.0/tests/unit/database/test_u_database_ddl.py +265 -0
- etlplus-0.7.0/tests/unit/database/test_u_database_engine.py +147 -0
- etlplus-0.7.0/tests/unit/database/test_u_database_orm.py +308 -0
- etlplus-0.7.0/tests/unit/database/test_u_database_schema.py +213 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.coveragerc +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.editorconfig +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.gitattributes +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.github/actions/python-bootstrap/action.yml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.github/workflows/ci.yml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.gitignore +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.pre-commit-config.yaml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/.ruff.toml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/CODE_OF_CONDUCT.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/CONTRIBUTING.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/DEMO.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/LICENSE +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/MANIFEST.in +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/Makefile +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/README.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/REFERENCES.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/docs/pipeline-guide.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/docs/snippets/installation_version.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/__main__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/__version__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/README.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/auth.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/endpoint_client.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/errors.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/pagination/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/pagination/client.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/pagination/config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/pagination/paginator.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/rate_limiting/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/rate_limiting/config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/request_manager.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/retry_manager.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/transport.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/api/types.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/cli/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/cli/app.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/cli/main.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/connector.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/jobs.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/pipeline.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/profile.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/types.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/config/utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/enums.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/extract.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/file.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/load.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/mixins.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/py.typed +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/run.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/run_helpers.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/templates/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/templates/ddl.sql.j2 +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/templates/view.sql.j2 +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/transform.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/types.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/validate.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/validation/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus/validation/utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus.egg-info/dependency_links.txt +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus.egg-info/entry_points.txt +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/etlplus.egg-info/top_level.txt +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/README.md +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/configs/ddl_spec.yml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/configs/pipeline.yml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/data/sample.csv +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/data/sample.json +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/data/sample.xml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/data/sample.xsd +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/data/sample.yaml +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/examples/quickstart_python.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/pytest.ini +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/setup.cfg +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/__init__.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/conftest.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/conftest.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_cli.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_examples_data_parity.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_pagination_strategy.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_pipeline_smoke.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_run.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/conftest.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_auth.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_endpoint_client.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_mocks.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_pagination_client.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_pagination_config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_paginator.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_rate_limit_config.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_rate_limiter.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_request_manager.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_retry_manager.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_transport.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/api/test_u_types.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/cli/conftest.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/config/test_u_config_utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/config/test_u_connector.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/config/test_u_jobs.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/config/test_u_pipeline.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/conftest.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_enums.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_extract.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_file.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_load.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_main.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_mixins.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_run.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_run_helpers.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_transform.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_validate.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/test_u_version.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tests/unit/validation/test_u_validation_utils.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tools/run_pipeline.py +0 -0
- {etlplus-0.5.5 → etlplus-0.7.0}/tools/update_demo_snippets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etlplus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -21,7 +21,10 @@ Requires-Dist: jinja2>=3.1.6
|
|
|
21
21
|
Requires-Dist: pyodbc>=5.3.0
|
|
22
22
|
Requires-Dist: python-dotenv>=1.2.1
|
|
23
23
|
Requires-Dist: pandas>=2.3.3
|
|
24
|
+
Requires-Dist: pydantic>=2.12.5
|
|
25
|
+
Requires-Dist: PyYAML>=6.0.3
|
|
24
26
|
Requires-Dist: requests>=2.32.5
|
|
27
|
+
Requires-Dist: SQLAlchemy>=2.0.45
|
|
25
28
|
Requires-Dist: typer>=0.21.0
|
|
26
29
|
Provides-Extra: dev
|
|
27
30
|
Requires-Dist: black>=25.9.0; extra == "dev"
|
|
@@ -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 ..
|
|
22
|
-
from ..
|
|
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,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
|
+
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
:mod:`etlplus.ddl` module.
|
|
2
|
+
:mod:`etlplus.database.ddl` module.
|
|
3
3
|
|
|
4
4
|
DDL rendering utilities for pipeline table schemas.
|
|
5
5
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
88
|
-
return env
|
|
157
|
+
return env, key
|
|
89
158
|
|
|
90
159
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
120
|
-
|
|
194
|
+
if suffix not in _SUPPORTED_SPEC_SUFFIXES:
|
|
195
|
+
raise ValueError('Spec must be .json, .yml, or .yaml')
|
|
121
196
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
129
|
-
|
|
203
|
+
) from e
|
|
204
|
+
raise
|
|
130
205
|
|
|
131
|
-
|
|
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 =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.database.engine` module.
|
|
3
|
+
|
|
4
|
+
Lightweight engine/session factory with optional config-driven URL loading.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from sqlalchemy import create_engine
|
|
15
|
+
from sqlalchemy.engine import Engine
|
|
16
|
+
from sqlalchemy.orm import sessionmaker
|
|
17
|
+
|
|
18
|
+
from ..file import File
|
|
19
|
+
|
|
20
|
+
# SECTION: EXPORTS ========================================================== #
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Functions
|
|
25
|
+
'load_database_url_from_config',
|
|
26
|
+
'make_engine',
|
|
27
|
+
# Singletons
|
|
28
|
+
'engine',
|
|
29
|
+
'session',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# SECTION: INTERNAL CONSTANTS =============================================== #
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DATABASE_URL: str = (
|
|
37
|
+
os.getenv('DATABASE_URL')
|
|
38
|
+
or os.getenv('DATABASE_DSN')
|
|
39
|
+
or 'sqlite+pysqlite:///:memory:'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_url_from_mapping(cfg: Mapping[str, Any]) -> str | None:
|
|
47
|
+
"""
|
|
48
|
+
Return a URL/DSN from a mapping if present.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
cfg : Mapping[str, Any]
|
|
53
|
+
Configuration mapping potentially containing connection fields.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
str | None
|
|
58
|
+
Resolved URL/DSN string, if present.
|
|
59
|
+
"""
|
|
60
|
+
conn = cfg.get('connection_string') or cfg.get('url') or cfg.get('dsn')
|
|
61
|
+
if isinstance(conn, str) and conn.strip():
|
|
62
|
+
return conn.strip()
|
|
63
|
+
|
|
64
|
+
# Some configs nest defaults.
|
|
65
|
+
# E.g., databases: { mssql: { default: {...} } }
|
|
66
|
+
default_cfg = cfg.get('default')
|
|
67
|
+
if isinstance(default_cfg, Mapping):
|
|
68
|
+
return _resolve_url_from_mapping(default_cfg)
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# SECTION: FUNCTIONS ======================================================== #
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_database_url_from_config(
|
|
77
|
+
path: str | Path,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Extract a database URL/DSN from a YAML/JSON config file.
|
|
83
|
+
|
|
84
|
+
The loader is schema-tolerant: it looks for a top-level "databases" map
|
|
85
|
+
and then for a named entry (``name``). Each entry may contain either a
|
|
86
|
+
``connection_string``/``url``/``dsn`` or a nested ``default`` block with
|
|
87
|
+
those fields.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
path : str | Path
|
|
92
|
+
Location of the configuration file.
|
|
93
|
+
name : str | None, optional
|
|
94
|
+
Named database entry under the ``databases`` map (default:
|
|
95
|
+
``default``).
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
str
|
|
100
|
+
Resolved database URL/DSN string.
|
|
101
|
+
|
|
102
|
+
Raises
|
|
103
|
+
------
|
|
104
|
+
KeyError
|
|
105
|
+
If the specified database entry is not found.
|
|
106
|
+
TypeError
|
|
107
|
+
If the config structure is invalid.
|
|
108
|
+
ValueError
|
|
109
|
+
If no connection string/URL/DSN is found for the specified entry.
|
|
110
|
+
"""
|
|
111
|
+
cfg = File.read_file(Path(path))
|
|
112
|
+
if not isinstance(cfg, Mapping):
|
|
113
|
+
raise TypeError('Database config must be a mapping')
|
|
114
|
+
|
|
115
|
+
databases = cfg.get('databases') if isinstance(cfg, Mapping) else None
|
|
116
|
+
if not isinstance(databases, Mapping):
|
|
117
|
+
raise KeyError('Config missing top-level "databases" mapping')
|
|
118
|
+
|
|
119
|
+
target = name or 'default'
|
|
120
|
+
entry = databases.get(target)
|
|
121
|
+
if entry is None:
|
|
122
|
+
raise KeyError(f'Database entry "{target}" not found in config')
|
|
123
|
+
if not isinstance(entry, Mapping):
|
|
124
|
+
raise TypeError(f'Database entry "{target}" must be a mapping')
|
|
125
|
+
|
|
126
|
+
url = _resolve_url_from_mapping(entry)
|
|
127
|
+
if not url:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f'Database entry "{target}" lacks connection_string/url/dsn',
|
|
130
|
+
)
|
|
131
|
+
return url
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def make_engine(url: str | None = None, **engine_kwargs: Any) -> Engine:
|
|
135
|
+
"""Create a SQLAlchemy Engine, defaulting to env config if no URL given."""
|
|
136
|
+
|
|
137
|
+
resolved_url = url or DATABASE_URL
|
|
138
|
+
return create_engine(resolved_url, pool_pre_ping=True, **engine_kwargs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# SECTION: SINGLETONS ======================================================= #
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Default engine/session for callers that rely on module-level singletons.
|
|
145
|
+
engine = make_engine()
|
|
146
|
+
session = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|