dbt-common 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

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.
dbt_common/__about__.py CHANGED
@@ -1 +1 @@
1
- version = "1.3.0"
1
+ version = "1.5.0"
@@ -62,7 +62,9 @@ class FindMatchingParams:
62
62
  # Do not record or replay filesystem searches that were performed against
63
63
  # files which are actually part of dbt's implementation.
64
64
  return (
65
- "dbt/include/global_project" not in self.root_path
65
+ "dbt/include"
66
+ not in self.root_path # TODO: This actually obviates the next two checks but is probably too coarse?
67
+ and "dbt/include/global_project" not in self.root_path
66
68
  and "/plugins/postgres/dbt/include/" not in self.root_path
67
69
  )
68
70
 
dbt_common/record.py CHANGED
@@ -2,15 +2,17 @@
2
2
  external systems during a command invocation, so that the command can be re-run
3
3
  later with the recording 'replayed' to dbt.
4
4
 
5
- The rationale for and architecture of this module is described in detail in the
5
+ The rationale for and architecture of this module are described in detail in the
6
6
  docs/guides/record_replay.md document in this repository.
7
7
  """
8
8
  import functools
9
9
  import dataclasses
10
10
  import json
11
11
  import os
12
+
13
+ from deepdiff import DeepDiff # type: ignore
12
14
  from enum import Enum
13
- from typing import Any, Dict, List, Mapping, Optional, Type
15
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Type
14
16
 
15
17
  from dbt_common.context import get_invocation_context
16
18
 
@@ -51,15 +53,68 @@ class Record:
51
53
 
52
54
 
53
55
  class Diff:
54
- """Marker class for diffs?"""
56
+ def __init__(self, current_recording_path: str, previous_recording_path: str) -> None:
57
+ self.current_recording_path = current_recording_path
58
+ self.previous_recording_path = previous_recording_path
59
+
60
+ def diff_query_records(self, current: List, previous: List) -> Dict[str, Any]:
61
+ # some of the table results are returned as a stringified list of dicts that don't
62
+ # diff because order isn't consistent. convert it into a list of dicts so it can
63
+ # be diffed ignoring order
64
+
65
+ for i in range(len(current)):
66
+ if current[i].get("result").get("table") is not None:
67
+ current[i]["result"]["table"] = json.loads(current[i]["result"]["table"])
68
+ for i in range(len(previous)):
69
+ if previous[i].get("result").get("table") is not None:
70
+ previous[i]["result"]["table"] = json.loads(previous[i]["result"]["table"])
71
+
72
+ return DeepDiff(previous, current, ignore_order=True, verbose_level=2)
73
+
74
+ def diff_env_records(self, current: List, previous: List) -> Dict[str, Any]:
75
+ # The mode and filepath may change. Ignore them.
76
+
77
+ exclude_paths = [
78
+ "root[0]['result']['env']['DBT_RECORDER_FILE_PATH']",
79
+ "root[0]['result']['env']['DBT_RECORDER_MODE']",
80
+ ]
81
+
82
+ return DeepDiff(
83
+ previous, current, ignore_order=True, verbose_level=2, exclude_paths=exclude_paths
84
+ )
85
+
86
+ def diff_default(self, current: List, previous: List) -> Dict[str, Any]:
87
+ return DeepDiff(previous, current, ignore_order=True, verbose_level=2)
88
+
89
+ def calculate_diff(self) -> Dict[str, Any]:
90
+ with open(self.current_recording_path) as current_recording:
91
+ current_dct = json.load(current_recording)
55
92
 
56
- pass
93
+ with open(self.previous_recording_path) as previous_recording:
94
+ previous_dct = json.load(previous_recording)
95
+
96
+ diff = {}
97
+ for record_type in current_dct:
98
+ if record_type == "QueryRecord":
99
+ diff[record_type] = self.diff_query_records(
100
+ current_dct[record_type], previous_dct[record_type]
101
+ )
102
+ elif record_type == "GetEnvRecord":
103
+ diff[record_type] = self.diff_env_records(
104
+ current_dct[record_type], previous_dct[record_type]
105
+ )
106
+ else:
107
+ diff[record_type] = self.diff_default(
108
+ current_dct[record_type], previous_dct[record_type]
109
+ )
110
+
111
+ return diff
57
112
 
58
113
 
59
114
  class RecorderMode(Enum):
60
115
  RECORD = 1
61
116
  REPLAY = 2
62
- RECORD_QUERIES = 3
117
+ DIFF = 3 # records and does diffing
63
118
 
64
119
 
65
120
  class Recorder:
@@ -67,15 +122,32 @@ class Recorder:
67
122
  _record_name_by_params_name: Dict[str, str] = {}
68
123
 
69
124
  def __init__(
70
- self, mode: RecorderMode, types: Optional[List], recording_path: Optional[str] = None
125
+ self,
126
+ mode: RecorderMode,
127
+ types: Optional[List],
128
+ current_recording_path: str = "recording.json",
129
+ previous_recording_path: Optional[str] = None,
71
130
  ) -> None:
72
131
  self.mode = mode
73
- self.types = types
132
+ self.recorded_types = types
74
133
  self._records_by_type: Dict[str, List[Record]] = {}
134
+ self._unprocessed_records_by_type: Dict[str, List[Dict[str, Any]]] = {}
75
135
  self._replay_diffs: List["Diff"] = []
136
+ self.diff: Optional[Diff] = None
137
+ self.previous_recording_path = previous_recording_path
138
+ self.current_recording_path = current_recording_path
139
+
140
+ if self.previous_recording_path is not None and self.mode in (
141
+ RecorderMode.REPLAY,
142
+ RecorderMode.DIFF,
143
+ ):
144
+ self.diff = Diff(
145
+ current_recording_path=self.current_recording_path,
146
+ previous_recording_path=self.previous_recording_path,
147
+ )
76
148
 
77
- if recording_path is not None:
78
- self._records_by_type = self.load(recording_path)
149
+ if self.mode == RecorderMode.REPLAY:
150
+ self._unprocessed_records_by_type = self.load(self.previous_recording_path)
79
151
 
80
152
  @classmethod
81
153
  def register_record_type(cls, rec_type) -> Any:
@@ -90,7 +162,14 @@ class Recorder:
90
162
  self._records_by_type[rec_cls_name].append(record)
91
163
 
92
164
  def pop_matching_record(self, params: Any) -> Optional[Record]:
93
- rec_type_name = self._record_name_by_params_name[type(params).__name__]
165
+ rec_type_name = self._record_name_by_params_name.get(type(params).__name__)
166
+
167
+ if rec_type_name is None:
168
+ raise Exception(
169
+ f"A record of type {type(params).__name__} was requested, but no such type has been registered."
170
+ )
171
+
172
+ self._ensure_records_processed(rec_type_name)
94
173
  records = self._records_by_type[rec_type_name]
95
174
  match: Optional[Record] = None
96
175
  for rec in records:
@@ -101,8 +180,8 @@ class Recorder:
101
180
 
102
181
  return match
103
182
 
104
- def write(self, file_name) -> None:
105
- with open(file_name, "w") as file:
183
+ def write(self) -> None:
184
+ with open(self.current_recording_path, "w") as file:
106
185
  json.dump(self._to_dict(), file)
107
186
 
108
187
  def _to_dict(self) -> Dict:
@@ -115,22 +194,20 @@ class Recorder:
115
194
  return dct
116
195
 
117
196
  @classmethod
118
- def load(cls, file_name: str) -> Dict[str, List[Record]]:
197
+ def load(cls, file_name: str) -> Dict[str, List[Dict[str, Any]]]:
119
198
  with open(file_name) as file:
120
- loaded_dct = json.load(file)
199
+ return json.load(file)
121
200
 
122
- records_by_type: Dict[str, List[Record]] = {}
201
+ def _ensure_records_processed(self, record_type_name: str) -> None:
202
+ if record_type_name in self._records_by_type:
203
+ return
123
204
 
124
- for record_type_name in loaded_dct:
125
- # TODO: this breaks with QueryRecord on replay since it's
126
- # not in common so isn't part of cls._record_cls_by_name yet
127
- record_cls = cls._record_cls_by_name[record_type_name]
128
- rec_list = []
129
- for record_dct in loaded_dct[record_type_name]:
130
- rec = record_cls.from_dict(record_dct)
131
- rec_list.append(rec) # type: ignore
132
- records_by_type[record_type_name] = rec_list
133
- return records_by_type
205
+ rec_list = []
206
+ record_cls = self._record_cls_by_name[record_type_name]
207
+ for record_dct in self._unprocessed_records_by_type[record_type_name]:
208
+ rec = record_cls.from_dict(record_dct)
209
+ rec_list.append(rec) # type: ignore
210
+ self._records_by_type[record_type_name] = rec_list
134
211
 
135
212
  def expect_record(self, params: Any) -> Any:
136
213
  record = self.pop_matching_record(params)
@@ -138,24 +215,27 @@ class Recorder:
138
215
  if record is None:
139
216
  raise Exception()
140
217
 
218
+ if record.result is None:
219
+ return None
220
+
141
221
  result_tuple = dataclasses.astuple(record.result)
142
222
  return result_tuple[0] if len(result_tuple) == 1 else result_tuple
143
223
 
144
224
  def write_diffs(self, diff_file_name) -> None:
145
- json.dump(
146
- self._replay_diffs,
147
- open(diff_file_name, "w"),
148
- )
225
+ assert self.diff is not None
226
+ with open(diff_file_name, "w") as f:
227
+ json.dump(self.diff.calculate_diff(), f)
149
228
 
150
229
  def print_diffs(self) -> None:
151
- print(repr(self._replay_diffs))
230
+ assert self.diff is not None
231
+ print(repr(self.diff.calculate_diff()))
152
232
 
153
233
 
154
234
  def get_record_mode_from_env() -> Optional[RecorderMode]:
155
235
  """
