datacoolie 0.1.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.
- datacoolie/__init__.py +242 -0
- datacoolie/core/__init__.py +81 -0
- datacoolie/core/constants.py +223 -0
- datacoolie/core/exceptions.py +114 -0
- datacoolie/core/models.py +1202 -0
- datacoolie/core/registry.py +208 -0
- datacoolie/core/secret_provider.py +233 -0
- datacoolie/core/secret_resolver.py +125 -0
- datacoolie/destinations/__init__.py +37 -0
- datacoolie/destinations/base.py +383 -0
- datacoolie/destinations/delta_writer.py +377 -0
- datacoolie/destinations/file_writer.py +143 -0
- datacoolie/destinations/iceberg_writer.py +167 -0
- datacoolie/destinations/load_strategies.py +323 -0
- datacoolie/engines/__init__.py +28 -0
- datacoolie/engines/base.py +1038 -0
- datacoolie/engines/polars_engine.py +2768 -0
- datacoolie/engines/spark_engine.py +1777 -0
- datacoolie/engines/spark_session_builder.py +78 -0
- datacoolie/logging/__init__.py +48 -0
- datacoolie/logging/base.py +561 -0
- datacoolie/logging/context.py +37 -0
- datacoolie/logging/etl_logger.py +769 -0
- datacoolie/logging/system_logger.py +110 -0
- datacoolie/metadata/__init__.py +34 -0
- datacoolie/metadata/api_client.py +632 -0
- datacoolie/metadata/base.py +743 -0
- datacoolie/metadata/database_provider.py +677 -0
- datacoolie/metadata/file_provider.py +757 -0
- datacoolie/orchestration/__init__.py +23 -0
- datacoolie/orchestration/driver.py +910 -0
- datacoolie/orchestration/job_distributor.py +147 -0
- datacoolie/orchestration/parallel_executor.py +326 -0
- datacoolie/orchestration/retry_handler.py +149 -0
- datacoolie/orchestration/utils.py +52 -0
- datacoolie/platforms/__init__.py +15 -0
- datacoolie/platforms/aws_platform.py +665 -0
- datacoolie/platforms/base.py +313 -0
- datacoolie/platforms/databricks_platform.py +630 -0
- datacoolie/platforms/fabric_platform.py +365 -0
- datacoolie/platforms/local_platform.py +294 -0
- datacoolie/sources/__init__.py +24 -0
- datacoolie/sources/api_reader.py +1270 -0
- datacoolie/sources/base.py +425 -0
- datacoolie/sources/database_reader.py +237 -0
- datacoolie/sources/delta_reader.py +94 -0
- datacoolie/sources/file_reader.py +440 -0
- datacoolie/sources/iceberg_reader.py +75 -0
- datacoolie/sources/python_function_reader.py +179 -0
- datacoolie/transformers/__init__.py +29 -0
- datacoolie/transformers/base.py +198 -0
- datacoolie/transformers/column_adder.py +124 -0
- datacoolie/transformers/column_name_sanitizer.py +89 -0
- datacoolie/transformers/deduplicator.py +84 -0
- datacoolie/transformers/partition_handler.py +76 -0
- datacoolie/transformers/schema_converter.py +107 -0
- datacoolie/utils/__init__.py +45 -0
- datacoolie/utils/converters.py +261 -0
- datacoolie/utils/helpers.py +165 -0
- datacoolie/utils/path_utils.py +60 -0
- datacoolie/watermark/__init__.py +19 -0
- datacoolie/watermark/base.py +172 -0
- datacoolie/watermark/watermark_manager.py +69 -0
- datacoolie-0.1.0.dist-info/METADATA +200 -0
- datacoolie-0.1.0.dist-info/RECORD +68 -0
- datacoolie-0.1.0.dist-info/WHEEL +4 -0
- datacoolie-0.1.0.dist-info/entry_points.txt +44 -0
- datacoolie-0.1.0.dist-info/licenses/LICENSE +661 -0
datacoolie/__init__.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""DataCoolie — Metadata-driven, engine-unified, cloud-agnostic ETL framework."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
# Base classes
|
|
7
|
+
"PluginRegistry",
|
|
8
|
+
"BaseSecretResolver",
|
|
9
|
+
"BaseEngine",
|
|
10
|
+
"BasePlatform",
|
|
11
|
+
"BaseSourceReader",
|
|
12
|
+
"BaseDestinationWriter",
|
|
13
|
+
"BaseTransformer",
|
|
14
|
+
# Registries
|
|
15
|
+
"engine_registry",
|
|
16
|
+
"platform_registry",
|
|
17
|
+
"source_registry",
|
|
18
|
+
"destination_registry",
|
|
19
|
+
"transformer_registry",
|
|
20
|
+
"resolver_registry",
|
|
21
|
+
# Factory functions
|
|
22
|
+
"create_engine",
|
|
23
|
+
"create_platform",
|
|
24
|
+
"create_source",
|
|
25
|
+
"create_destination",
|
|
26
|
+
"create_transformer",
|
|
27
|
+
"create_resolver",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
import logging as _logging
|
|
31
|
+
|
|
32
|
+
from datacoolie.core.registry import PluginRegistry
|
|
33
|
+
from datacoolie.core.secret_resolver import BaseSecretResolver
|
|
34
|
+
from datacoolie.destinations.base import BaseDestinationWriter
|
|
35
|
+
from datacoolie.engines.base import BaseEngine
|
|
36
|
+
from datacoolie.platforms.base import BasePlatform
|
|
37
|
+
from datacoolie.sources.base import BaseSourceReader
|
|
38
|
+
from datacoolie.transformers.base import BaseTransformer
|
|
39
|
+
|
|
40
|
+
_logger = _logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Global plugin registries
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
engine_registry: PluginRegistry[BaseEngine] = PluginRegistry("datacoolie.engines", BaseEngine)
|
|
47
|
+
platform_registry: PluginRegistry[BasePlatform] = PluginRegistry("datacoolie.platforms", BasePlatform)
|
|
48
|
+
source_registry: PluginRegistry[BaseSourceReader] = PluginRegistry("datacoolie.sources", BaseSourceReader)
|
|
49
|
+
destination_registry: PluginRegistry[BaseDestinationWriter] = PluginRegistry("datacoolie.destinations", BaseDestinationWriter)
|
|
50
|
+
transformer_registry: PluginRegistry[BaseTransformer] = PluginRegistry("datacoolie.transformers", BaseTransformer)
|
|
51
|
+
resolver_registry: PluginRegistry[BaseSecretResolver] = PluginRegistry("datacoolie.resolvers", BaseSecretResolver)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Factory functions
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def create_engine(name: str, **kwargs: object) -> BaseEngine:
|
|
59
|
+
"""Create an engine instance by name (e.g. ``"spark"``, ``"polars"``)."""
|
|
60
|
+
return engine_registry.get(name, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_platform(name: str, **kwargs: object) -> BasePlatform:
|
|
64
|
+
"""Create a platform instance by name (e.g. ``"local"``, ``"fabric"``)."""
|
|
65
|
+
return platform_registry.get(name, **kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_source(name: str, **kwargs: object) -> BaseSourceReader:
|
|
69
|
+
"""Create a source reader by name (e.g. ``"delta"``, ``"csv"``)."""
|
|
70
|
+
return source_registry.get(name, **kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_destination(name: str, **kwargs: object) -> BaseDestinationWriter:
|
|
74
|
+
"""Create a destination writer by name (e.g. ``"delta"``, ``"parquet"``)."""
|
|
75
|
+
return destination_registry.get(name, **kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_transformer(name: str, **kwargs: object) -> BaseTransformer:
|
|
79
|
+
"""Create a transformer by name (e.g. ``"schema_converter"``)."""
|
|
80
|
+
return transformer_registry.get(name, **kwargs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_resolver(name: str, **kwargs: object) -> BaseSecretResolver:
|
|
84
|
+
"""Create a secret resolver by name (e.g. ``"env"``)."""
|
|
85
|
+
return resolver_registry.get(name, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Built-in registrations (after all packages are initialised)
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def _register_builtins() -> None:
|
|
93
|
+
"""Register all built-in plugins.
|
|
94
|
+
|
|
95
|
+
Uses try/except for each registration so that missing optional
|
|
96
|
+
dependencies (e.g. PySpark, Polars) don't break the import.
|
|
97
|
+
"""
|
|
98
|
+
# -- Engines --
|
|
99
|
+
try:
|
|
100
|
+
from datacoolie.engines.spark_engine import SparkEngine
|
|
101
|
+
engine_registry.register("spark", SparkEngine)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
_logger.debug("Skipping built-in 'spark' engine: %s", exc)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
from datacoolie.engines.polars_engine import PolarsEngine
|
|
107
|
+
engine_registry.register("polars", PolarsEngine)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
_logger.debug("Skipping built-in 'polars' engine: %s", exc)
|
|
110
|
+
|
|
111
|
+
# -- Platforms --
|
|
112
|
+
try:
|
|
113
|
+
from datacoolie.platforms.local_platform import LocalPlatform
|
|
114
|
+
platform_registry.register("local", LocalPlatform)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
_logger.debug("Skipping built-in 'local' platform: %s", exc)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
from datacoolie.platforms.fabric_platform import FabricPlatform
|
|
120
|
+
platform_registry.register("fabric", FabricPlatform)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
_logger.debug("Skipping built-in 'fabric' platform: %s", exc)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
from datacoolie.platforms.databricks_platform import DatabricksPlatform
|
|
126
|
+
platform_registry.register("databricks", DatabricksPlatform)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
_logger.debug("Skipping built-in 'databricks' platform: %s", exc)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
from datacoolie.platforms.aws_platform import AWSPlatform
|
|
132
|
+
platform_registry.register("aws", AWSPlatform)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
_logger.debug("Skipping built-in 'aws' platform: %s", exc)
|
|
135
|
+
|
|
136
|
+
# -- Sources --
|
|
137
|
+
try:
|
|
138
|
+
from datacoolie.sources.delta_reader import DeltaReader
|
|
139
|
+
source_registry.register("delta", DeltaReader)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
_logger.debug("Skipping built-in 'delta' source: %s", exc)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
from datacoolie.sources.file_reader import FileReader
|
|
145
|
+
source_registry.register("parquet", FileReader)
|
|
146
|
+
source_registry.register("csv", FileReader)
|
|
147
|
+
source_registry.register("json", FileReader)
|
|
148
|
+
source_registry.register("jsonl", FileReader)
|
|
149
|
+
source_registry.register("avro", FileReader)
|
|
150
|
+
source_registry.register("excel", FileReader)
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
_logger.debug("Skipping built-in file source readers: %s", exc)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
from datacoolie.sources.python_function_reader import PythonFunctionReader
|
|
156
|
+
source_registry.register("function", PythonFunctionReader)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
_logger.debug("Skipping built-in 'function' source: %s", exc)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
from datacoolie.sources.database_reader import DatabaseReader
|
|
162
|
+
source_registry.register("sql", DatabaseReader)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
_logger.debug("Skipping built-in 'sql' source: %s", exc)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
from datacoolie.sources.iceberg_reader import IcebergReader
|
|
168
|
+
source_registry.register("iceberg", IcebergReader)
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
_logger.debug("Skipping built-in 'iceberg' source: %s", exc)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
from datacoolie.sources.api_reader import APIReader
|
|
174
|
+
source_registry.register("api", APIReader)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
_logger.debug("Skipping built-in 'api' source: %s", exc)
|
|
177
|
+
|
|
178
|
+
# -- Destinations --
|
|
179
|
+
try:
|
|
180
|
+
from datacoolie.destinations.delta_writer import DeltaWriter
|
|
181
|
+
destination_registry.register("delta", DeltaWriter)
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
_logger.debug("Skipping built-in 'delta' destination: %s", exc)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
from datacoolie.destinations.file_writer import FileWriter
|
|
187
|
+
destination_registry.register("parquet", FileWriter)
|
|
188
|
+
destination_registry.register("csv", FileWriter)
|
|
189
|
+
destination_registry.register("json", FileWriter)
|
|
190
|
+
destination_registry.register("jsonl", FileWriter)
|
|
191
|
+
destination_registry.register("avro", FileWriter)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
_logger.debug("Skipping built-in file destination writers: %s", exc)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
from datacoolie.destinations.iceberg_writer import IcebergWriter
|
|
197
|
+
destination_registry.register("iceberg", IcebergWriter)
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
_logger.debug("Skipping built-in 'iceberg' destination: %s", exc)
|
|
200
|
+
|
|
201
|
+
# -- Transformers --
|
|
202
|
+
try:
|
|
203
|
+
from datacoolie.transformers.schema_converter import SchemaConverter
|
|
204
|
+
transformer_registry.register("schema_converter", SchemaConverter)
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
_logger.debug("Skipping built-in 'schema_converter' transformer: %s", exc)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
from datacoolie.transformers.deduplicator import Deduplicator
|
|
210
|
+
transformer_registry.register("deduplicator", Deduplicator)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
_logger.debug("Skipping built-in 'deduplicator' transformer: %s", exc)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
from datacoolie.transformers.column_adder import ColumnAdder, SCD2ColumnAdder, SystemColumnAdder
|
|
216
|
+
transformer_registry.register("column_adder", ColumnAdder)
|
|
217
|
+
transformer_registry.register("scd2_column_adder", SCD2ColumnAdder)
|
|
218
|
+
transformer_registry.register("system_column_adder", SystemColumnAdder)
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
_logger.debug("Skipping built-in column adder transformers: %s", exc)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
from datacoolie.transformers.partition_handler import PartitionHandler
|
|
224
|
+
transformer_registry.register("partition_handler", PartitionHandler)
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
_logger.debug("Skipping built-in 'partition_handler' transformer: %s", exc)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
from datacoolie.transformers.column_name_sanitizer import ColumnNameSanitizer
|
|
230
|
+
transformer_registry.register("column_name_sanitizer", ColumnNameSanitizer)
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
_logger.debug("Skipping built-in 'column_name_sanitizer' transformer: %s", exc)
|
|
233
|
+
|
|
234
|
+
# -- Secret Resolvers --
|
|
235
|
+
try:
|
|
236
|
+
from datacoolie.core.secret_resolver import EnvResolver
|
|
237
|
+
resolver_registry.register("env", EnvResolver)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
_logger.debug("Skipping built-in 'env' resolver: %s", exc)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
_register_builtins()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""DataCoolie core — domain models, constants, exceptions, and plugin registry."""
|
|
2
|
+
|
|
3
|
+
from datacoolie.core.constants import (
|
|
4
|
+
ConnectionType,
|
|
5
|
+
DataFlowStatus,
|
|
6
|
+
ExecutionType,
|
|
7
|
+
DatabaseType,
|
|
8
|
+
Format,
|
|
9
|
+
LoadType,
|
|
10
|
+
LogPurpose,
|
|
11
|
+
LogType,
|
|
12
|
+
MaintenanceType,
|
|
13
|
+
ProcessingMode,
|
|
14
|
+
ColumnCaseMode,
|
|
15
|
+
SystemColumn,
|
|
16
|
+
FileInfoColumn,
|
|
17
|
+
)
|
|
18
|
+
from datacoolie.core.exceptions import (
|
|
19
|
+
ConfigurationError,
|
|
20
|
+
DataCoolieError,
|
|
21
|
+
DataFlowError,
|
|
22
|
+
DestinationError,
|
|
23
|
+
EngineError,
|
|
24
|
+
MetadataError,
|
|
25
|
+
PlatformError,
|
|
26
|
+
SourceError,
|
|
27
|
+
TransformError,
|
|
28
|
+
WatermarkError,
|
|
29
|
+
)
|
|
30
|
+
from datacoolie.core.models import (
|
|
31
|
+
SchemaHint,
|
|
32
|
+
PartitionColumn,
|
|
33
|
+
AdditionalColumn,
|
|
34
|
+
Connection,
|
|
35
|
+
Source,
|
|
36
|
+
Destination,
|
|
37
|
+
Transform,
|
|
38
|
+
DataFlow,
|
|
39
|
+
DataCoolieRunConfig,
|
|
40
|
+
)
|
|
41
|
+
from datacoolie.core.registry import PluginRegistry
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Enums
|
|
45
|
+
"ConnectionType",
|
|
46
|
+
"DataFlowStatus",
|
|
47
|
+
"ExecutionType",
|
|
48
|
+
"DatabaseType",
|
|
49
|
+
"Format",
|
|
50
|
+
"LoadType",
|
|
51
|
+
"LogPurpose",
|
|
52
|
+
"LogType",
|
|
53
|
+
"MaintenanceType",
|
|
54
|
+
"ProcessingMode",
|
|
55
|
+
"ColumnCaseMode",
|
|
56
|
+
"SystemColumn",
|
|
57
|
+
"FileInfoColumn",
|
|
58
|
+
# Exceptions
|
|
59
|
+
"ConfigurationError",
|
|
60
|
+
"DataCoolieError",
|
|
61
|
+
"DataFlowError",
|
|
62
|
+
"DestinationError",
|
|
63
|
+
"EngineError",
|
|
64
|
+
"MetadataError",
|
|
65
|
+
"PlatformError",
|
|
66
|
+
"SourceError",
|
|
67
|
+
"TransformError",
|
|
68
|
+
"WatermarkError",
|
|
69
|
+
# Models
|
|
70
|
+
"SchemaHint",
|
|
71
|
+
"PartitionColumn",
|
|
72
|
+
"AdditionalColumn",
|
|
73
|
+
"Connection",
|
|
74
|
+
"Source",
|
|
75
|
+
"Destination",
|
|
76
|
+
"Transform",
|
|
77
|
+
"DataFlow",
|
|
78
|
+
"DataCoolieRunConfig",
|
|
79
|
+
# Registry
|
|
80
|
+
"PluginRegistry",
|
|
81
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Framework-wide enums and constants for the DataCoolie framework.
|
|
2
|
+
|
|
3
|
+
Every enum inherits from ``str`` so its ``.value`` is a plain string
|
|
4
|
+
that serialises naturally to JSON/YAML and matches database values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Dict, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Enums
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
class LoadType(str, Enum):
|
|
19
|
+
"""Supported load (write) strategies."""
|
|
20
|
+
|
|
21
|
+
FULL_LOAD = "full_load"
|
|
22
|
+
OVERWRITE = "overwrite"
|
|
23
|
+
APPEND = "append"
|
|
24
|
+
MERGE_UPSERT = "merge_upsert"
|
|
25
|
+
MERGE_OVERWRITE = "merge_overwrite"
|
|
26
|
+
SCD2 = "scd2"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Format(str, Enum):
|
|
30
|
+
"""Supported data formats."""
|
|
31
|
+
|
|
32
|
+
DELTA = "delta"
|
|
33
|
+
ICEBERG = "iceberg"
|
|
34
|
+
PARQUET = "parquet"
|
|
35
|
+
CSV = "csv"
|
|
36
|
+
JSON = "json"
|
|
37
|
+
JSONL = "jsonl"
|
|
38
|
+
AVRO = "avro"
|
|
39
|
+
EXCEL = "excel"
|
|
40
|
+
SQL = "sql"
|
|
41
|
+
API = "api"
|
|
42
|
+
FUNCTION = "function"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConnectionType(str, Enum):
|
|
46
|
+
"""Connection endpoint categories."""
|
|
47
|
+
|
|
48
|
+
LAKEHOUSE = "lakehouse"
|
|
49
|
+
FILE = "file"
|
|
50
|
+
DATABASE = "database"
|
|
51
|
+
API = "api"
|
|
52
|
+
STREAMING = "streaming"
|
|
53
|
+
FUNCTION = "function"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Connection type ↔ format mapping
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
CONNECTION_TYPE_FORMATS: Dict[str, frozenset] = {
|
|
61
|
+
ConnectionType.FILE.value: frozenset({
|
|
62
|
+
Format.PARQUET.value,
|
|
63
|
+
Format.CSV.value,
|
|
64
|
+
Format.JSON.value,
|
|
65
|
+
Format.JSONL.value,
|
|
66
|
+
Format.AVRO.value,
|
|
67
|
+
Format.EXCEL.value,
|
|
68
|
+
}),
|
|
69
|
+
ConnectionType.LAKEHOUSE.value: frozenset({
|
|
70
|
+
Format.DELTA.value,
|
|
71
|
+
Format.ICEBERG.value,
|
|
72
|
+
}),
|
|
73
|
+
ConnectionType.DATABASE.value: frozenset({
|
|
74
|
+
Format.SQL.value,
|
|
75
|
+
}),
|
|
76
|
+
ConnectionType.API.value: frozenset({
|
|
77
|
+
Format.API.value,
|
|
78
|
+
}),
|
|
79
|
+
ConnectionType.FUNCTION.value: frozenset({
|
|
80
|
+
Format.FUNCTION.value,
|
|
81
|
+
}),
|
|
82
|
+
ConnectionType.STREAMING.value: frozenset(
|
|
83
|
+
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ProcessingMode(str, Enum):
|
|
89
|
+
"""ETL processing modes."""
|
|
90
|
+
|
|
91
|
+
BATCH = "batch"
|
|
92
|
+
MICROBATCH = "microbatch"
|
|
93
|
+
STREAMING = "streaming"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DataFlowStatus(str, Enum):
|
|
97
|
+
"""Dataflow execution statuses."""
|
|
98
|
+
|
|
99
|
+
PENDING = "pending"
|
|
100
|
+
RUNNING = "running"
|
|
101
|
+
SUCCEEDED = "succeeded"
|
|
102
|
+
FAILED = "failed"
|
|
103
|
+
SKIPPED = "skipped"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ExecutionType(str, Enum):
|
|
107
|
+
"""ETL execution types."""
|
|
108
|
+
|
|
109
|
+
ETL = "etl"
|
|
110
|
+
MAINTENANCE = "maintenance"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class DatabaseType(str, Enum):
|
|
114
|
+
"""Supported database types for connection URL generation."""
|
|
115
|
+
|
|
116
|
+
POSTGRESQL = "postgresql"
|
|
117
|
+
MYSQL = "mysql"
|
|
118
|
+
MSSQL = "mssql"
|
|
119
|
+
ORACLE = "oracle"
|
|
120
|
+
SQLITE = "sqlite"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MaintenanceType(str, Enum):
|
|
124
|
+
"""Maintenance operation types."""
|
|
125
|
+
|
|
126
|
+
COMPACT = "compact"
|
|
127
|
+
CLEANUP = "cleanup"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ColumnCaseMode(str, Enum):
|
|
131
|
+
"""Column name case-conversion modes used by :class:`ColumnNameSanitizer`."""
|
|
132
|
+
|
|
133
|
+
LOWER = "lower"
|
|
134
|
+
SNAKE = "snake"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LogType(str, Enum):
|
|
138
|
+
"""Log entry types for structured logging."""
|
|
139
|
+
|
|
140
|
+
JOB_RUN_LOG = "job_run_log"
|
|
141
|
+
DATAFLOW_RUN_LOG = "dataflow_run_log"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class LogPurpose(str, Enum):
|
|
145
|
+
"""Log output purposes / formats."""
|
|
146
|
+
|
|
147
|
+
DEBUG = "debug_json"
|
|
148
|
+
ANALYST = "analyst"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# System columns (auto-added to destination data)
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
class SystemColumn(str, Enum):
|
|
156
|
+
"""Column names automatically added to every destination write."""
|
|
157
|
+
|
|
158
|
+
CREATED_AT = "__created_at"
|
|
159
|
+
UPDATED_AT = "__updated_at"
|
|
160
|
+
UPDATED_BY = "__updated_by"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class FileInfoColumn(str, Enum):
|
|
164
|
+
"""Column names injected for file-based sources."""
|
|
165
|
+
|
|
166
|
+
FILE_NAME = "__file_name"
|
|
167
|
+
FILE_PATH = "__file_path"
|
|
168
|
+
FILE_MODIFICATION_TIME = "__file_modification_time"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SCD2Column(str, Enum):
|
|
172
|
+
"""Column names for Slowly Changing Dimension Type 2."""
|
|
173
|
+
|
|
174
|
+
VALID_FROM = "__valid_from"
|
|
175
|
+
VALID_TO = "__valid_to"
|
|
176
|
+
IS_CURRENT = "__is_current"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Canonical order for columns that should always appear at the end of a
|
|
180
|
+
# DataFrame / table schema. Shared by the transformer pipeline
|
|
181
|
+
# (``ColumnNameSanitizer``) and the Iceberg adapter
|
|
182
|
+
# (``_reorder_iceberg_trailing_columns``).
|
|
183
|
+
TRAILING_COLUMNS: tuple[str, ...] = (
|
|
184
|
+
*(c.value for c in SCD2Column),
|
|
185
|
+
*(c.value for c in FileInfoColumn),
|
|
186
|
+
*(c.value for c in SystemColumn),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Watermark
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
WATERMARK_FILE_NAME: str = "watermark_value.json"
|
|
194
|
+
DATETIME_PATTERN: str = "__datetime__"
|
|
195
|
+
DATE_PATTERN: str = "__date__"
|
|
196
|
+
TIME_PATTERN: str = "__time__"
|
|
197
|
+
DATE_FOLDER_PARTITION_KEY: str = "__date_folder_partition__"
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Date folder partition placeholders
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
DATE_PLACEHOLDERS: Dict[str, tuple[str, str]] = {
|
|
204
|
+
"{year}": (r"(\d{4})", "year"),
|
|
205
|
+
"{month}": (r"(\d{2})", "month"),
|
|
206
|
+
"{day}": (r"(\d{2})", "day"),
|
|
207
|
+
"{hour}": (r"(\d{2})", "hour"),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Defaults
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
# Log partition pattern default is date-based, but can be overridden by config
|
|
215
|
+
DEFAULT_PARTITION_PATTERN = "__run_date={year}-{month}-{day}"
|
|
216
|
+
|
|
217
|
+
# Default values for various parameters across the framework
|
|
218
|
+
DEFAULT_AUTHOR: str = "DataCoolie"
|
|
219
|
+
DEFAULT_MAX_WORKERS: int = 8
|
|
220
|
+
DEFAULT_RETRY_COUNT: int = 0
|
|
221
|
+
DEFAULT_RETRY_DELAY: float = 5.0
|
|
222
|
+
DEFAULT_RETENTION_HOURS: int = 168 # 7 days
|
|
223
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Exception hierarchy for the DataCoolie framework.
|
|
2
|
+
|
|
3
|
+
All framework exceptions inherit from :class:`DataCoolieError` which carries
|
|
4
|
+
a human-readable ``message`` and an optional ``details`` dict for structured
|
|
5
|
+
error context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataCoolieError(Exception):
|
|
14
|
+
"""Base exception for all DataCoolie errors.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
message: Human-readable error description.
|
|
18
|
+
details: Optional structured context for logging / debugging.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
|
|
22
|
+
self.message = message
|
|
23
|
+
self.details = details or {}
|
|
24
|
+
super().__init__(self.message)
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
if self.details:
|
|
28
|
+
return f"{self.message} | Details: {self.details}"
|
|
29
|
+
return self.message
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Component-specific errors
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfigurationError(DataCoolieError):
|
|
38
|
+
"""Raised for invalid or missing configuration."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MetadataError(DataCoolieError):
|
|
42
|
+
"""Raised for metadata loading / parsing failures."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SourceError(DataCoolieError):
|
|
46
|
+
"""Raised when reading from a data source fails."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TransformError(DataCoolieError):
|
|
50
|
+
"""Raised when a transformation step fails."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DestinationError(DataCoolieError):
|
|
54
|
+
"""Raised when writing to a destination fails."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WatermarkError(DataCoolieError):
|
|
58
|
+
"""Raised for watermark read / write / parse failures."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EngineError(DataCoolieError):
|
|
62
|
+
"""Raised for engine-level failures (Spark, Polars)."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PlatformError(DataCoolieError):
|
|
66
|
+
"""Raised for platform-specific failures (Fabric, Databricks, AWS)."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PipelineError(DataCoolieError):
|
|
70
|
+
"""Raised when a pipeline step fails, carrying partial runtime results.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
partial_result: The incomplete result tuple collected before the failure.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
message: str,
|
|
79
|
+
partial_result: Any = None,
|
|
80
|
+
details: dict[str, Any] | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(message, details)
|
|
83
|
+
self.partial_result = partial_result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DataFlowError(DataCoolieError):
|
|
87
|
+
"""Raised for dataflow-level execution errors.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
dataflow_id: Identifier of the failed dataflow.
|
|
91
|
+
stage: Pipeline stage where the error occurred.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
message: str,
|
|
97
|
+
dataflow_id: str | None = None,
|
|
98
|
+
stage: str | None = None,
|
|
99
|
+
details: dict[str, Any] | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.dataflow_id = dataflow_id
|
|
102
|
+
self.stage = stage
|
|
103
|
+
super().__init__(message, details)
|
|
104
|
+
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
parts = [self.message]
|
|
107
|
+
if self.dataflow_id:
|
|
108
|
+
parts.append(f"DataFlow: {self.dataflow_id}")
|
|
109
|
+
if self.stage:
|
|
110
|
+
parts.append(f"Stage: {self.stage}")
|
|
111
|
+
if self.details:
|
|
112
|
+
parts.append(f"Details: {self.details}")
|
|
113
|
+
return " | ".join(parts)
|
|
114
|
+
|