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.
Files changed (122) hide show
  1. etlplus/README.md +4 -4
  2. etlplus/api/README.md +33 -2
  3. etlplus/api/auth.py +1 -1
  4. etlplus/api/config.py +5 -10
  5. etlplus/api/endpoint_client.py +4 -4
  6. etlplus/api/pagination/config.py +1 -1
  7. etlplus/api/pagination/paginator.py +6 -7
  8. etlplus/api/rate_limiting/config.py +4 -4
  9. etlplus/api/rate_limiting/rate_limiter.py +1 -1
  10. etlplus/api/retry_manager.py +2 -2
  11. etlplus/api/transport.py +1 -1
  12. etlplus/api/types.py +99 -0
  13. etlplus/api/utils.py +6 -2
  14. etlplus/cli/README.md +2 -2
  15. etlplus/cli/commands.py +75 -42
  16. etlplus/cli/constants.py +1 -1
  17. etlplus/cli/handlers.py +33 -15
  18. etlplus/cli/io.py +2 -2
  19. etlplus/cli/main.py +2 -2
  20. etlplus/cli/state.py +4 -7
  21. etlplus/connector/__init__.py +43 -0
  22. etlplus/connector/api.py +161 -0
  23. etlplus/connector/connector.py +26 -0
  24. etlplus/connector/core.py +132 -0
  25. etlplus/connector/database.py +122 -0
  26. etlplus/connector/enums.py +52 -0
  27. etlplus/connector/file.py +120 -0
  28. etlplus/connector/types.py +40 -0
  29. etlplus/connector/utils.py +122 -0
  30. etlplus/database/README.md +2 -2
  31. etlplus/database/ddl.py +2 -2
  32. etlplus/database/engine.py +19 -3
  33. etlplus/database/orm.py +2 -0
  34. etlplus/enums.py +1 -33
  35. etlplus/file/README.md +2 -2
  36. etlplus/file/_imports.py +1 -0
  37. etlplus/file/_io.py +52 -4
  38. etlplus/file/accdb.py +3 -2
  39. etlplus/file/arrow.py +3 -2
  40. etlplus/file/avro.py +3 -2
  41. etlplus/file/bson.py +3 -2
  42. etlplus/file/cbor.py +3 -2
  43. etlplus/file/cfg.py +3 -2
  44. etlplus/file/conf.py +3 -2
  45. etlplus/file/core.py +11 -8
  46. etlplus/file/csv.py +3 -2
  47. etlplus/file/dat.py +3 -2
  48. etlplus/file/dta.py +3 -2
  49. etlplus/file/duckdb.py +3 -2
  50. etlplus/file/enums.py +1 -1
  51. etlplus/file/feather.py +3 -2
  52. etlplus/file/fwf.py +3 -2
  53. etlplus/file/gz.py +3 -2
  54. etlplus/file/hbs.py +3 -2
  55. etlplus/file/hdf5.py +3 -2
  56. etlplus/file/ini.py +3 -2
  57. etlplus/file/ion.py +3 -2
  58. etlplus/file/jinja2.py +3 -2
  59. etlplus/file/json.py +5 -16
  60. etlplus/file/log.py +3 -2
  61. etlplus/file/mat.py +3 -2
  62. etlplus/file/mdb.py +3 -2
  63. etlplus/file/msgpack.py +3 -2
  64. etlplus/file/mustache.py +3 -2
  65. etlplus/file/nc.py +3 -2
  66. etlplus/file/ndjson.py +3 -2
  67. etlplus/file/numbers.py +3 -2
  68. etlplus/file/ods.py +3 -2
  69. etlplus/file/orc.py +3 -2
  70. etlplus/file/parquet.py +3 -2
  71. etlplus/file/pb.py +3 -2
  72. etlplus/file/pbf.py +3 -2
  73. etlplus/file/properties.py +3 -2
  74. etlplus/file/proto.py +3 -2
  75. etlplus/file/psv.py +3 -2
  76. etlplus/file/rda.py +3 -2
  77. etlplus/file/rds.py +3 -2
  78. etlplus/file/sas7bdat.py +3 -2
  79. etlplus/file/sav.py +3 -2
  80. etlplus/file/sqlite.py +3 -2
  81. etlplus/file/stub.py +1 -0
  82. etlplus/file/sylk.py +3 -2
  83. etlplus/file/tab.py +3 -2
  84. etlplus/file/toml.py +3 -2
  85. etlplus/file/tsv.py +3 -2
  86. etlplus/file/txt.py +4 -3
  87. etlplus/file/vm.py +3 -2
  88. etlplus/file/wks.py +3 -2
  89. etlplus/file/xls.py +3 -2
  90. etlplus/file/xlsm.py +3 -2
  91. etlplus/file/xlsx.py +3 -2
  92. etlplus/file/xml.py +9 -3
  93. etlplus/file/xpt.py +3 -2
  94. etlplus/file/yaml.py +5 -16
  95. etlplus/file/zip.py +3 -2
  96. etlplus/file/zsav.py +3 -2
  97. etlplus/ops/extract.py +13 -1
  98. etlplus/ops/load.py +15 -2
  99. etlplus/ops/run.py +18 -13
  100. etlplus/ops/transform.py +2 -2
  101. etlplus/ops/utils.py +6 -35
  102. etlplus/ops/validate.py +3 -3
  103. etlplus/templates/README.md +2 -2
  104. etlplus/types.py +3 -2
  105. etlplus/utils.py +163 -29
  106. etlplus/{config → workflow}/README.md +6 -6
  107. etlplus/workflow/__init__.py +32 -0
  108. etlplus/{dag.py → workflow/dag.py} +6 -4
  109. etlplus/{config → workflow}/jobs.py +101 -38
  110. etlplus/{config → workflow}/pipeline.py +59 -51
  111. etlplus/{config → workflow}/profile.py +8 -5
  112. {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/METADATA +4 -4
  113. etlplus-0.16.0.dist-info/RECORD +141 -0
  114. {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/WHEEL +1 -1
  115. etlplus/config/__init__.py +0 -56
  116. etlplus/config/connector.py +0 -372
  117. etlplus/config/types.py +0 -204
  118. etlplus/config/utils.py +0 -120
  119. etlplus-0.14.3.dist-info/RECORD +0 -135
  120. {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/entry_points.txt +0 -0
  121. {etlplus-0.14.3.dist-info → etlplus-0.16.0.dist-info}/licenses/LICENSE +0 -0
  122. {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
- sections['transforms'] = [
125
- getattr(trf, 'name', None) for trf in cfg.transforms
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 ``job``. Default is ``None``.
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
- # Convert target to Path and target_format to FileFormat if needed
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 ``output_path``.
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 ``source`` references a file.
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 ``ctx`` was ``None``.
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 '').strip().lower()
326
- normalized_choices = {c.lower() for c in choices}
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
- # Preserve original casing from choices when possible for messages
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
+ ]
@@ -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
+ }