156
236
  Get the record mode from the environment variables.
157
237
 
158
- If the mode is not set to 'RECORD' or 'REPLAY', return None.
238
+ If the mode is not set to 'RECORD', 'DIFF' or 'REPLAY', return None.
159
239
  Expected format: 'DBT_RECORDER_MODE=RECORD'
160
240
  """
161
241
  record_mode = os.environ.get("DBT_RECORDER_MODE")
@@ -165,6 +245,9 @@ def get_record_mode_from_env() -> Optional[RecorderMode]:
165
245
 
166
246
  if record_mode.lower() == "record":
167
247
  return RecorderMode.RECORD
248
+ # diffing requires a file path, otherwise treat as noop
249
+ elif record_mode.lower() == "diff" and os.environ.get("DBT_RECORDER_FILE_PATH") is not None:
250
+ return RecorderMode.DIFF
168
251
  # replaying requires a file path, otherwise treat as noop
169
252
  elif record_mode.lower() == "replay" and os.environ.get("DBT_RECORDER_FILE_PATH") is not None:
170
253
  return RecorderMode.REPLAY
@@ -190,7 +273,21 @@ def get_record_types_from_env() -> Optional[List]:
190
273
  return record_types_str.split(",")
191
274
 
192
275
 
193
- def record_function(record_type, method=False, tuple_result=False):
276
+ def get_record_types_from_dict(fp: str) -> List:
277
+ """
278
+ Get the record subset from the dict.
279
+ """
280
+ with open(fp) as file:
281
+ loaded_dct = json.load(file)
282
+ return list(loaded_dct.keys())
283
+
284
+
285
+ def record_function(
286
+ record_type,
287
+ method: bool = False,
288
+ tuple_result: bool = False,
289
+ id_field_name: Optional[str] = None,
290
+ ) -> Callable:
194
291
  def record_function_inner(func_to_record):
195
292
  # To avoid runtime overhead and other unpleasantness, we only apply the
196
293
  # record/replay decorator if a relevant env var is set.
@@ -208,12 +305,17 @@ def record_function(record_type, method=False, tuple_result=False):
208
305
  if recorder is None:
209
306
  return func_to_record(*args, **kwargs)
210
307
 
211
- if recorder.types is not None and record_type.__name__ not in recorder.types:
308
+ if (
309
+ recorder.recorded_types is not None
310
+ and record_type.__name__ not in recorder.recorded_types
311
+ ):
212
312
  return func_to_record(*args, **kwargs)
213
313
 
214
314
  # For methods, peel off the 'self' argument before calling the
215
315
  # params constructor.
216
316
  param_args = args[1:] if method else args
317
+ if method and id_field_name is not None:
318
+ param_args = (getattr(args[0], id_field_name),) + param_args
217
319
 
218
320
  params = record_type.params_cls(*param_args, **kwargs)
219
321
 
@@ -230,7 +332,7 @@ def record_function(record_type, method=False, tuple_result=False):
230
332
  r = func_to_record(*args, **kwargs)
231
333
  result = (
232
334
  None
233
- if r is None or record_type.result_cls is None
335
+ if record_type.result_cls is None
234
336
  else record_type.result_cls(*r)
235
337
  if tuple_result
236
338
  else record_type.result_cls(r)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbt-common
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: The shared common utilities that dbt-core and adapter implementations use
5
5
  Project-URL: Homepage, https://github.com/dbt-labs/dbt-common
6
6
  Project-URL: Repository, https://github.com/dbt-labs/dbt-common.git
@@ -26,6 +26,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
26
26
  Requires-Python: >=3.8
27
27
  Requires-Dist: agate<1.10,>=1.7.0
28
28
  Requires-Dist: colorama<0.5,>=0.3.9
29
+ Requires-Dist: deepdiff<8.0,>=7.0
29
30
  Requires-Dist: isodate<0.7,>=0.6
30
31
  Requires-Dist: jinja2<4,>=3.1.3
31
32
  Requires-Dist: jsonschema<5.0,>=4.0
@@ -1,4 +1,4 @@
1
- dbt_common/__about__.py,sha256=qg6ZAU0TpLWOHtkFC272q_j3aVEzW76H0lONbKcgFA8,18
1
+ dbt_common/__about__.py,sha256=Wgohrh7g_peUNIz0Cro4w7u88rZU94fNypIcm9oMWgQ,18
2
2
  dbt_common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  dbt_common/constants.py,sha256=-Y5DIL1SDPQWtlCNizXRYxFgbx1D7LaLs1ysamvGMRk,278
4
4
  dbt_common/context.py,sha256=BhgT7IgyvpZHEtIdFVVuBBBX5LuU7obXT7NvIPeuD2g,1760
@@ -6,7 +6,7 @@ dbt_common/dataclass_schema.py,sha256=t3HGD0oXTSjitctuCVHv3iyq5BT3jxoSxv_VGkrJlE
6
6
  dbt_common/helper_types.py,sha256=NoxqGFAq9bOjh7rqtz_eepXAxk20n3mmW_gUVpnMyYU,3901
7
7
  dbt_common/invocation.py,sha256=Zw8jRPn75oi2VrUD6qGvaCDtSyIfqm5pJlPpRjs3s1E,202
8
8
  dbt_common/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- dbt_common/record.py,sha256=fwp8Q2x0UiB9fHbMFkmOlAgKLst1dhDxQpSSsd04aDw,8204
9
+ dbt_common/record.py,sha256=F-FbFt6Js_U4r5b5hXGp8DY-MTEKNb-dLMaBT80UWHw,12415
10
10
  dbt_common/semver.py,sha256=2zoZYCQ7PfswqslT2NHuMGgPGMuMuX-yRThVoqfDWQU,13954
11
11
  dbt_common/tests.py,sha256=6lC_JuRtoYO6cbAF8-R5aTM4HtQiM_EH8X5m_97duGY,315
12
12
  dbt_common/ui.py,sha256=rc2TEM29raBFc_LXcg901pMDD07C2ohwp9qzkE-7pBY,2567
@@ -14,7 +14,7 @@ dbt_common/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
14
14
  dbt_common/clients/_jinja_blocks.py,sha256=xoJK9Y0F93U2PKfT_3SJbBopCGYCtl7LiwKuylXnrEE,12947
15
15
  dbt_common/clients/agate_helper.py,sha256=n5Q0_gJPbBhFvjd286NGYGlcTtdEExYmIT3968lppyg,9124
16
16
  dbt_common/clients/jinja.py,sha256=i6VQ94FU4F6ZCQLHTxNSeGHmvyYSIe34nDhNkH6wO08,18502
17
- dbt_common/clients/system.py,sha256=OOhRDWR5t0Ns3OhkqjPTNTtyl_RMRWPDHWCzDoFtgkA,23014
17
+ dbt_common/clients/system.py,sha256=K-b9Lx8ZhgR5tKq1vnqbX8sEVaVRQHqkEkLYQIjFc64,23158
18
18
  dbt_common/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  dbt_common/contracts/constraints.py,sha256=hyqTW2oPB1dfXWW388LWnL-EFdqTpQciKISH3CeLkro,1267
20
20
  dbt_common/contracts/metadata.py,sha256=K_M06Rue0wmrQhFP_mq3uvQszq10CIt93oGiAVgbRfE,1293
@@ -56,7 +56,7 @@ dbt_common/utils/encoding.py,sha256=6_kSY2FvGNYMg7oX7PrbvVioieydih3Kl7Ii802LaHI,
56
56
  dbt_common/utils/executor.py,sha256=Zyzd1wML3aN-iYn9ZG2Gc_jj5vknmvQNyH-c0RaPIpo,2446
57
57
  dbt_common/utils/formatting.py,sha256=JUn5rzJ-uajs9wPCN0-f2iRFY1pOJF5YjTD9dERuLoc,165
58
58
  dbt_common/utils/jinja.py,sha256=XNfZHuZhLM_R_yPmzYojPm6bF7QOoxIjSWrkJRw6wks,965
59
- dbt_common-1.3.0.dist-info/METADATA,sha256=WLyv25ll2nMlvoBBwbM8rrEIyB6MwbUTpT69REVxOg4,5264
60
- dbt_common-1.3.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
61
- dbt_common-1.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
62
- dbt_common-1.3.0.dist-info/RECORD,,
59
+ dbt_common-1.5.0.dist-info/METADATA,sha256=phlsK2cVVw1AWNpJa2_uNaM2KG5HroA0RZw4gUsMCFQ,5298
60
+ dbt_common-1.5.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
61
+ dbt_common-1.5.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
62
+ dbt_common-1.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.24.2
2
+ Generator: hatchling 1.25.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any