dlt-utils-lib 1.3.1__tar.gz → 1.3.2__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.3.1 → dlt_utils_lib-1.3.2}/PKG-INFO +1 -1
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/dlt_transformations.py +9 -1
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/main_cdc_utils.py +2 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/main_json_utils.py +2 -1
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils_lib.egg-info/PKG-INFO +1 -1
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/test_main_cdc_utils.py +60 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/test_main_json_utils.py +46 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/test_transformation_module.py +24 -1
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/__init__.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/dlt_autoloader_recovery_configuration.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/dlt_metadata_receiver.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils/dlt_table_properties.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils_lib.egg-info/SOURCES.txt +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils_lib.egg-info/dependency_links.txt +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils_lib.egg-info/requires.txt +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/dlt_utils_lib.egg-info/top_level.txt +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/setup.cfg +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/setup.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/__init__.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/conftest.py +0 -0
- {dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/tests/test_autoloader_recovery_configuration.py +0 -0
|
@@ -12,12 +12,20 @@ def apply_partitions(df: DataFrame, partitions: dict):
|
|
|
12
12
|
return df
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def add_file_landed_timestamp(df: DataFrame) -> DataFrame:
|
|
16
|
+
# When the source file landed in storage (its S3 LastModified), so latency can
|
|
17
|
+
# be measured from there to silver. _metadata exists only on the file source
|
|
18
|
+
# itself, so apply this straight after load().
|
|
19
|
+
return df.withColumn('file_landed_timestamp', col('_metadata.file_modification_time'))
|
|
20
|
+
|
|
21
|
+
|
|
15
22
|
def update_cdc_timestamp(df: DataFrame, time_diff_threshold: int) -> DataFrame:
|
|
16
23
|
# if cdc_timestamp is null or time difference is greater than threshold, set it to the max timestamp in the row
|
|
17
24
|
timestamp_cols = [col.name for col in df.schema.fields if
|
|
18
25
|
isinstance(col.dataType, (TimestampType, DateType)) and col.name not in ['cdc_timestamp',
|
|
19
26
|
'commit_timestamp',
|
|
20
|
-
'dlt_timestamp'
|
|
27
|
+
'dlt_timestamp',
|
|
28
|
+
'file_landed_timestamp']]
|
|
21
29
|
|
|
22
30
|
if timestamp_cols:
|
|
23
31
|
max_timestamp_per_row = None
|
|
@@ -6,6 +6,7 @@ from .dlt_autoloader_recovery_configuration import resolve_autoloader_recovery
|
|
|
6
6
|
from .dlt_table_properties import build_table_properties
|
|
7
7
|
from .dlt_transformations import (
|
|
8
8
|
add_default_value_for_removed_col,
|
|
9
|
+
add_file_landed_timestamp,
|
|
9
10
|
apply_partitions,
|
|
10
11
|
rename_columns,
|
|
11
12
|
update_cdc_timestamp,
|
|
@@ -171,6 +172,7 @@ def create_bronze_table_definition(spark,
|
|
|
171
172
|
)
|
|
172
173
|
|
|
173
174
|
return reader.load(files_path) \
|
|
175
|
+
.transform(add_file_landed_timestamp) \
|
|
174
176
|
.withColumn('cdc_timestamp', col('cdc_timestamp').cast('timestamp')) \
|
|
175
177
|
.withColumn('ar_h_change_seq', col('ar_h_change_seq').cast('string')) \
|
|
176
178
|
.withColumn('dlt_timestamp', current_timestamp()) \
|
|
@@ -5,6 +5,7 @@ from pyspark.sql.functions import col, struct
|
|
|
5
5
|
|
|
6
6
|
from .dlt_autoloader_recovery_configuration import resolve_autoloader_recovery
|
|
7
7
|
from .dlt_table_properties import build_table_properties
|
|
8
|
+
from .dlt_transformations import add_file_landed_timestamp
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def base_json_replication_process(
|
|
@@ -113,7 +114,7 @@ def _build_autoloader_reader(
|
|
|
113
114
|
.option("cloudFiles.validateOptions", "false")
|
|
114
115
|
)
|
|
115
116
|
|
|
116
|
-
loaded = reader.load(source_path)
|
|
117
|
+
loaded = reader.load(source_path).transform(add_file_landed_timestamp)
|
|
117
118
|
|
|
118
119
|
if include_source_file:
|
|
119
120
|
# Which file a row was read from. Off by default because it adds a
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
import dlt_utils.main_cdc_utils as main_cdc_utils
|
|
6
|
+
from dlt_utils.dlt_transformations import add_file_landed_timestamp
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class _DltSpy:
|
|
@@ -263,6 +266,63 @@ def test_create_bronze_table_definition_falls_back_to_directory_when_listing_emp
|
|
|
263
266
|
assert load_paths == [directory]
|
|
264
267
|
|
|
265
268
|
|
|
269
|
+
class _FakeStream:
|
|
270
|
+
"""Records what the bronze body does to the loaded stream, in order."""
|
|
271
|
+
|
|
272
|
+
def __init__(self):
|
|
273
|
+
self.steps = []
|
|
274
|
+
|
|
275
|
+
def transform(self, fn):
|
|
276
|
+
self.steps.append(fn)
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
def withColumn(self, name, _value):
|
|
280
|
+
self.steps.append(name)
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _FakeStreamReader:
|
|
285
|
+
def __init__(self, stream):
|
|
286
|
+
self.stream = stream
|
|
287
|
+
|
|
288
|
+
def format(self, _fmt):
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def option(self, _key, _value):
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
def load(self, _path):
|
|
295
|
+
return self.stream
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_bronze_takes_file_landed_timestamp_straight_from_the_source(monkeypatch):
|
|
299
|
+
stream = _FakeStream()
|
|
300
|
+
spark = _make_spark_with_reader([], {})
|
|
301
|
+
spark.readStream = _FakeStreamReader(stream)
|
|
302
|
+
monkeypatch.setattr(main_cdc_utils, "_pick_latest_source_file", lambda *_: None)
|
|
303
|
+
monkeypatch.setattr(main_cdc_utils, "resolve_autoloader_recovery", lambda _spark: False)
|
|
304
|
+
monkeypatch.setattr(main_cdc_utils, "col", lambda _name: mock.MagicMock())
|
|
305
|
+
monkeypatch.setattr(main_cdc_utils, "current_timestamp", lambda: None)
|
|
306
|
+
|
|
307
|
+
bronze = main_cdc_utils.create_bronze_table_definition(
|
|
308
|
+
spark=spark,
|
|
309
|
+
dlt=_DltSpy(),
|
|
310
|
+
source_table_name="settled_bet",
|
|
311
|
+
table_name="artemis_bronze.settled_bet",
|
|
312
|
+
files_path="/Volumes/artemis_prod/stage/dms_raw/settled_bet/",
|
|
313
|
+
file_format="parquet",
|
|
314
|
+
partitions=[],
|
|
315
|
+
schema_exclude_columns=[],
|
|
316
|
+
time_diff_history_cdc_timestamp=30,
|
|
317
|
+
default_value_for_removed_col={},
|
|
318
|
+
use_managed_file_events=True,
|
|
319
|
+
)
|
|
320
|
+
bronze()
|
|
321
|
+
|
|
322
|
+
# _metadata resolves only on the file source, so nothing may come before it.
|
|
323
|
+
assert stream.steps[0] is add_file_landed_timestamp
|
|
324
|
+
|
|
325
|
+
|
|
266
326
|
@pytest.mark.parametrize("source_options, expected", [
|
|
267
327
|
({"bucket_name": "s3://bucket", "bronze_directory": "dms_bronze"},
|
|
268
328
|
"s3://bucket/dms_bronze/bifrost/customer/"),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dlt_utils.main_json_utils as main_json_utils
|
|
2
|
+
from dlt_utils.dlt_transformations import add_file_landed_timestamp
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class _DltSpy:
|
|
@@ -201,3 +202,48 @@ def test_table_properties_override_the_library_defaults():
|
|
|
201
202
|
# Caller override wins over the library default.
|
|
202
203
|
assert properties["delta.enableTypeWidening"] == "false"
|
|
203
204
|
assert properties["my.custom.prop"] == "42"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _FakeStream:
|
|
208
|
+
def __init__(self):
|
|
209
|
+
self.steps = []
|
|
210
|
+
|
|
211
|
+
def transform(self, fn):
|
|
212
|
+
self.steps.append(fn)
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def withColumn(self, name, _value):
|
|
216
|
+
self.steps.append(name)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class _FakeSpark:
|
|
221
|
+
def __init__(self, stream):
|
|
222
|
+
self.readStream = self
|
|
223
|
+
self._stream = stream
|
|
224
|
+
|
|
225
|
+
def format(self, _fmt):
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def option(self, _key, _value):
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def load(self, _path):
|
|
232
|
+
return self._stream
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_bronze_takes_file_landed_timestamp_straight_from_the_source(monkeypatch):
|
|
236
|
+
monkeypatch.setattr(main_json_utils, "resolve_autoloader_recovery", lambda _spark: False)
|
|
237
|
+
monkeypatch.setattr(main_json_utils, "col", lambda value: value)
|
|
238
|
+
stream = _FakeStream()
|
|
239
|
+
|
|
240
|
+
main_json_utils._build_autoloader_reader(
|
|
241
|
+
spark=_FakeSpark(stream),
|
|
242
|
+
source_path="s3://bucket/raw/exposure_state",
|
|
243
|
+
file_format="json",
|
|
244
|
+
schema_hints=None,
|
|
245
|
+
use_managed_file_events=True,
|
|
246
|
+
include_source_file=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert stream.steps == [add_file_landed_timestamp, "source_file"]
|
|
@@ -6,7 +6,7 @@ from pyspark.sql import SparkSession, Row
|
|
|
6
6
|
from pyspark.sql.functions import expr
|
|
7
7
|
from pyspark.sql.types import TimestampType, StringType
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from dlt_utils.dlt_transformations import rename_columns, apply_partitions, update_cdc_timestamp, add_default_value_for_removed_col, rename_columns
|
|
9
|
+
from dlt_utils.dlt_transformations import rename_columns, apply_partitions, update_cdc_timestamp, add_default_value_for_removed_col, rename_columns, add_file_landed_timestamp
|
|
10
10
|
# Mock databricks.sdk.runtime if not in a Databricks environment.
|
|
11
11
|
#
|
|
12
12
|
# Catching Exception and not ImportError: importing this module builds a
|
|
@@ -79,6 +79,29 @@ def test_update_cdc_timestamp(spark):
|
|
|
79
79
|
assert result[1]['cdc_timestamp'] == datetime(2024, 1, 1)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def test_update_cdc_timestamp_ignores_file_landed_timestamp(spark):
|
|
83
|
+
data = [
|
|
84
|
+
Row(id=1, updated_at=datetime(2024, 1, 10), file_landed_timestamp=datetime(2024, 3, 1), cdc_timestamp=None),
|
|
85
|
+
Row(id=2, updated_at=datetime(2024, 1, 10), file_landed_timestamp=datetime(2024, 3, 1),
|
|
86
|
+
cdc_timestamp=datetime(2024, 1, 9)),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
result = update_cdc_timestamp(spark.createDataFrame(data), time_diff_threshold=5).collect()
|
|
90
|
+
|
|
91
|
+
assert result[0]['cdc_timestamp'] == datetime(2024, 1, 10)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_add_file_landed_timestamp_is_the_file_modification_time(spark, tmp_path):
|
|
95
|
+
path = str(tmp_path / "landed")
|
|
96
|
+
spark.createDataFrame([Row(id=1)]).coalesce(1).write.parquet(path)
|
|
97
|
+
part = next(name for name in os.listdir(path) if name.endswith(".parquet"))
|
|
98
|
+
modified = datetime.fromtimestamp(int(os.path.getmtime(os.path.join(path, part))))
|
|
99
|
+
|
|
100
|
+
result = add_file_landed_timestamp(spark.read.parquet(path)).collect()[0]
|
|
101
|
+
|
|
102
|
+
assert result['file_landed_timestamp'].replace(microsecond=0) == modified
|
|
103
|
+
|
|
104
|
+
|
|
82
105
|
|
|
83
106
|
|
|
84
107
|
def test_add_default_value_for_removed_col(spark):
|
|
File without changes
|
{dlt_utils_lib-1.3.1 → dlt_utils_lib-1.3.2}/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
|