dlt-utils-lib 1.2.2__tar.gz → 1.2.4__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 (20) hide show
  1. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/PKG-INFO +1 -1
  2. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/main_cdc_utils.py +46 -9
  3. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils_lib.egg-info/PKG-INFO +1 -1
  4. dlt_utils_lib-1.2.4/tests/test_main_cdc_utils.py +222 -0
  5. dlt_utils_lib-1.2.2/tests/test_main_cdc_utils.py +0 -99
  6. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/__init__.py +0 -0
  7. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/dlt_autoloader_recovery_configuration.py +0 -0
  8. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/dlt_metadata_receiver.py +0 -0
  9. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/dlt_transformations.py +0 -0
  10. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils/main_json_utils.py +0 -0
  11. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils_lib.egg-info/SOURCES.txt +0 -0
  12. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils_lib.egg-info/dependency_links.txt +0 -0
  13. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils_lib.egg-info/requires.txt +0 -0
  14. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/dlt_utils_lib.egg-info/top_level.txt +0 -0
  15. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/setup.cfg +0 -0
  16. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/setup.py +0 -0
  17. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/tests/__init__.py +0 -0
  18. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/tests/conftest.py +0 -0
  19. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/tests/test_main_json_utils.py +0 -0
  20. {dlt_utils_lib-1.2.2 → dlt_utils_lib-1.2.4}/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.2
3
+ Version: 1.2.4
4
4
  Summary: UNKNOWN
5
5
  Home-page: UNKNOWN
6
6
  License: UNKNOWN
@@ -10,6 +10,29 @@ from .dlt_transformations import (
10
10
  rename_columns,
11
11
  update_cdc_timestamp,
12
12
  )
13
+
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
+
13
36
  # Base code pipeline: streaming process
