dlt-utils-lib 1.2.7__tar.gz → 1.3.0__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.7 → dlt_utils_lib-1.3.0}/PKG-INFO +1 -1
- dlt_utils_lib-1.3.0/dlt_utils/dlt_table_properties.py +32 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/main_cdc_utils.py +3 -24
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/main_json_utils.py +28 -12
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils_lib.egg-info/PKG-INFO +1 -1
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils_lib.egg-info/SOURCES.txt +1 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/test_main_json_utils.py +60 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/test_transformation_module.py +10 -2
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/__init__.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/dlt_autoloader_recovery_configuration.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/dlt_metadata_receiver.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils/dlt_transformations.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils_lib.egg-info/dependency_links.txt +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils_lib.egg-info/requires.txt +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/dlt_utils_lib.egg-info/top_level.txt +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/setup.cfg +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/setup.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/__init__.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/conftest.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/test_autoloader_recovery_configuration.py +0 -0
- {dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/tests/test_main_cdc_utils.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
# delta.enableTypeWidening lets Delta accept widened column types (e.g. decimal
|
|
5
|
+
# scale bumps) from upstream schema drift without a manual ALTER + full refresh.
|
|
6
|
+
# Applied to every bronze and silver table so future source-side type changes
|
|
7
|
+
# flow through the pipeline dynamically.
|
|
8
|
+
#
|
|
9
|
+
# Shared by the CDC and JSON replication paths rather than defined in each. They
|
|
10
|
+
# held separate copies once and the copies disagreed: the JSON path never gained
|
|
11
|
+
# type widening, so Kafka-sourced tables needed a full refresh for a change the
|
|
12
|
+
# CDC tables absorbed.
|
|
13
|
+
DEFAULT_TABLE_PROPERTIES = {
|
|
14
|
+
"delta.autoOptimize.optimizeWrite": "true",
|
|
15
|
+
"delta.autoOptimize.autoCompact": "true",
|
|
16
|
+
"delta.enableTypeWidening": "true",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_table_properties(extra: Optional[dict] = None,
|
|
21
|
+
column_comments: Optional[dict] = None) -> dict:
|
|
22
|
+
"""Defaults, plus column comments when the caller tracks them, plus overrides.
|
|
23
|
+
|
|
24
|
+
`column_comments=None` leaves the key out entirely, which is how bronze
|
|
25
|
+
tables are built; an empty dict still writes "{}" so the property exists.
|
|
26
|
+
"""
|
|
27
|
+
props = dict(DEFAULT_TABLE_PROPERTIES)
|
|
28
|
+
if column_comments is not None:
|
|
29
|
+
props["column_comments"] = json.dumps(column_comments) if column_comments else "{}"
|
|
30
|
+
if extra:
|
|
31
|
+
props.update(extra)
|
|
32
|
+
return props
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import json
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from pyspark.sql.functions import col, expr, current_timestamp
|
|
5
4
|
|
|
6
5
|
from .dlt_autoloader_recovery_configuration import resolve_autoloader_recovery
|
|
6
|
+
from .dlt_table_properties import build_table_properties
|
|
7
7
|
from .dlt_transformations import (
|
|
8
8
|
add_default_value_for_removed_col,
|
|
9
9
|
apply_partitions,
|
|
@@ -12,27 +12,6 @@ from .dlt_transformations import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
# delta.enableTypeWidening lets Delta accept widened column types (e.g. decimal
|
|
16
|
-
# scale bumps) from upstream schema drift without a manual ALTER + full refresh.
|
|
17
|
-
# Applied to every bronze and silver table so future source-side type changes
|
|
18
|
-
# flow through the pipeline dynamically.
|
|
19
|
-
_DEFAULT_TABLE_PROPERTIES = {
|
|
20
|
-
"delta.autoOptimize.optimizeWrite": "true",
|
|
21
|
-
"delta.autoOptimize.autoCompact": "true",
|
|
22
|
-
"delta.enableTypeWidening": "true",
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _build_table_properties(extra: Optional[dict] = None,
|
|
27
|
-
column_comments: Optional[dict] = None) -> dict:
|
|
28
|
-
props = dict(_DEFAULT_TABLE_PROPERTIES)
|
|
29
|
-
if column_comments is not None:
|
|
30
|
-
props["column_comments"] = json.dumps(column_comments) if column_comments else "{}"
|
|
31
|
-
if extra:
|
|
32
|
-
props.update(extra)
|
|
33
|
-
return props
|
|
34
|
-
|
|
35
|
-
|
|
36
15
|
def _pick_latest_source_file(spark, files_path: str) -> Optional[str]:
|
|
37
16
|
"""Return the path of the most recently modified source file, or None.
|
|
38
17
|
|
|
@@ -137,7 +116,7 @@ def create_bronze_table_definition(spark,
|
|
|
137
116
|
use_managed_file_events: bool,
|
|
138
117
|
extra_table_properties: Optional[dict] = None,
|
|
139
118
|
):
|
|
140
|
-
table_properties =
|
|
119
|
+
table_properties = build_table_properties(extra_table_properties)
|
|
141
120
|
|
|
142
121
|
# Resolve the schemaHints outside the @dlt.table body so that the
|
|
143
122
|
# collect() inside _pick_latest_source_file runs once at pipeline
|
|
@@ -208,7 +187,7 @@ def silver_streaming_process(dlt,
|
|
|
208
187
|
cluster_by_value: list = None,
|
|
209
188
|
is_cluster_by_auto: bool = True,
|
|
210
189
|
extra_table_properties: Optional[dict] = None):
|
|
211
|
-
table_properties =
|
|
190
|
+
table_properties = build_table_properties(
|
|
212
191
|
extra_table_properties,
|
|
213
192
|
column_comments=column_comments,
|
|
214
193
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import json
|
|
2
1
|
from typing import Callable, Optional
|
|
3
2
|
|
|
4
3
|
from pyspark.sql import DataFrame
|
|
5
4
|
from pyspark.sql.functions import col, struct
|
|
6
5
|
|
|
7
6
|
from .dlt_autoloader_recovery_configuration import resolve_autoloader_recovery
|
|
7
|
+
from .dlt_table_properties import build_table_properties
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def base_json_replication_process(
|
|
@@ -29,6 +29,7 @@ def base_json_replication_process(
|
|
|
29
29
|
is_cluster_by_auto = table.get("is_cluster_by_auto", False)
|
|
30
30
|
keys = table.get("keys")
|
|
31
31
|
sequence_by_columns = table.get("sequence_by_columns")
|
|
32
|
+
extra_table_properties = table.get("table_properties") or {}
|
|
32
33
|
|
|
33
34
|
if bronze_schema and silver_schema:
|
|
34
35
|
bronze_table_name = f"{bronze_schema}.{dataset_name}"
|
|
@@ -47,6 +48,7 @@ def base_json_replication_process(
|
|
|
47
48
|
schema_hints=schema_hints,
|
|
48
49
|
partitions=bronze_partitions,
|
|
49
50
|
use_managed_file_events=use_managed_file_events,
|
|
51
|
+
extra_table_properties=extra_table_properties,
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
if keys and sequence_by_columns:
|
|
@@ -64,6 +66,7 @@ def base_json_replication_process(
|
|
|
64
66
|
is_cluster_by_auto=is_cluster_by_auto,
|
|
65
67
|
keys=keys,
|
|
66
68
|
sequence_by_columns=sequence_by_columns,
|
|
69
|
+
extra_table_properties=extra_table_properties,
|
|
67
70
|
)
|
|
68
71
|
else:
|
|
69
72
|
create_silver_json_table_definition(
|
|
@@ -78,6 +81,7 @@ def base_json_replication_process(
|
|
|
78
81
|
column_comments=column_comments,
|
|
79
82
|
cluster_by_value=cluster_by_value,
|
|
80
83
|
is_cluster_by_auto=is_cluster_by_auto,
|
|
84
|
+
extra_table_properties=extra_table_properties,
|
|
81
85
|
)
|
|
82
86
|
|
|
83
87
|
|
|
@@ -87,6 +91,7 @@ def _build_autoloader_reader(
|
|
|
87
91
|
file_format: str,
|
|
88
92
|
schema_hints: Optional[str],
|
|
89
93
|
use_managed_file_events: bool,
|
|
94
|
+
include_source_file: bool = False,
|
|
90
95
|
):
|
|
91
96
|
reader = (
|
|
92
97
|
spark.readStream
|
|
@@ -108,7 +113,15 @@ def _build_autoloader_reader(
|
|
|
108
113
|
.option("cloudFiles.validateOptions", "false")
|
|
109
114
|
)
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
loaded = reader.load(source_path)
|
|
117
|
+
|
|
118
|
+
if include_source_file:
|
|
119
|
+
# Which file a row was read from. Off by default because it adds a
|
|
120
|
+
# column, and a column added to an existing bronze table is a schema
|
|
121
|
+
# change for everything already reading it.
|
|
122
|
+
loaded = loaded.withColumn("source_file", col("_metadata.file_path"))
|
|
123
|
+
|
|
124
|
+
return loaded
|
|
112
125
|
|
|
113
126
|
|
|
114
127
|
def create_bronze_json_table_definition(
|
|
@@ -121,11 +134,14 @@ def create_bronze_json_table_definition(
|
|
|
121
134
|
schema_hints: Optional[str],
|
|
122
135
|
partitions: Optional[list],
|
|
123
136
|
use_managed_file_events: bool,
|
|
137
|
+
extra_table_properties: Optional[dict] = None,
|
|
138
|
+
include_source_file: bool = False,
|
|
124
139
|
):
|
|
125
140
|
table_kwargs = {
|
|
126
141
|
"name": table_name,
|
|
127
142
|
"comment": f"Raw JSON stream data for {dataset_name}.",
|
|
128
143
|
"temporary": False,
|
|
144
|
+
"table_properties": build_table_properties(extra_table_properties),
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
if partitions:
|
|
@@ -139,6 +155,7 @@ def create_bronze_json_table_definition(
|
|
|
139
155
|
file_format=file_format,
|
|
140
156
|
schema_hints=schema_hints,
|
|
141
157
|
use_managed_file_events=use_managed_file_events,
|
|
158
|
+
include_source_file=include_source_file,
|
|
142
159
|
)
|
|
143
160
|
|
|
144
161
|
return transform_json_to_bronze
|
|
@@ -156,17 +173,18 @@ def create_silver_json_table_definition(
|
|
|
156
173
|
column_comments: Optional[dict] = None,
|
|
157
174
|
cluster_by_value: Optional[list] = None,
|
|
158
175
|
is_cluster_by_auto: bool = False,
|
|
176
|
+
extra_table_properties: Optional[dict] = None,
|
|
159
177
|
):
|
|
160
178
|
supports_clustering = not partitions
|
|
161
179
|
table_kwargs = {
|
|
162
180
|
"name": table_name,
|
|
163
181
|
"comment": table_description or f"Structured JSON stream data for {dataset_name}.",
|
|
164
182
|
"temporary": False,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
183
|
+
# `or {}` rather than passing None through: the JSON path has always
|
|
184
|
+
# written a column_comments property even when it had none, and dropping
|
|
185
|
+
# the key would change it on every existing silver table. The CDC path
|
|
186
|
+
# omits it instead, which is why the two calls differ here.
|
|
187
|
+
"table_properties": build_table_properties(extra_table_properties, column_comments=column_comments or {}),
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
if partitions:
|
|
@@ -200,6 +218,7 @@ def create_silver_json_cdc_definition(
|
|
|
200
218
|
column_comments: Optional[dict] = None,
|
|
201
219
|
cluster_by_value: Optional[list] = None,
|
|
202
220
|
is_cluster_by_auto: bool = False,
|
|
221
|
+
extra_table_properties: Optional[dict] = None,
|
|
203
222
|
):
|
|
204
223
|
staging_view_name = f"{dataset_name}_cdc_source"
|
|
205
224
|
supports_clustering = not partitions
|
|
@@ -211,11 +230,8 @@ def create_silver_json_cdc_definition(
|
|
|
211
230
|
|
|
212
231
|
dlt.create_streaming_table(
|
|
213
232
|
name=table_name,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"delta.autoOptimize.autoCompact": "true",
|
|
217
|
-
"column_comments": json.dumps(column_comments) if column_comments else "{}",
|
|
218
|
-
},
|
|
233
|
+
# See the note in create_silver_json_table_definition on `or {}`.
|
|
234
|
+
table_properties=build_table_properties(extra_table_properties, column_comments=column_comments or {}),
|
|
219
235
|
comment=table_description or f"Structured JSON stream data for {dataset_name}.",
|
|
220
236
|
partition_cols=partitions if partitions else None,
|
|
221
237
|
cluster_by=cluster_by_value if supports_clustering else None,
|
|
@@ -141,3 +141,63 @@ def test_base_json_replication_process_enables_managed_file_events(monkeypatch):
|
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
assert captured_bronze_kwargs["use_managed_file_events"] is True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_bronze_and_silver_enable_type_widening_by_default():
|
|
147
|
+
dlt_spy = _DltSpy()
|
|
148
|
+
|
|
149
|
+
main_json_utils.base_json_replication_process(
|
|
150
|
+
dlt=dlt_spy,
|
|
151
|
+
spark=object(),
|
|
152
|
+
json_tables_map=[_build_json_table()],
|
|
153
|
+
bronze_schema="bifrost_bronze",
|
|
154
|
+
silver_schema="bifrost_silver",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Bronze carried no table properties at all before this; a Kafka-sourced
|
|
158
|
+
# table needed a full refresh for a widening the CDC tables absorbed.
|
|
159
|
+
for table_kwargs in dlt_spy.decorated_tables:
|
|
160
|
+
properties = table_kwargs["table_properties"]
|
|
161
|
+
assert properties["delta.enableTypeWidening"] == "true"
|
|
162
|
+
assert properties["delta.autoOptimize.optimizeWrite"] == "true"
|
|
163
|
+
assert properties["delta.autoOptimize.autoCompact"] == "true"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_cdc_silver_enables_type_widening_by_default(monkeypatch):
|
|
167
|
+
monkeypatch.setattr(main_json_utils, "col", lambda value: value)
|
|
168
|
+
monkeypatch.setattr(main_json_utils, "struct", lambda *values: values)
|
|
169
|
+
|
|
170
|
+
dlt_spy = _DltSpy()
|
|
171
|
+
json_table = dict(_build_json_table(), keys=["id"], sequence_by_columns=["kafka_timestamp"])
|
|
172
|
+
|
|
173
|
+
main_json_utils.base_json_replication_process(
|
|
174
|
+
dlt=dlt_spy,
|
|
175
|
+
spark=object(),
|
|
176
|
+
json_tables_map=[json_table],
|
|
177
|
+
bronze_schema="bifrost_bronze",
|
|
178
|
+
silver_schema="bifrost_silver",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert dlt_spy.streaming_tables[0]["table_properties"]["delta.enableTypeWidening"] == "true"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_table_properties_override_the_library_defaults():
|
|
185
|
+
dlt_spy = _DltSpy()
|
|
186
|
+
json_table = dict(
|
|
187
|
+
_build_json_table(),
|
|
188
|
+
table_properties={"delta.enableTypeWidening": "false", "my.custom.prop": "42"},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
main_json_utils.base_json_replication_process(
|
|
192
|
+
dlt=dlt_spy,
|
|
193
|
+
spark=object(),
|
|
194
|
+
json_tables_map=[json_table],
|
|
195
|
+
bronze_schema="bifrost_bronze",
|
|
196
|
+
silver_schema="bifrost_silver",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
for table_kwargs in dlt_spy.decorated_tables:
|
|
200
|
+
properties = table_kwargs["table_properties"]
|
|
201
|
+
# Caller override wins over the library default.
|
|
202
|
+
assert properties["delta.enableTypeWidening"] == "false"
|
|
203
|
+
assert properties["my.custom.prop"] == "42"
|
|
@@ -7,10 +7,18 @@ from pyspark.sql.functions import expr
|
|
|
7
7
|
from pyspark.sql.types import TimestampType, StringType
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from dlt_utils.dlt_transformations import rename_columns, apply_partitions, update_cdc_timestamp, add_default_value_for_removed_col, rename_columns
|
|
10
|
-
# Mock databricks.sdk.runtime if not in a Databricks environment
|
|
10
|
+
# Mock databricks.sdk.runtime if not in a Databricks environment.
|
|
11
|
+
#
|
|
12
|
+
# Catching Exception and not ImportError: importing this module builds a
|
|
13
|
+
# Config and tries to authenticate, so off a cluster it fails in whatever way
|
|
14
|
+
# the surrounding credentials happen to be wrong. In CI that is
|
|
15
|
+
# "more than one authorization method configured: oauth and pat", because the
|
|
16
|
+
# project still carries a PAT next to the OAuth variables. On a laptop it is
|
|
17
|
+
# usually several matching profiles in ~/.databrickscfg. Both mean the same
|
|
18
|
+
# thing here, which is that we are not on a cluster and should mock.
|
|
11
19
|
try:
|
|
12
20
|
from databricks.sdk.runtime import *
|
|
13
|
-
except
|
|
21
|
+
except Exception:
|
|
14
22
|
print("Databricks runtime not available. Mocking required modules.")
|
|
15
23
|
mock_dbutils = mock.Mock()
|
|
16
24
|
sys.modules['databricks.sdk.runtime'] = mock.Mock()
|
|
File without changes
|
{dlt_utils_lib-1.2.7 → dlt_utils_lib-1.3.0}/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
|