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.
Files changed (19) hide show
  1. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/PKG-INFO +1 -1
  2. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/main_cdc_utils.py +44 -8
  3. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/PKG-INFO +1 -1
  4. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_main_cdc_utils.py +80 -41
  5. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/__init__.py +0 -0
  6. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_autoloader_recovery_configuration.py +0 -0
  7. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_metadata_receiver.py +0 -0
  8. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/dlt_transformations.py +0 -0
  9. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils/main_json_utils.py +0 -0
  10. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/SOURCES.txt +0 -0
  11. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/dependency_links.txt +0 -0
  12. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/requires.txt +0 -0
  13. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/dlt_utils_lib.egg-info/top_level.txt +0 -0
  14. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/setup.cfg +0 -0
  15. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/setup.py +0 -0
  16. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/__init__.py +0 -0
  17. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/conftest.py +0 -0
  18. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_main_json_utils.py +0 -0
  19. {dlt_utils_lib-1.2.4 → dlt_utils_lib-1.2.6}/tests/test_transformation_module.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dlt_utils_lib
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: UNKNOWN
5
5
  Home-page: UNKNOWN
6
6
  License: UNKNOWN
@@ -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') \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dlt-utils-lib
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: UNKNOWN
5
5
  Home-page: UNKNOWN
6
6
  License: UNKNOWN
@@ -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 = object()
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 = object()
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
- def test_create_bronze_table_definition_reads_schema_with_merge_and_enables_type_widening():
144
- class _DT:
145
- def __init__(self, t):
146
- self._t = t
150
+ class _FakeField:
151
+ def __init__(self, name, type_str):
152
+ self.name = name
147
153
 
148
- def simpleString(self):
149
- return self._t
154
+ class _DT:
155
+ def simpleString(self_inner):
156
+ return type_str
150
157
 
151
- class _Field:
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
- self.schema = _Schema()
162
- self.schema.fields = fields
161
+ class _FakeDF:
162
+ def __init__(self, fields):
163
+ class _Schema:
164
+ pass
163
165
 
164
- def transform(self, _fn):
165
- return self
166
+ self.schema = _Schema()
167
+ self.schema.fields = fields
166
168
 
167
- calls = {"options": {}, "format": None}
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
- def option(self, key, value):
175
- calls["options"][key] = value
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, _path):
179
- # After a mergeSchema=true read the parquet reader would union
180
- # decimal(19,2) and decimal(19,3) footers into decimal(19,3).
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
- # dlt.table just captures the transform — invoke it to exercise the
213
- # initial spark.read path (the readStream chain that follows is not
214
- # exercised here; the AttributeError it raises is expected).
215
- try:
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
File without changes