14
37
  def base_cdc_replication_process(dlt,
15
38
  spark,
@@ -38,6 +61,7 @@ def base_cdc_replication_process(dlt,
38
61
  table_description = table.get('table_description')
39
62
  column_comments = table.get('column_comments')
40
63
  partition_col = table.get('partition_col')
64
+ extra_table_properties = table.get('table_properties') or {}
41
65
 
42
66
  if bronze_schema and silver_schema:
43
67
  bronze_table_name = f"{bronze_schema}.{source_table_name}"
@@ -58,6 +82,7 @@ def base_cdc_replication_process(dlt,
58
82
  time_diff_history_cdc_timestamp=time_diff_history_cdc_timestamp,
59
83
  default_value_for_removed_col=default_value_for_removed_col,
60
84
  use_managed_file_events=use_managed_file_events,
85
+ extra_table_properties=extra_table_properties,
61
86
  )
62
87
 
63
88
  silver_streaming_process(dlt=dlt,
@@ -70,7 +95,8 @@ def base_cdc_replication_process(dlt,
70
95
  table_description=table_description,
71
96
  column_comments=column_comments,
72
97
  cluster_by_value=cluster_by_value,
73
- is_cluster_by_auto=is_cluster_by_auto
98
+ is_cluster_by_auto=is_cluster_by_auto,
99
+ extra_table_properties=extra_table_properties,
74
100
  )
75
101
 
76
102
 
@@ -86,14 +112,23 @@ def create_bronze_table_definition(spark,
86
112
  time_diff_history_cdc_timestamp: int,
87
113
  default_value_for_removed_col: dict,
88
114
  use_managed_file_events: bool,
115
+ extra_table_properties: Optional[dict] = None,
89
116
  ):
117
+ table_properties = _build_table_properties(extra_table_properties)
118
+
90
119
  @dlt.table(
91
120
  name=table_name,
92
121
  comment=f"This is the bronze table for table {source_table_name}.",
93
- temporary=False
122
+ temporary=False,
123
+ table_properties=table_properties,
94
124
  )
95
125
  def transform_cdc_to_bronze():
96
- df = spark.read.format(file_format).load(files_path).transform(rename_columns)
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)
97
132
  fields = [field for field in df.schema.fields if field.name not in schema_exclude_columns]
98
133
  schema_string = ', '.join([f"{field.name} {field.dataType.simpleString()}" for field in fields])
99
134
  reader = spark \
@@ -135,14 +170,16 @@ def silver_streaming_process(dlt,
135
170
  column_comments: dict = None,
136
171
  enable_truncate: bool = False,
137
172
  cluster_by_value: list = None,
138
- is_cluster_by_auto: bool = True):
173
+ is_cluster_by_auto: bool = True,
174
+ extra_table_properties: Optional[dict] = None):
175
+ table_properties = _build_table_properties(
176
+ extra_table_properties,
177
+ column_comments=column_comments,
178
+ )
179
+
139
180
  dlt.create_streaming_table(
140
181
  name=table_name,
141
- table_properties={
142
- "delta.autoOptimize.optimizeWrite": "true",
143
- "delta.autoOptimize.autoCompact": "true",
144
- "column_comments": json.dumps(column_comments) if column_comments else "{}"
145
- },
182
+ table_properties=table_properties,
146
183
  comment=table_description or "This is the silver table with source in",
147
184
  partition_cols=partitions if partitions else None,
148
185
  cluster_by=cluster_by_value,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dlt-utils-lib
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: UNKNOWN
5
5
  Home-page: UNKNOWN
6
6
  License: UNKNOWN
@@ -0,0 +1,222 @@
1
+ import dlt_utils.main_cdc_utils as main_cdc_utils
2
+
3
+
4
+ class _DltSpy:
5
+ def __init__(self):
6
+ self.decorated_tables = []
7
+ self.streaming_tables = []
8
+ self.apply_changes_calls = []
9
+
10
+ def table(self, **kwargs):
11
+ self.decorated_tables.append(kwargs)
12
+
13
+ def decorator(func):
14
+ return func
15
+
16
+ return decorator
17
+
18
+ def create_streaming_table(self, **kwargs):
19
+ self.streaming_tables.append(kwargs)
20
+
21
+ def apply_changes(self, **kwargs):
22
+ self.apply_changes_calls.append(kwargs)
23
+
24
+
25
+ def _build_cdc_table():
26
+ return {
27
+ "source_table_name": "customer",
28
+ "source_schema": "bifrost",
29
+ "format": "parquet",
30
+ "keys": ["id"],
31
+ "exclude_columns": [],
32
+ "enable_truncate": False,
33
+ }
34
+
35
+
36
+ def test_base_cdc_replication_process_uses_pipeline_schema_configuration(monkeypatch):
37
+ dlt_spy = _DltSpy()
38
+ spark = object()
39
+
40
+ monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
41
+ monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
42
+
43
+ main_cdc_utils.base_cdc_replication_process(
44
+ dlt=dlt_spy,
45
+ spark=spark,
46
+ cdc_tables_map=[_build_cdc_table()],
47
+ bronze_directory="dms_bronze",
48
+ bucket_name="s3://bucket",
49
+ bronze_schema="bifrost_bronze",
50
+ silver_schema="bifrost_silver",
51
+ )
52
+
53
+ assert dlt_spy.decorated_tables[0]["name"] == "bifrost_bronze.customer"
54
+ assert dlt_spy.streaming_tables[0]["name"] == "bifrost_silver.customer"
55
+ assert dlt_spy.apply_changes_calls[0]["source"] == "bifrost_bronze.customer"
56
+ assert dlt_spy.apply_changes_calls[0]["target"] == "bifrost_silver.customer"
57
+
58
+
59
+ def test_base_cdc_replication_process_uses_legacy_names_without_both_schemas(monkeypatch):
60
+ dlt_spy = _DltSpy()
61
+ spark = object()
62
+
63
+ monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
64
+ monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
65
+
66
+ main_cdc_utils.base_cdc_replication_process(
67
+ dlt=dlt_spy,
68
+ spark=spark,
69
+ cdc_tables_map=[_build_cdc_table()],
70
+ bronze_directory="dms_bronze",
71
+ bucket_name="s3://bucket",
72
+ bronze_schema="bifrost_bronze",
73
+ )
74
+
75
+ assert dlt_spy.decorated_tables[0]["name"] == "bronze_customer"
76
+ assert dlt_spy.streaming_tables[0]["name"] == "customer"
77
+ assert dlt_spy.apply_changes_calls[0]["source"] == "bronze_customer"
78
+ assert dlt_spy.apply_changes_calls[0]["target"] == "customer"
79
+
80
+
81
+ def test_base_cdc_replication_process_enables_managed_file_events(monkeypatch):
82
+ captured_bronze_kwargs = {}
83
+
84
+ def fake_create_bronze_table_definition(**kwargs):
85
+ captured_bronze_kwargs.update(kwargs)
86
+
87
+ monkeypatch.setattr(main_cdc_utils, "create_bronze_table_definition", fake_create_bronze_table_definition)
88
+ monkeypatch.setattr(main_cdc_utils, "silver_streaming_process", lambda **kwargs: None)
89
+
90
+ main_cdc_utils.base_cdc_replication_process(
91
+ dlt=object(),
92
+ spark=object(),
93
+ cdc_tables_map=[_build_cdc_table()],
94
+ bronze_directory="dms_bronze",
95
+ bucket_name="s3://bucket",
96
+ use_managed_file_events=True,
97
+ )
98
+
99
+ assert captured_bronze_kwargs["use_managed_file_events"] is True
100
+
101
+
102
+ def test_silver_streaming_process_enables_type_widening_by_default(monkeypatch):
103
+ monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
104
+ monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
105
+
106
+ dlt_spy = _DltSpy()
107
+ main_cdc_utils.silver_streaming_process(
108
+ dlt=dlt_spy,
109
+ table_name="apollo.artemis.settled_bet",
110
+ bronze_table_name="apollo.artemis.bronze_settled_bet",
111
+ keys=["id"],
112
+ partitions=[],
113
+ exclude_columns=[],
114
+ )
115
+
116
+ table_properties = dlt_spy.streaming_tables[0]["table_properties"]
117
+ assert table_properties["delta.enableTypeWidening"] == "true"
118
+ assert table_properties["delta.autoOptimize.optimizeWrite"] == "true"
119
+ assert table_properties["delta.autoOptimize.autoCompact"] == "true"
120
+
121
+
122
+ def test_silver_streaming_process_merges_extra_table_properties(monkeypatch):
123
+ monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
124
+ monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
125
+
126
+ dlt_spy = _DltSpy()
127
+ main_cdc_utils.silver_streaming_process(
128
+ dlt=dlt_spy,
129
+ table_name="apollo.artemis.settled_bet",
130
+ bronze_table_name="apollo.artemis.bronze_settled_bet",
131
+ keys=["id"],
132
+ partitions=[],
133
+ exclude_columns=[],
134
+ extra_table_properties={"delta.enableTypeWidening": "false", "my.custom.prop": "42"},
135
+ )
136
+
137
+ table_properties = dlt_spy.streaming_tables[0]["table_properties"]
138
+ # Caller override wins over the library default.
139
+ assert table_properties["delta.enableTypeWidening"] == "false"
140
+ assert table_properties["my.custom.prop"] == "42"
141
+
142
+
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
147
+
148
+ def simpleString(self):
149
+ return self._t
150
+
151
+ class _Field:
152
+ def __init__(self, name, type_str):
153
+ self.name = name
154
+ self.dataType = _DT(type_str)
155
+
156
+ class _DF:
157
+ def __init__(self, fields):
158
+ class _Schema:
159
+ pass
160
+
161
+ self.schema = _Schema()
162
+ self.schema.fields = fields
163
+
164
+ def transform(self, _fn):
165
+ return self
166
+
167
+ calls = {"options": {}, "format": None}
168
+
169
+ class _Reader:
170
+ def format(self, fmt):
171
+ calls["format"] = fmt
172
+ return self
173
+
174
+ def option(self, key, value):
175
+ calls["options"][key] = value
176
+ return self
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")])
182
+
183
+ spark = type("_Spark", (), {})()
184
+ spark.read = _Reader()
185
+
186
+ captured = {}
187
+
188
+ class _Dlt:
189
+ def table(self, **kwargs):
190
+ captured["kwargs"] = kwargs
191
+
192
+ def _decorator(fn):
193
+ captured["fn"] = fn
194
+ return fn
195
+
196
+ return _decorator
197
+
198
+ main_cdc_utils.create_bronze_table_definition(
199
+ spark=spark,
200
+ dlt=_Dlt(),
201
+ source_table_name="settled_bet",
202
+ table_name="apollo.artemis.bronze_settled_bet",
203
+ files_path="s3://bucket/dms_bronze/artemis/settled_bet/",
204
+ file_format="parquet",
205
+ partitions=[],
206
+ schema_exclude_columns=[],
207
+ time_diff_history_cdc_timestamp=30,
208
+ default_value_for_removed_col={},
209
+ use_managed_file_events=False,
210
+ )
211
+
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"
222
+ assert captured["kwargs"]["table_properties"]["delta.enableTypeWidening"] == "true"
@@ -1,99 +0,0 @@
1
- import dlt_utils.main_cdc_utils as main_cdc_utils
2
-
3
-
4
- class _DltSpy:
5
- def __init__(self):
6
- self.decorated_tables = []
7
- self.streaming_tables = []
8
- self.apply_changes_calls = []
9
-
10
- def table(self, **kwargs):
11
- self.decorated_tables.append(kwargs)
12
-
13
- def decorator(func):
14
- return func
15
-
16
- return decorator
17
-
18
- def create_streaming_table(self, **kwargs):
19
- self.streaming_tables.append(kwargs)
20
-
21
- def apply_changes(self, **kwargs):
22
- self.apply_changes_calls.append(kwargs)
23
-
24
-
25
- def _build_cdc_table():
26
- return {
27
- "source_table_name": "customer",
28
- "source_schema": "bifrost",
29
- "format": "parquet",
30
- "keys": ["id"],
31
- "exclude_columns": [],
32
- "enable_truncate": False,
33
- }
34
-
35
-
36
- def test_base_cdc_replication_process_uses_pipeline_schema_configuration(monkeypatch):
37
- dlt_spy = _DltSpy()
38
- spark = object()
39
-
40
- monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
41
- monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
42
-
43
- main_cdc_utils.base_cdc_replication_process(
44
- dlt=dlt_spy,
45
- spark=spark,
46
- cdc_tables_map=[_build_cdc_table()],
47
- bronze_directory="dms_bronze",
48
- bucket_name="s3://bucket",
49
- bronze_schema="bifrost_bronze",
50
- silver_schema="bifrost_silver",
51
- )
52
-
53
- assert dlt_spy.decorated_tables[0]["name"] == "bifrost_bronze.customer"
54
- assert dlt_spy.streaming_tables[0]["name"] == "bifrost_silver.customer"
55
- assert dlt_spy.apply_changes_calls[0]["source"] == "bifrost_bronze.customer"
56
- assert dlt_spy.apply_changes_calls[0]["target"] == "bifrost_silver.customer"
57
-
58
-
59
- def test_base_cdc_replication_process_uses_legacy_names_without_both_schemas(monkeypatch):
60
- dlt_spy = _DltSpy()
61
- spark = object()
62
-
63
- monkeypatch.setattr(main_cdc_utils, "col", lambda value: value)
64
- monkeypatch.setattr(main_cdc_utils, "expr", lambda value: value)
65
-
66
- main_cdc_utils.base_cdc_replication_process(
67
- dlt=dlt_spy,
68
- spark=spark,
69
- cdc_tables_map=[_build_cdc_table()],
70
- bronze_directory="dms_bronze",
71
- bucket_name="s3://bucket",
72
- bronze_schema="bifrost_bronze",
73
- )
74
-
75
- assert dlt_spy.decorated_tables[0]["name"] == "bronze_customer"
76
- assert dlt_spy.streaming_tables[0]["name"] == "customer"
77
- assert dlt_spy.apply_changes_calls[0]["source"] == "bronze_customer"
78
- assert dlt_spy.apply_changes_calls[0]["target"] == "customer"
79
-
80
-
81
- def test_base_cdc_replication_process_enables_managed_file_events(monkeypatch):
82
- captured_bronze_kwargs = {}
83
-
84
- def fake_create_bronze_table_definition(**kwargs):
85
- captured_bronze_kwargs.update(kwargs)
86
-
87
- monkeypatch.setattr(main_cdc_utils, "create_bronze_table_definition", fake_create_bronze_table_definition)
88
- monkeypatch.setattr(main_cdc_utils, "silver_streaming_process", lambda **kwargs: None)
89
-
90
- main_cdc_utils.base_cdc_replication_process(
91
- dlt=object(),
92
- spark=object(),
93
- cdc_tables_map=[_build_cdc_table()],
94
- bronze_directory="dms_bronze",
95
- bucket_name="s3://bucket",
96
- use_managed_file_events=True,
97
- )
98
-
99
- assert captured_bronze_kwargs["use_managed_file_events"] is True
File without changes
File without changes