dlt-utils-lib 1.2.4__tar.gz → 1.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/PKG-INFO +1 -1
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/main_cdc_utils.py +44 -8
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/PKG-INFO +1 -1
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_main_cdc_utils.py +80 -41
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/__init__.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_autoloader_recovery_configuration.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_metadata_receiver.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_transformations.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/main_json_utils.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/SOURCES.txt +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/dependency_links.txt +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/requires.txt +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/top_level.txt +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/setup.cfg +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/setup.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/__init__.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/conftest.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_main_json_utils.py +0 -0
- {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_transformation_module.py +0 -0
|
@@ -33,6 +33,29 @@ def _build_table_properties(extra: Optional[dict] = None,
|
|
|
33
33
|
return props
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _pick_latest_source_file(spark, files_path: str) -> Optional[str]:
|
|
37
|
+
"""Return the path of the most recently modified source file, or None.
|
|
38
|
+
|
|
39
|
+
Spark's parquet reader cannot mergeSchema across decimal scales
|
|
40
|
+
(e.g. decimal(19,2) vs decimal(19,3) — their integer-digit counts
|
|
41
|
+
differ). Grabbing the newest file's schema instead gives us the
|
|
42
|
+
current source-of-truth type; older files with narrower types are
|
|
43
|
+
read losslessly under that hint by Auto Loader, and Delta type
|
|
44
|
+
widening handles the existing bronze table.
|
|
45
|
+
"""
|
|
46
|
+
listing = (
|
|
47
|
+
spark.read.format('binaryFile')
|
|
48
|
+
.load(files_path)
|
|
49
|
+
.select('path', 'modificationTime')
|
|
50
|
+
.orderBy('modificationTime', ascending=False)
|
|
51
|
+
.limit(1)
|
|
52
|
+
.collect()
|
|
53
|
+
)
|
|
54
|
+
if not listing:
|
|
55
|
+
return None
|
|
56
|
+
return listing[0]['path']
|
|
57
|
+
|
|
58
|
+
|
|
36
59
|
# Base code pipeline: streaming process
|
|
37
60
|
def base_cdc_replication_process(dlt,
|
|
38
61
|
spark,
|
|
@@ -116,6 +139,27 @@ def create_bronze_table_definition(spark,
|
|
|
116
139
|
):
|
|
117
140
|
table_properties = _build_table_properties(extra_table_properties)
|
|
118
141
|
|
|
142
|
+
# Resolve the schemaHints outside the @dlt.table body so that the
|
|
143
|
+
# collect() inside _pick_latest_source_file runs once at pipeline
|
|
144
|
+
# setup — Lakeflow Declarative Pipelines forbids eager Spark ops
|
|
145
|
+
# like collect() inside a decorated function.
|
|
146
|
+
#
|
|
147
|
+
# We take the schema from the newest source file so hints track the
|
|
148
|
+
# current source-of-truth type after upstream drift (e.g. DMS
|
|
149
|
+
# widening decimal(19,2) -> decimal(19,3)). Directory reads — with
|
|
150
|
+
# or without mergeSchema — fail for scale-changed decimals because
|
|
151
|
+
# Spark cannot merge them (integer-digit counts differ). Older
|
|
152
|
+
# files with narrower types cast up losslessly under this hint, and
|
|
153
|
+
# delta.enableTypeWidening handles the existing bronze table.
|
|
154
|
+
latest_file = _pick_latest_source_file(spark, files_path)
|
|
155
|
+
schema_source_path = latest_file if latest_file else files_path
|
|
156
|
+
inferred_df = spark.read.format(file_format).load(schema_source_path).transform(rename_columns)
|
|
157
|
+
schema_string = ', '.join([
|
|
158
|
+
f"{field.name} {field.dataType.simpleString()}"
|
|
159
|
+
for field in inferred_df.schema.fields
|
|
160
|
+
if field.name not in schema_exclude_columns
|
|
161
|
+
])
|
|
162
|
+
|
|
119
163
|
@dlt.table(
|
|
120
164
|
name=table_name,
|
|
121
165
|
comment=f"This is the bronze table for table {source_table_name}.",
|
|
@@ -123,14 +167,6 @@ def create_bronze_table_definition(spark,
|
|
|
123
167
|
table_properties=table_properties,
|
|
124
168
|
)
|
|
125
169
|
def transform_cdc_to_bronze():
|
|
126
|
-
# mergeSchema=true tells the parquet reader to union footers across every
|
|
127
|
-
# bronze file, so schemaHints below always reflect the widest observed
|
|
128
|
-
# type for each column (e.g. picks decimal(19,3) once the source starts
|
|
129
|
-
# writing at that scale, instead of pinning to the first-seen (19,2)
|
|
130
|
-
# and rescuing new values to NULL).
|
|
131
|
-
df = spark.read.format(file_format).option("mergeSchema", "true").load(files_path).transform(rename_columns)
|
|
132
|
-
fields = [field for field in df.schema.fields if field.name not in schema_exclude_columns]
|
|
133
|
-
schema_string = ', '.join([f"{field.name} {field.dataType.simpleString()}" for field in fields])
|
|
134
170
|
reader = spark \
|
|
135
171
|
.readStream \
|
|
136
172
|
.format('cloudFiles') \
|
|
@@ -33,9 +33,15 @@ def _build_cdc_table():
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _stub_bronze_setup(monkeypatch):
|
|
37
|
+
"""Bypass the setup-time parquet read used to infer schemaHints."""
|
|
38
|
+
monkeypatch.setattr(main_cdc_utils, "_pick_latest_source_file", lambda *_: None)
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
def test_base_cdc_replication_process_uses_pipeline_schema_configuration(monkeypatch):
|
|
37
42
|
dlt_spy = _DltSpy()
|
|
38
|
-
spark =
|
|
43
|
+
spark = _make_spark_with_reader([], {})
|
|
44
|
+
_stub_bronze_setup(monkeypatch)
|
|
39
45
|
|
|
40
46
|
monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
|
|
41
47
|
monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
|
|
@@ -58,7 +64,8 @@ def test_base_cdc_replication_process_uses_pipeline_schema_configuration(monkeyp
|
|
|
58
64
|
|
|
59
65
|
def test_base_cdc_replication_process_uses_legacy_names_without_both_schemas(monkeypatch):
|
|
60
66
|
dlt_spy = _DltSpy()
|
|
61
|
-
spark =
|
|
67
|
+
spark = _make_spark_with_reader([], {})
|
|
68
|
+
_stub_bronze_setup(monkeypatch)
|
|
62
69
|
|
|
63
70
|
monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
|
|
64
71
|
monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
|
|
@@ -140,48 +147,52 @@ def test_silver_streaming_process_merges_extra_table_properties(monkeypatch):
|
|
|
140
147
|
assert table_properties["my.custom.prop"] == "42"
|
|
141
148
|
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
self._t = t
|
|
150
|
+
class _FakeField:
|
|
151
|
+
def __init__(self, name, type_str):
|
|
152
|
+
self.name = name
|
|
147
153
|
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
class _DT:
|
|
155
|
+
def simpleString(self_inner):
|
|
156
|
+
return type_str
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
def __init__(self, name, type_str):
|
|
153
|
-
self.name = name
|
|
154
|
-
self.dataType = _DT(type_str)
|
|
158
|
+
self.dataType = _DT()
|
|
155
159
|
|
|
156
|
-
class _DF:
|
|
157
|
-
def __init__(self, fields):
|
|
158
|
-
class _Schema:
|
|
159
|
-
pass
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
class _FakeDF:
|
|
162
|
+
def __init__(self, fields):
|
|
163
|
+
class _Schema:
|
|
164
|
+
pass
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
self.schema = _Schema()
|
|
167
|
+
self.schema.fields = fields
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
def transform(self, _fn):
|
|
170
|
+
return self
|
|
168
171
|
|
|
169
|
-
class _Reader:
|
|
170
|
-
def format(self, fmt):
|
|
171
|
-
calls["format"] = fmt
|
|
172
|
-
return self
|
|
173
172
|
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
def _make_spark_with_reader(load_paths, fields_by_path):
|
|
174
|
+
class _Reader:
|
|
175
|
+
def format(self, _fmt):
|
|
176
176
|
return self
|
|
177
177
|
|
|
178
|
-
def load(self,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return _DF([_Field("pnl", "decimal(19,3)"), _Field("id", "bigint")])
|
|
178
|
+
def load(self, path):
|
|
179
|
+
load_paths.append(path)
|
|
180
|
+
return _FakeDF(fields_by_path.get(path, []))
|
|
182
181
|
|
|
183
182
|
spark = type("_Spark", (), {})()
|
|
184
183
|
spark.read = _Reader()
|
|
184
|
+
return spark
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_create_bronze_table_definition_reads_schema_from_latest_file_at_setup_time(monkeypatch):
|
|
188
|
+
load_paths = []
|
|
189
|
+
latest_path = "s3://bucket/dms_bronze/artemis/settled_bet/20260701-090901614.parquet"
|
|
190
|
+
spark = _make_spark_with_reader(
|
|
191
|
+
load_paths,
|
|
192
|
+
{latest_path: [_FakeField("pnl", "decimal(19,3)"), _FakeField("id", "bigint")]},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
monkeypatch.setattr(main_cdc_utils, "_pick_latest_source_file", lambda *_: latest_path)
|
|
185
196
|
|
|
186
197
|
captured = {}
|
|
187
198
|
|
|
@@ -209,14 +220,42 @@ def test_create_bronze_table_definition_reads_schema_with_merge_and_enables_type
|
|
|
209
220
|
use_managed_file_events=False,
|
|
210
221
|
)
|
|
211
222
|
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
captured["fn"]()
|
|
217
|
-
except AttributeError:
|
|
218
|
-
pass
|
|
219
|
-
|
|
220
|
-
assert calls["format"] == "parquet"
|
|
221
|
-
assert calls["options"].get("mergeSchema") == "true"
|
|
223
|
+
# The schema-inference read must happen at setup time, BEFORE the
|
|
224
|
+
# @dlt.table body ever runs. That means load_paths already contains
|
|
225
|
+
# the latest file just from calling create_bronze_table_definition.
|
|
226
|
+
assert load_paths == [latest_path]
|
|
222
227
|
assert captured["kwargs"]["table_properties"]["delta.enableTypeWidening"] == "true"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_create_bronze_table_definition_falls_back_to_directory_when_listing_empty(monkeypatch):
|
|
231
|
+
load_paths = []
|
|
232
|
+
directory = "s3://bucket/dms_bronze/artemis/settled_bet/"
|
|
233
|
+
spark = _make_spark_with_reader(
|
|
234
|
+
load_paths,
|
|
235
|
+
{directory: [_FakeField("id", "bigint")]},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
monkeypatch.setattr(main_cdc_utils, "_pick_latest_source_file", lambda *_: None)
|
|
239
|
+
|
|
240
|
+
class _Dlt:
|
|
241
|
+
def table(self, **_kwargs):
|
|
242
|
+
def _decorator(fn):
|
|
243
|
+
return fn
|
|
244
|
+
|
|
245
|
+
return _decorator
|
|
246
|
+
|
|
247
|
+
main_cdc_utils.create_bronze_table_definition(
|
|
248
|
+
spark=spark,
|
|
249
|
+
dlt=_Dlt(),
|
|
250
|
+
source_table_name="settled_bet",
|
|
251
|
+
table_name="apollo.artemis.bronze_settled_bet",
|
|
252
|
+
files_path=directory,
|
|
253
|
+
file_format="parquet",
|
|
254
|
+
partitions=[],
|
|
255
|
+
schema_exclude_columns=[],
|
|
256
|
+
time_diff_history_cdc_timestamp=30,
|
|
257
|
+
default_value_for_removed_col={},
|
|
258
|
+
use_managed_file_events=False,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
assert load_paths == [directory]
|
|
File without changes
|
{dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_autoloader_recovery_configuration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|