etlplus 0.14.3__py3-none-any.whl → 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- etlplus/README.md +4 -4
- etlplus/api/README.md +33 -2
- etlplus/api/auth.py +1 -1
- etlplus/api/config.py +5 -10
- etlplus/api/endpoint_client.py +4 -4
- etlplus/api/pagination/config.py +1 -1
- etlplus/api/pagination/paginator.py +6 -7
- etlplus/api/rate_limiting/config.py +4 -4
- etlplus/api/rate_limiting/rate_limiter.py +1 -1
- etlplus/api/retry_manager.py +2 -2
- etlplus/api/transport.py +1 -1
- etlplus/api/types.py +99 -0
- etlplus/api/utils.py +6 -2
- etlplus/cli/README.md +2 -2
- etlplus/cli/commands.py +75 -42
- etlplus/cli/constants.py +1 -1
- etlplus/cli/handlers.py +33 -15
- etlplus/cli/io.py +2 -2
- etlplus/cli/main.py +2 -2
- etlplus/cli/state.py +4 -7
- 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/README.md +2 -2
- etlplus/database/ddl.py +2 -2
- etlplus/database/engine.py +19 -3
- etlplus/database/orm.py +2 -0
- etlplus/enums.py +1 -33
- etlplus/file/README.md +2 -2
- 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/extract.py +13 -1
- etlplus/ops/load.py +15 -2
- etlplus/ops/run.py +18 -13
- etlplus/ops/transform.py +2 -2
- etlplus/ops/utils.py +6 -35
- etlplus/ops/validate.py +3 -3
- etlplus/templates/README.md +2 -2
- etlplus/types.py +3 -2
- etlplus/utils.py +163 -29
- etlplus/{config → workflow}/README.md +6 -6
- etlplus/workflow/__init__.py +32 -0
- etlplus/{dag.py → workflow/dag.py} +6 -4
- etlplus/{config → workflow}/jobs.py +101 -38
- etlplus/{config → workflow}/pipeline.py +59 -51
- etlplus/{config → workflow}/profile.py +8 -5
- {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/METADATA +4 -4
- etlplus-0.16.0.dist-info/RECORD +141 -0
- {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/WHEEL +1 -1
- etlplus/config/__init__.py +0 -56
- etlplus/config/connector.py +0 -372
- etlplus/config/types.py +0 -204
- etlplus/config/utils.py +0 -120
- etlplus-0.14.3.dist-info/RECORD +0 -135
- {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/entry_points.txt +0 -0
- {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/top_level.txt +0 -0
etlplus/cli/handlers.py
CHANGED
|
@@ -14,8 +14,6 @@ from typing import Any
|
|
|
14
14
|
from typing import Literal
|
|
15
15
|
from typing import cast
|
|
16
16
|
|
|
17
|
-
from ..config import PipelineConfig
|
|
18
|
-
from ..config import load_pipeline_config
|
|
19
17
|
from ..database import load_table_spec
|
|
20
18
|
from ..database import render_tables
|
|
21
19
|
from ..file import File
|
|
@@ -28,6 +26,8 @@ from ..ops import validate
|
|
|
28
26
|
from ..ops.validate import FieldRules
|
|
29
27
|
from ..types import JSONData
|
|
30
28
|
from ..types import TemplateKey
|
|
29
|
+
from ..workflow import PipelineConfig
|
|
30
|
+
from ..workflow import load_pipeline_config
|
|
31
31
|
from . import io as cli_io
|
|
32
32
|
|
|
33
33
|
# SECTION: EXPORTS ========================================================== #
|
|
@@ -121,9 +121,12 @@ def _check_sections(
|
|
|
121
121
|
if targets:
|
|
122
122
|
sections['targets'] = [tgt.name for tgt in cfg.targets]
|
|
123
123
|
if transforms:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if isinstance(cfg.transforms, Mapping):
|
|
125
|
+
sections['transforms'] = list(cfg.transforms)
|
|
126
|
+
else:
|
|
127
|
+
sections['transforms'] = [
|
|
128
|
+
getattr(trf, 'name', None) for trf in cfg.transforms
|
|
129
|
+
]
|
|
127
130
|
if not sections:
|
|
128
131
|
sections['jobs'] = _pipeline_summary(cfg)['jobs']
|
|
129
132
|
return sections
|
|
@@ -157,6 +160,29 @@ def _pipeline_summary(
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
|
|
163
|
+
def _write_file_payload(
|
|
164
|
+
payload: JSONData,
|
|
165
|
+
target: str,
|
|
166
|
+
*,
|
|
167
|
+
format_hint: str | None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Write a JSON-like payload to a file path using an optional format hint.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
payload : JSONData
|
|
175
|
+
The structured data to write.
|
|
176
|
+
target : str
|
|
177
|
+
File path to write to.
|
|
178
|
+
format_hint : str | None
|
|
179
|
+
Optional format hint for :class:`FileFormat`.
|
|
180
|
+
"""
|
|
181
|
+
file_path = Path(target)
|
|
182
|
+
file_format = FileFormat.coerce(format_hint) if format_hint else None
|
|
183
|
+
File(file_path, file_format=file_format).write(payload)
|
|
184
|
+
|
|
185
|
+
|
|
160
186
|
# SECTION: FUNCTIONS ======================================================== #
|
|
161
187
|
|
|
162
188
|
|
|
@@ -479,7 +505,7 @@ def run_handler(
|
|
|
479
505
|
Name of the job to run. If not provided, runs the entire pipeline.
|
|
480
506
|
Default is ``None``.
|
|
481
507
|
pipeline : str | None, optional
|
|
482
|
-
Alias for
|
|
508
|
+
Alias for *job*. Default is ``None``.
|
|
483
509
|
pretty : bool, optional
|
|
484
510
|
Whether to pretty-print output. Default is ``True``.
|
|
485
511
|
|
|
@@ -572,15 +598,7 @@ def transform_handler(
|
|
|
572
598
|
|
|
573
599
|
# TODO: Generalize to handle non-file targets.
|
|
574
600
|
if target and target != '-':
|
|
575
|
-
|
|
576
|
-
file_path = Path(target)
|
|
577
|
-
file_format = None
|
|
578
|
-
if target_format is not None:
|
|
579
|
-
try:
|
|
580
|
-
file_format = FileFormat(target_format)
|
|
581
|
-
except ValueError:
|
|
582
|
-
file_format = None # or handle error as appropriate
|
|
583
|
-
File(file_path, file_format=file_format).write(data)
|
|
601
|
+
_write_file_payload(data, target, format_hint=target_format)
|
|
584
602
|
print(f'Data transformed and saved to {target}')
|
|
585
603
|
return 0
|
|
586
604
|
|
etlplus/cli/io.py
CHANGED
|
@@ -71,7 +71,7 @@ def emit_or_write(
|
|
|
71
71
|
success_message: str,
|
|
72
72
|
) -> None:
|
|
73
73
|
"""
|
|
74
|
-
Emit JSON or persist to disk based on
|
|
74
|
+
Emit JSON or persist to disk based on *output_path*.
|
|
75
75
|
|
|
76
76
|
Parameters
|
|
77
77
|
----------
|
|
@@ -122,7 +122,7 @@ def materialize_file_payload(
|
|
|
122
122
|
format_explicit: bool,
|
|
123
123
|
) -> JSONData | object:
|
|
124
124
|
"""
|
|
125
|
-
Return structured payloads when
|
|
125
|
+
Return structured payloads when *source* references a file.
|
|
126
126
|
|
|
127
127
|
Parameters
|
|
128
128
|
----------
|
etlplus/cli/main.py
CHANGED
|
@@ -44,13 +44,13 @@ def _emit_context_help(
|
|
|
44
44
|
Returns
|
|
45
45
|
-------
|
|
46
46
|
bool
|
|
47
|
-
``True`` when help was emitted, ``False`` when
|
|
47
|
+
``True`` when help was emitted, ``False`` when *ctx* was ``None``.
|
|
48
48
|
"""
|
|
49
49
|
if ctx is None:
|
|
50
50
|
return False
|
|
51
51
|
|
|
52
52
|
with contextlib.redirect_stdout(sys.stderr):
|
|
53
|
-
ctx.get_help()
|
|
53
|
+
print(ctx.get_help())
|
|
54
54
|
return True
|
|
55
55
|
|
|
56
56
|
|
etlplus/cli/state.py
CHANGED
|
@@ -15,6 +15,7 @@ from typing import Final
|
|
|
15
15
|
|
|
16
16
|
import typer
|
|
17
17
|
|
|
18
|
+
from ..utils import normalize_str
|
|
18
19
|
from .constants import DATA_CONNECTORS
|
|
19
20
|
|
|
20
21
|
# SECTION: EXPORTS ========================================================== #
|
|
@@ -322,14 +323,10 @@ def validate_choice(
|
|
|
322
323
|
typer.BadParameter
|
|
323
324
|
If the input value is not in the set of valid choices.
|
|
324
325
|
"""
|
|
325
|
-
v = str(value or '')
|
|
326
|
-
normalized_choices = {c
|
|
326
|
+
v = normalize_str(str(value or ''))
|
|
327
|
+
normalized_choices = {normalize_str(c): c for c in choices}
|
|
327
328
|
if v in normalized_choices:
|
|
328
|
-
|
|
329
|
-
for choice in choices:
|
|
330
|
-
if choice.lower() == v:
|
|
331
|
-
return choice
|
|
332
|
-
return v
|
|
329
|
+
return normalized_choices[v]
|
|
333
330
|
allowed = ', '.join(sorted(choices))
|
|
334
331
|
raise typer.BadParameter(
|
|
335
332
|
f"Invalid {label} '{value}'. Choose from: {allowed}",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector` package.
|
|
3
|
+
|
|
4
|
+
Connector configuration types and enums.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .api import ConnectorApi
|
|
10
|
+
from .api import ConnectorApiConfigMap
|
|
11
|
+
from .connector import Connector
|
|
12
|
+
from .core import ConnectorBase
|
|
13
|
+
from .core import ConnectorProtocol
|
|
14
|
+
from .database import ConnectorDb
|
|
15
|
+
from .database import ConnectorDbConfigMap
|
|
16
|
+
from .enums import DataConnectorType
|
|
17
|
+
from .file import ConnectorFile
|
|
18
|
+
from .file import ConnectorFileConfigMap
|
|
19
|
+
from .types import ConnectorType
|
|
20
|
+
from .utils import parse_connector
|
|
21
|
+
|
|
22
|
+
# SECTION: EXPORTS ========================================================== #
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Data Classes
|
|
27
|
+
'ConnectorApi',
|
|
28
|
+
'ConnectorDb',
|
|
29
|
+
'ConnectorFile',
|
|
30
|
+
# Enums
|
|
31
|
+
'DataConnectorType',
|
|
32
|
+
# Functions
|
|
33
|
+
'parse_connector',
|
|
34
|
+
# Type Aliases
|
|
35
|
+
'Connector',
|
|
36
|
+
'ConnectorBase',
|
|
37
|
+
'ConnectorProtocol',
|
|
38
|
+
'ConnectorType',
|
|
39
|
+
# Typed Dicts
|
|
40
|
+
'ConnectorApiConfigMap',
|
|
41
|
+
'ConnectorDbConfigMap',
|
|
42
|
+
'ConnectorFileConfigMap',
|
|
43
|
+
]
|
etlplus/connector/api.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.connector.api` module.
|
|
3
|
+
|
|
4
|
+
API 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 ..api import PaginationConfig
|
|
25
|
+
from ..api import PaginationConfigMap
|
|
26
|
+
from ..api import RateLimitConfig
|
|
27
|
+
from ..api import RateLimitConfigMap
|
|
28
|
+
from ..types import StrAnyMap
|
|
29
|
+
from ..types import StrStrMap
|
|
30
|
+
from ..utils import cast_str_dict
|
|
31
|
+
from ..utils import coerce_dict
|
|
32
|
+
from ..utils import maybe_mapping
|
|
33
|
+
from .core import ConnectorBase
|
|
34
|
+
from .enums import DataConnectorType
|
|
35
|
+
from .types import ConnectorType
|
|
36
|
+
|
|
37
|
+
# SECTION: EXPORTS ========================================================== #
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
'ConnectorApi',
|
|
42
|
+
'ConnectorApiConfigMap',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConnectorApiConfigMap(TypedDict, total=False):
|
|
50
|
+
"""
|
|
51
|
+
Shape accepted by :meth:`ConnectorApi.from_obj` (all keys optional).
|
|
52
|
+
|
|
53
|
+
See Also
|
|
54
|
+
--------
|
|
55
|
+
- :meth:`etlplus.connector.api.ConnectorApi.from_obj`
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
type: ConnectorType
|
|
60
|
+
url: str
|
|
61
|
+
method: str
|
|
62
|
+
headers: StrStrMap
|
|
63
|
+
query_params: StrAnyMap
|
|
64
|
+
pagination: PaginationConfigMap
|
|
65
|
+
rate_limit: RateLimitConfigMap
|
|
66
|
+
api: str
|
|
67
|
+
endpoint: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# SECTION: DATA CLASSES ===================================================== #
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(kw_only=True, slots=True)
|
|
74
|
+
class ConnectorApi(ConnectorBase):
|
|
75
|
+
"""
|
|
76
|
+
Configuration for an API-based data connector.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
type : ConnectorType
|
|
81
|
+
Connector kind, always ``'api'``.
|
|
82
|
+
url : str | None
|
|
83
|
+
Direct absolute URL (when not using ``service``/``endpoint`` refs).
|
|
84
|
+
method : str | None
|
|
85
|
+
Optional HTTP method; typically omitted for sources (defaults to
|
|
86
|
+
GET) and used for targets (e.g., ``'post'``).
|
|
87
|
+
headers : dict[str, str]
|
|
88
|
+
Additional request headers.
|
|
89
|
+
query_params : dict[str, Any]
|
|
90
|
+
Default query parameters.
|
|
91
|
+
pagination : PaginationConfig | None
|
|
92
|
+
Pagination settings (optional).
|
|
93
|
+
rate_limit : RateLimitConfig | None
|
|
94
|
+
Rate limiting settings (optional).
|
|
95
|
+
api : str | None
|
|
96
|
+
Service reference into the pipeline ``apis`` block (a.k.a.
|
|
97
|
+
``service``).
|
|
98
|
+
endpoint : str | None
|
|
99
|
+
Endpoint name within the referenced service.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# -- Attributes -- #
|
|
103
|
+
|
|
104
|
+
type: ConnectorType = DataConnectorType.API
|
|
105
|
+
|
|
106
|
+
# Direct form
|
|
107
|
+
url: str | None = None
|
|
108
|
+
# Optional HTTP method; typically omitted for sources (defaults to GET)
|
|
109
|
+
# at runtime) and used for targets (e.g., 'post', 'put').
|
|
110
|
+
method: str | None = None
|
|
111
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
112
|
+
query_params: dict[str, Any] = field(default_factory=dict)
|
|
113
|
+
pagination: PaginationConfig | None = None
|
|
114
|
+
rate_limit: RateLimitConfig | None = None
|
|
115
|
+
|
|
116
|
+
# Reference form (to top-level APIs/endpoints)
|
|
117
|
+
api: str | None = None
|
|
118
|
+
endpoint: str | None = None
|
|
119
|
+
|
|
120
|
+
# -- Class Methods -- #
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
@overload
|
|
124
|
+
def from_obj(cls, obj: ConnectorApiConfigMap) -> Self: ...
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
@overload
|
|
128
|
+
def from_obj(cls, obj: StrAnyMap) -> Self: ...
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_obj(
|
|
132
|
+
cls,
|
|
133
|
+
obj: StrAnyMap,
|
|
134
|
+
) -> Self:
|
|
135
|
+
"""
|
|
136
|
+
Parse a mapping into a ``ConnectorApi`` instance.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
obj : StrAnyMap
|
|
141
|
+
Mapping with at least ``name``.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Self
|
|
146
|
+
Parsed connector instance.
|
|
147
|
+
"""
|
|
148
|
+
name = cls._require_name(obj, kind='Api')
|
|
149
|
+
headers = cast_str_dict(maybe_mapping(obj.get('headers')))
|
|
150
|
+
|
|
151
|
+
return cls(
|
|
152
|
+
name=name,
|
|
153
|
+
url=obj.get('url'),
|
|
154
|
+
method=obj.get('method'),
|
|
155
|
+
headers=headers,
|
|
156
|
+
query_params=coerce_dict(obj.get('query_params')),
|
|
157
|
+
pagination=PaginationConfig.from_obj(obj.get('pagination')),
|
|
158
|
+
rate_limit=RateLimitConfig.from_obj(obj.get('rate_limit')),
|
|
159
|
+
api=obj.get('api') or obj.get('service'),
|
|
160
|
+
endpoint=obj.get('endpoint'),
|
|
161
|
+
)
|
|
@@ -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
|
+
'ConnectorDbConfigMap',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# SECTION: TYPED DICTS ====================================================== #
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConnectorDbConfigMap(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: ConnectorDbConfigMap) -> 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
|
+
}
|