etlplus 0.15.0__py3-none-any.whl → 0.16.6__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/README.md +25 -3
- etlplus/__init__.py +2 -0
- etlplus/api/README.md +31 -0
- etlplus/api/__init__.py +14 -14
- etlplus/api/auth.py +10 -7
- etlplus/api/config.py +8 -13
- etlplus/api/endpoint_client.py +20 -20
- etlplus/api/errors.py +4 -4
- etlplus/api/pagination/__init__.py +6 -6
- etlplus/api/pagination/config.py +12 -10
- etlplus/api/pagination/paginator.py +6 -7
- etlplus/api/rate_limiting/__init__.py +2 -2
- etlplus/api/rate_limiting/config.py +14 -14
- etlplus/api/rate_limiting/rate_limiter.py +3 -3
- etlplus/api/request_manager.py +4 -4
- etlplus/api/retry_manager.py +8 -8
- etlplus/api/transport.py +11 -11
- etlplus/api/types.py +131 -11
- etlplus/api/utils.py +50 -50
- etlplus/cli/commands.py +93 -60
- etlplus/cli/constants.py +1 -1
- etlplus/cli/handlers.py +43 -26
- etlplus/cli/io.py +2 -2
- etlplus/cli/main.py +2 -2
- etlplus/cli/state.py +4 -7
- etlplus/{workflow/pipeline.py → config.py} +62 -99
- etlplus/connector/__init__.py +43 -0
- etlplus/connector/api.py +161 -0
- etlplus/connector/connector.py +26 -0
- etlplus/connector/core.py +132 -0
- etlplus/connector/database.py +122 -0
- etlplus/connector/enums.py +52 -0
- etlplus/connector/file.py +120 -0
- etlplus/connector/types.py +40 -0
- etlplus/connector/utils.py +122 -0
- etlplus/database/ddl.py +2 -2
- etlplus/database/engine.py +19 -3
- etlplus/database/orm.py +2 -0
- etlplus/enums.py +36 -200
- etlplus/file/_imports.py +1 -0
- etlplus/file/_io.py +52 -4
- etlplus/file/accdb.py +3 -2
- etlplus/file/arrow.py +3 -2
- etlplus/file/avro.py +3 -2
- etlplus/file/bson.py +3 -2
- etlplus/file/cbor.py +3 -2
- etlplus/file/cfg.py +3 -2
- etlplus/file/conf.py +3 -2
- etlplus/file/core.py +11 -8
- etlplus/file/csv.py +3 -2
- etlplus/file/dat.py +3 -2
- etlplus/file/dta.py +3 -2
- etlplus/file/duckdb.py +3 -2
- etlplus/file/enums.py +1 -1
- etlplus/file/feather.py +3 -2
- etlplus/file/fwf.py +3 -2
- etlplus/file/gz.py +3 -2
- etlplus/file/hbs.py +3 -2
- etlplus/file/hdf5.py +3 -2
- etlplus/file/ini.py +3 -2
- etlplus/file/ion.py +3 -2
- etlplus/file/jinja2.py +3 -2
- etlplus/file/json.py +5 -16
- etlplus/file/log.py +3 -2
- etlplus/file/mat.py +3 -2
- etlplus/file/mdb.py +3 -2
- etlplus/file/msgpack.py +3 -2
- etlplus/file/mustache.py +3 -2
- etlplus/file/nc.py +3 -2
- etlplus/file/ndjson.py +3 -2
- etlplus/file/numbers.py +3 -2
- etlplus/file/ods.py +3 -2
- etlplus/file/orc.py +3 -2
- etlplus/file/parquet.py +3 -2
- etlplus/file/pb.py +3 -2
- etlplus/file/pbf.py +3 -2
- etlplus/file/properties.py +3 -2
- etlplus/file/proto.py +3 -2
- etlplus/file/psv.py +3 -2
- etlplus/file/rda.py +3 -2
- etlplus/file/rds.py +3 -2
- etlplus/file/sas7bdat.py +3 -2
- etlplus/file/sav.py +3 -2
- etlplus/file/sqlite.py +3 -2
- etlplus/file/stub.py +1 -0
- etlplus/file/sylk.py +3 -2
- etlplus/file/tab.py +3 -2
- etlplus/file/toml.py +3 -2
- etlplus/file/tsv.py +3 -2
- etlplus/file/txt.py +4 -3
- etlplus/file/vm.py +3 -2
- etlplus/file/wks.py +3 -2
- etlplus/file/xls.py +3 -2
- etlplus/file/xlsm.py +3 -2
- etlplus/file/xlsx.py +3 -2
- etlplus/file/xml.py +9 -3
- etlplus/file/xpt.py +3 -2
- etlplus/file/yaml.py +5 -16
- etlplus/file/zip.py +3 -2
- etlplus/file/zsav.py +3 -2
- etlplus/ops/__init__.py +1 -0
- etlplus/ops/enums.py +173 -0
- etlplus/ops/extract.py +222 -23
- etlplus/ops/load.py +155 -36
- etlplus/ops/run.py +92 -107
- etlplus/ops/transform.py +48 -29
- etlplus/ops/types.py +147 -0
- etlplus/ops/utils.py +11 -40
- etlplus/ops/validate.py +16 -16
- etlplus/types.py +6 -102
- etlplus/utils.py +163 -29
- etlplus/workflow/README.md +0 -24
- etlplus/workflow/__init__.py +2 -15
- etlplus/workflow/dag.py +23 -1
- etlplus/workflow/jobs.py +83 -39
- etlplus/workflow/profile.py +4 -2
- {etlplus-0.15.0.dist-info → etlplus-0.16.6.dist-info}/METADATA +4 -4
- etlplus-0.16.6.dist-info/RECORD +143 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.6.dist-info}/WHEEL +1 -1
- etlplus/config/README.md +0 -50
- etlplus/config/__init__.py +0 -33
- etlplus/config/types.py +0 -140
- etlplus/dag.py +0 -103
- etlplus/workflow/connector.py +0 -373
- etlplus/workflow/types.py +0 -115
- etlplus/workflow/utils.py +0 -120
- etlplus-0.15.0.dist-info/RECORD +0 -139
- {etlplus-0.15.0.dist-info → etlplus-0.16.6.dist-info}/entry_points.txt +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.6.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.15.0.dist-info → etlplus-0.16.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.connector` module.
|
|
3
|
+
|
|
4
|
+
Compatibility re-exports for connector configuration classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .api import ConnectorApi
|
|
10
|
+
from .database import ConnectorDb
|
|
11
|
+
from .file import ConnectorFile
|
|
12
|
+
|
|
13
|
+
# SECTION: EXPORTS ========================================================== #
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Type aliases
|
|
18
|
+
'Connector',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# SECTION: TYPED ALIASES ==================================================== #
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Type alias representing any supported connector
|
|
26
|
+
type Connector = ConnectorApi | ConnectorDb | ConnectorFile
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.core` module.
|
|
3
|
+
|
|
4
|
+
Protocols and base classes for connector implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC
|
|
10
|
+
from abc import abstractmethod
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
from typing import Self
|
|
14
|
+
from typing import runtime_checkable
|
|
15
|
+
|
|
16
|
+
from ..types import StrAnyMap
|
|
17
|
+
from .types import ConnectorType
|
|
18
|
+
|
|
19
|
+
# SECTION: EXPORTS ========================================================== #
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'ConnectorBase',
|
|
24
|
+
'ConnectorProtocol',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# SECTION: PROTOCOLS ======================================================== #
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class ConnectorProtocol(Protocol):
|
|
33
|
+
"""
|
|
34
|
+
Structural contract for connector implementations.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
name : str
|
|
39
|
+
Unique connector name.
|
|
40
|
+
type : ConnectorType
|
|
41
|
+
Connector kind.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# -- Attributes -- #
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
type: ConnectorType
|
|
48
|
+
|
|
49
|
+
# -- Class Methods -- #
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_obj(cls, obj: StrAnyMap) -> Self:
|
|
53
|
+
"""
|
|
54
|
+
Parse a mapping into a connector instance.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
obj : StrAnyMap
|
|
59
|
+
Mapping with at least ``name``.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
Self
|
|
64
|
+
Parsed connector instance.
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# -- Internal Static Methods -- #
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _require_name(obj: StrAnyMap, *, kind: str) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Extract and validate the ``name`` field from connector mappings.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
obj : StrAnyMap
|
|
78
|
+
Connector mapping with a ``name`` entry.
|
|
79
|
+
kind : str
|
|
80
|
+
Connector kind used in the error message.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
str
|
|
85
|
+
Valid connector name.
|
|
86
|
+
|
|
87
|
+
Raises
|
|
88
|
+
------
|
|
89
|
+
TypeError
|
|
90
|
+
If ``name`` is missing or not a string.
|
|
91
|
+
"""
|
|
92
|
+
name = obj.get('name')
|
|
93
|
+
if not isinstance(name, str):
|
|
94
|
+
raise TypeError(f'Connector{kind} requires a "name" (str)')
|
|
95
|
+
return name
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# SECTION: ABSTRACT BASE DATA CLASSES ======================================= #
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(kw_only=True, slots=True)
|
|
102
|
+
class ConnectorBase(ABC, ConnectorProtocol):
|
|
103
|
+
"""
|
|
104
|
+
Abstract base class for connector implementations.
|
|
105
|
+
|
|
106
|
+
Attributes
|
|
107
|
+
----------
|
|
108
|
+
name : str
|
|
109
|
+
Unique connector name.
|
|
110
|
+
type : ConnectorType
|
|
111
|
+
Connector kind.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
type: ConnectorType
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def from_obj(cls, obj: StrAnyMap) -> Self:
|
|
120
|
+
"""
|
|
121
|
+
Parse a mapping into a connector instance.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
obj : StrAnyMap
|
|
126
|
+
Mapping with at least ``name``.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Self
|
|
131
|
+
Parsed connector instance.
|
|
132
|
+
"""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.database` module.
|
|
3
|
+
|
|
4
|
+
Database connector configuration dataclass.
|
|
5
|
+
|
|
6
|
+
Notes
|
|
7
|
+
-----
|
|
8
|
+
- TypedDicts in this module are intentionally ``total=False`` and are not
|
|
9
|
+
enforced at runtime.
|
|
10
|
+
- :meth:`*.from_obj` constructors accept :class:`Mapping[str, Any]` and perform
|
|
11
|
+
tolerant parsing and light casting. This keeps the runtime permissive while
|
|
12
|
+
improving autocomplete and static analysis for contributors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Self
|
|
19
|
+
from typing import TypedDict
|
|
20
|
+
from typing import overload
|
|
21
|
+
|
|
22
|
+
from ..types import StrAnyMap
|
|
23
|
+
from .core import ConnectorBase
|
|
24
|
+
from .enums import DataConnectorType
|
|
25
|
+
from .types import ConnectorType
|
|
26
|
+
|
|
27
|
+
# SECTION: EXPORTS ========================================================== #
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'ConnectorDb',
|
|
32
|
+
'ConnectorDbConfigDict',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConnectorDbConfigDict(TypedDict, total=False):
|
|
40
|
+
"""
|
|
41
|
+
Shape accepted by :meth:`ConnectorDb.from_obj` (all keys optional).
|
|
42
|
+
|
|
43
|
+
See Also
|
|
44
|
+
--------
|
|
45
|
+
- :meth:`etlplus.connector.database.ConnectorDb.from_obj`
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
type: ConnectorType
|
|
50
|
+
connection_string: str
|
|
51
|
+
query: str
|
|
52
|
+
table: str
|
|
53
|
+
mode: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# SECTION: DATA CLASSES ===================================================== #
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(kw_only=True, slots=True)
|
|
60
|
+
class ConnectorDb(ConnectorBase):
|
|
61
|
+
"""
|
|
62
|
+
Configuration for a database-based data connector.
|
|
63
|
+
|
|
64
|
+
Attributes
|
|
65
|
+
----------
|
|
66
|
+
type : ConnectorType
|
|
67
|
+
Connector kind, always ``'database'``.
|
|
68
|
+
connection_string : str | None
|
|
69
|
+
Connection string/DSN for the database.
|
|
70
|
+
query : str | None
|
|
71
|
+
Query to execute for extraction (optional).
|
|
72
|
+
table : str | None
|
|
73
|
+
Target/source table name (optional).
|
|
74
|
+
mode : str | None
|
|
75
|
+
Load mode hint (e.g., ``'append'``, ``'replace'``) - future use.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# -- Attributes -- #
|
|
79
|
+
|
|
80
|
+
type: ConnectorType = DataConnectorType.DATABASE
|
|
81
|
+
connection_string: str | None = None
|
|
82
|
+
query: str | None = None
|
|
83
|
+
table: str | None = None
|
|
84
|
+
mode: str | None = None # append|replace|upsert (future)
|
|
85
|
+
|
|
86
|
+
# -- Class Methods -- #
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
@overload
|
|
90
|
+
def from_obj(cls, obj: ConnectorDbConfigDict) -> Self: ...
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
@overload
|
|
94
|
+
def from_obj(cls, obj: StrAnyMap) -> Self: ...
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_obj(
|
|
98
|
+
cls,
|
|
99
|
+
obj: StrAnyMap,
|
|
100
|
+
) -> Self:
|
|
101
|
+
"""
|
|
102
|
+
Parse a mapping into a ``ConnectorDb`` instance.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
obj : StrAnyMap
|
|
107
|
+
Mapping with at least ``name``.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Self
|
|
112
|
+
Parsed connector instance.
|
|
113
|
+
"""
|
|
114
|
+
name = cls._require_name(obj, kind='Db')
|
|
115
|
+
|
|
116
|
+
return cls(
|
|
117
|
+
name=name,
|
|
118
|
+
connection_string=obj.get('connection_string'),
|
|
119
|
+
query=obj.get('query'),
|
|
120
|
+
table=obj.get('table'),
|
|
121
|
+
mode=obj.get('mode'),
|
|
122
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.enums` module.
|
|
3
|
+
|
|
4
|
+
Connector enums and helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..enums import CoercibleStrEnum
|
|
10
|
+
from ..types import StrStrMap
|
|
11
|
+
|
|
12
|
+
# SECTION: EXPORTS ========================================================= #
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Enums
|
|
17
|
+
'DataConnectorType',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# SECTION: ENUMS ============================================================ #
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DataConnectorType(CoercibleStrEnum):
|
|
25
|
+
"""Supported data connector types."""
|
|
26
|
+
|
|
27
|
+
# -- Constants -- #
|
|
28
|
+
|
|
29
|
+
API = 'api'
|
|
30
|
+
DATABASE = 'database'
|
|
31
|
+
FILE = 'file'
|
|
32
|
+
|
|
33
|
+
# -- Class Methods -- #
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def aliases(cls) -> StrStrMap:
|
|
37
|
+
"""
|
|
38
|
+
Return a mapping of common aliases for each enum member.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
StrStrMap
|
|
43
|
+
A mapping of alias names to their corresponding enum member names.
|
|
44
|
+
"""
|
|
45
|
+
return {
|
|
46
|
+
'http': 'api',
|
|
47
|
+
'https': 'api',
|
|
48
|
+
'rest': 'api',
|
|
49
|
+
'db': 'database',
|
|
50
|
+
'filesystem': 'file',
|
|
51
|
+
'fs': 'file',
|
|
52
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.file` module.
|
|
3
|
+
|
|
4
|
+
File connector configuration dataclass.
|
|
5
|
+
|
|
6
|
+
Notes
|
|
7
|
+
-----
|
|
8
|
+
- TypedDicts in this module are intentionally ``total=False`` and are not
|
|
9
|
+
enforced at runtime.
|
|
10
|
+
- :meth:`*.from_obj` constructors accept :class:`Mapping[str, Any]` and perform
|
|
11
|
+
tolerant parsing and light casting. This keeps the runtime permissive while
|
|
12
|
+
improving autocomplete and static analysis for contributors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from dataclasses import field
|
|
19
|
+
from typing import Any
|
|
20
|
+
from typing import Self
|
|
21
|
+
from typing import TypedDict
|
|
22
|
+
from typing import overload
|
|
23
|
+
|
|
24
|
+
from ..types import StrAnyMap
|
|
25
|
+
from ..utils import coerce_dict
|
|
26
|
+
from .core import ConnectorBase
|
|
27
|
+
from .enums import DataConnectorType
|
|
28
|
+
from .types import ConnectorType
|
|
29
|
+
|
|
30
|
+
# SECTION: EXPORTS ========================================================== #
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'ConnectorFile',
|
|
35
|
+
'ConnectorFileConfigDict',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConnectorFileConfigDict(TypedDict, total=False):
|
|
43
|
+
"""
|
|
44
|
+
Shape accepted by :meth:`ConnectorFile.from_obj` (all keys optional).
|
|
45
|
+
|
|
46
|
+
See Also
|
|
47
|
+
--------
|
|
48
|
+
- :meth:`etlplus.connector.file.ConnectorFile.from_obj`
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
type: ConnectorType
|
|
53
|
+
format: str
|
|
54
|
+
path: str
|
|
55
|
+
options: StrAnyMap
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# SECTION: DATA CLASSES ===================================================== #
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(kw_only=True, slots=True)
|
|
62
|
+
class ConnectorFile(ConnectorBase):
|
|
63
|
+
"""
|
|
64
|
+
Configuration for a file-based data connector.
|
|
65
|
+
|
|
66
|
+
Attributes
|
|
67
|
+
----------
|
|
68
|
+
type : ConnectorType
|
|
69
|
+
Connector kind, always ``'file'``.
|
|
70
|
+
format : str | None
|
|
71
|
+
File format (e.g., ``'json'``, ``'csv'``).
|
|
72
|
+
path : str | None
|
|
73
|
+
File path or URI.
|
|
74
|
+
options : dict[str, Any]
|
|
75
|
+
Reader/writer format options.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# -- Attributes -- #
|
|
79
|
+
|
|
80
|
+
type: ConnectorType = DataConnectorType.FILE
|
|
81
|
+
format: str | None = None
|
|
82
|
+
path: str | None = None
|
|
83
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
# -- Class Methods -- #
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
@overload
|
|
89
|
+
def from_obj(cls, obj: ConnectorFileConfigDict) -> Self: ...
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
@overload
|
|
93
|
+
def from_obj(cls, obj: StrAnyMap) -> Self: ...
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_obj(
|
|
97
|
+
cls,
|
|
98
|
+
obj: StrAnyMap,
|
|
99
|
+
) -> Self:
|
|
100
|
+
"""
|
|
101
|
+
Parse a mapping into a ``ConnectorFile`` instance.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
obj : StrAnyMap
|
|
106
|
+
Mapping with at least ``name``.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
Self
|
|
111
|
+
Parsed connector instance.
|
|
112
|
+
"""
|
|
113
|
+
name = cls._require_name(obj, kind='File')
|
|
114
|
+
|
|
115
|
+
return cls(
|
|
116
|
+
name=name,
|
|
117
|
+
format=obj.get('format'),
|
|
118
|
+
path=obj.get('path'),
|
|
119
|
+
options=coerce_dict(obj.get('options')),
|
|
120
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.types` module.
|
|
3
|
+
|
|
4
|
+
Connector type aliases for :mod:`etlplus.connector`.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
>>> from etlplus.connector import Connector
|
|
9
|
+
>>> src: Connector = {
|
|
10
|
+
>>> "type": "file",
|
|
11
|
+
>>> "path": "/data/input.csv",
|
|
12
|
+
>>> }
|
|
13
|
+
>>> tgt: Connector = {
|
|
14
|
+
>>> "type": "database",
|
|
15
|
+
>>> "connection_string": "postgresql://user:pass@localhost/db",
|
|
16
|
+
>>> }
|
|
17
|
+
>>> from etlplus.api import RetryPolicyDict
|
|
18
|
+
>>> rp: RetryPolicyDict = {"max_attempts": 3, "backoff": 0.5}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from .enums import DataConnectorType
|
|
26
|
+
|
|
27
|
+
# SECTION: EXPORTS ========================================================= #
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Type Aliases
|
|
32
|
+
'ConnectorType',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# SECTION: TYPE ALIASES ===================================================== #
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Literal type for supported connector kinds (strings or enum members)
|
|
40
|
+
type ConnectorType = DataConnectorType | Literal['api', 'database', 'file']
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.utils` module.
|
|
3
|
+
|
|
4
|
+
Shared connector parsing helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .api import ConnectorApi
|
|
13
|
+
from .connector import Connector
|
|
14
|
+
from .database import ConnectorDb
|
|
15
|
+
from .enums import DataConnectorType
|
|
16
|
+
from .file import ConnectorFile
|
|
17
|
+
|
|
18
|
+
# SECTION: EXPORTS ========================================================== #
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Functions
|
|
23
|
+
'parse_connector',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# SECTION: INTERNAL FUNCTIONS =============================================== #
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _coerce_connector_type(
|
|
31
|
+
obj: Mapping[str, Any],
|
|
32
|
+
) -> DataConnectorType:
|
|
33
|
+
"""
|
|
34
|
+
Normalize and validate the connector ``type`` field.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
obj : Mapping[str, Any]
|
|
39
|
+
Mapping with a ``type`` entry.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
DataConnectorType
|
|
44
|
+
Normalized connector type enum.
|
|
45
|
+
|
|
46
|
+
Raises
|
|
47
|
+
------
|
|
48
|
+
TypeError
|
|
49
|
+
If ``type`` is missing or unsupported.
|
|
50
|
+
"""
|
|
51
|
+
if 'type' not in obj:
|
|
52
|
+
raise TypeError('Connector requires a "type"')
|
|
53
|
+
try:
|
|
54
|
+
return DataConnectorType.coerce(obj.get('type'))
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
allowed = ', '.join(DataConnectorType.choices())
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f'Unsupported connector type: {obj.get("type")!r}. '
|
|
59
|
+
f'Expected one of {allowed}.',
|
|
60
|
+
) from exc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_connector(
|
|
64
|
+
kind: DataConnectorType,
|
|
65
|
+
) -> type[Connector]:
|
|
66
|
+
"""
|
|
67
|
+
Resolve the connector class for the requested kind.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
kind : DataConnectorType
|
|
72
|
+
Connector kind enum.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
type[Connector]
|
|
77
|
+
Connector class corresponding to *kind*.
|
|
78
|
+
"""
|
|
79
|
+
match kind:
|
|
80
|
+
case DataConnectorType.API:
|
|
81
|
+
return ConnectorApi
|
|
82
|
+
case DataConnectorType.DATABASE:
|
|
83
|
+
return ConnectorDb
|
|
84
|
+
case DataConnectorType.FILE:
|
|
85
|
+
return ConnectorFile
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# SECTION: FUNCTIONS ======================================================== #
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_connector(
|
|
92
|
+
obj: Mapping[str, Any],
|
|
93
|
+
) -> Connector:
|
|
94
|
+
"""
|
|
95
|
+
Dispatch to a concrete connector constructor based on ``type``.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
obj : Mapping[str, Any]
|
|
100
|
+
Mapping with at least ``name`` and ``type``.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
Connector
|
|
105
|
+
Concrete connector instance.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
TypeError
|
|
110
|
+
If the mapping is invalid or the connector type is unsupported.
|
|
111
|
+
|
|
112
|
+
Notes
|
|
113
|
+
-----
|
|
114
|
+
Delegates to the tolerant ``from_obj`` constructors for each connector
|
|
115
|
+
kind. Connector types are normalized via
|
|
116
|
+
:class:`etlplus.connector.enums.DataConnectorType`, so common aliases
|
|
117
|
+
(e.g., ``'db'`` or ``'http'``) are accepted.
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(obj, Mapping):
|
|
120
|
+
raise TypeError('Connector configuration must be a mapping.')
|
|
121
|
+
connector_cls = _load_connector(_coerce_connector_type(obj))
|
|
122
|
+
return connector_cls.from_obj(obj)
|
etlplus/database/ddl.py
CHANGED
|
@@ -233,7 +233,7 @@ def render_table_sql(
|
|
|
233
233
|
template : TemplateKey | None, optional
|
|
234
234
|
Template key to use (default: 'ddl').
|
|
235
235
|
template_path : str | None, optional
|
|
236
|
-
Path to a custom template file (overrides
|
|
236
|
+
Path to a custom template file (overrides *template*).
|
|
237
237
|
|
|
238
238
|
Returns
|
|
239
239
|
-------
|
|
@@ -264,7 +264,7 @@ def render_tables(
|
|
|
264
264
|
template : TemplateKey | None, optional
|
|
265
265
|
Template key to use (default: 'ddl').
|
|
266
266
|
template_path : str | None, optional
|
|
267
|
-
Path to a custom template file (overrides
|
|
267
|
+
Path to a custom template file (overrides *template*).
|
|
268
268
|
|
|
269
269
|
Returns
|
|
270
270
|
-------
|
etlplus/database/engine.py
CHANGED
|
@@ -87,7 +87,7 @@ def load_database_url_from_config(
|
|
|
87
87
|
Extract a database URL/DSN from a YAML/JSON config file.
|
|
88
88
|
|
|
89
89
|
The loader is schema-tolerant: it looks for a top-level "databases" map
|
|
90
|
-
and then for a named entry (
|
|
90
|
+
and then for a named entry (*name*). Each entry may contain either a
|
|
91
91
|
``connection_string``/``url``/``dsn`` or a nested ``default`` block with
|
|
92
92
|
those fields.
|
|
93
93
|
|
|
@@ -136,9 +136,25 @@ def load_database_url_from_config(
|
|
|
136
136
|
return url
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def make_engine(
|
|
140
|
-
|
|
139
|
+
def make_engine(
|
|
140
|
+
url: str | None = None,
|
|
141
|
+
**engine_kwargs: Any,
|
|
142
|
+
) -> Engine:
|
|
143
|
+
"""
|
|
144
|
+
Create a SQLAlchemy Engine, defaulting to env config if no URL given.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
url : str | None, optional
|
|
149
|
+
Database URL/DSN string. When omitted, ``DATABASE_URL`` is used.
|
|
150
|
+
**engine_kwargs : Any
|
|
151
|
+
Extra keyword arguments forwarded to ``create_engine``.
|
|
141
152
|
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
Engine
|
|
156
|
+
Configured SQLAlchemy engine instance.
|
|
157
|
+
"""
|
|
142
158
|
resolved_url = url or DATABASE_URL
|
|
143
159
|
return create_engine(resolved_url, pool_pre_ping=True, **engine_kwargs)
|
|
144
160
|
|
etlplus/database/orm.py
CHANGED
|
@@ -201,12 +201,14 @@ def build_models(
|
|
|
201
201
|
) -> ModelRegistry:
|
|
202
202
|
"""
|
|
203
203
|
Build SQLAlchemy ORM models from table specifications.
|
|
204
|
+
|
|
204
205
|
Parameters
|
|
205
206
|
----------
|
|
206
207
|
specs : list[TableSpec]
|
|
207
208
|
List of table specifications.
|
|
208
209
|
base : type[DeclarativeBase], optional
|
|
209
210
|
Base class for the ORM models (default: :class:`Base`).
|
|
211
|
+
|
|
210
212
|
Returns
|
|
211
213
|
-------
|
|
212
214
|
ModelRegistry
|