collate-data-diff 0.11.2__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.
- collate_data_diff-0.11.2.dist-info/LICENSE +18 -0
- collate_data_diff-0.11.2.dist-info/METADATA +77 -0
- collate_data_diff-0.11.2.dist-info/RECORD +54 -0
- collate_data_diff-0.11.2.dist-info/WHEEL +4 -0
- collate_data_diff-0.11.2.dist-info/entry_points.txt +3 -0
- data_diff/__init__.py +180 -0
- data_diff/__main__.py +618 -0
- data_diff/abcs/__init__.py +0 -0
- data_diff/abcs/compiler.py +13 -0
- data_diff/abcs/database_types.py +308 -0
- data_diff/cloud/__init__.py +2 -0
- data_diff/cloud/data_source.py +318 -0
- data_diff/cloud/datafold_api.py +304 -0
- data_diff/config.py +127 -0
- data_diff/databases/__init__.py +17 -0
- data_diff/databases/_connect.py +306 -0
- data_diff/databases/base.py +1291 -0
- data_diff/databases/bigquery.py +315 -0
- data_diff/databases/clickhouse.py +203 -0
- data_diff/databases/databricks.py +248 -0
- data_diff/databases/duckdb.py +192 -0
- data_diff/databases/mssql.py +229 -0
- data_diff/databases/mysql.py +159 -0
- data_diff/databases/oracle.py +195 -0
- data_diff/databases/postgresql.py +258 -0
- data_diff/databases/presto.py +197 -0
- data_diff/databases/redshift.py +217 -0
- data_diff/databases/snowflake.py +207 -0
- data_diff/databases/trino.py +50 -0
- data_diff/databases/vertica.py +160 -0
- data_diff/dbt.py +604 -0
- data_diff/dbt_config_validators.py +65 -0
- data_diff/dbt_parser.py +523 -0
- data_diff/diff_tables.py +416 -0
- data_diff/errors.py +74 -0
- data_diff/format.py +359 -0
- data_diff/hashdiff_tables.py +264 -0
- data_diff/info_tree.py +62 -0
- data_diff/joindiff_tables.py +399 -0
- data_diff/lexicographic_space.py +240 -0
- data_diff/parse_time.py +74 -0
- data_diff/py.typed +0 -0
- data_diff/queries/__init__.py +0 -0
- data_diff/queries/api.py +200 -0
- data_diff/queries/ast_classes.py +798 -0
- data_diff/queries/base.py +24 -0
- data_diff/queries/extras.py +29 -0
- data_diff/query_utils.py +56 -0
- data_diff/schema.py +52 -0
- data_diff/table_segment.py +286 -0
- data_diff/thread_utils.py +98 -0
- data_diff/tracking.py +237 -0
- data_diff/utils.py +625 -0
- data_diff/version.py +1 -0
data_diff/tracking.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This module contains all the functionality related to the anonymous tracking of data-diff use.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import platform
|
|
9
|
+
from time import time
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
import urllib.request
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
import toml
|
|
14
|
+
from rich import get_console
|
|
15
|
+
|
|
16
|
+
from data_diff.version import __version__
|
|
17
|
+
|
|
18
|
+
TRACK_URL = "https://hosted.rudderlabs.com/v1/track"
|
|
19
|
+
START_EVENT = "os_diff_run_start"
|
|
20
|
+
END_EVENT = "os_diff_run_end"
|
|
21
|
+
TOKEN = "2HgtM4Hcq9BmeiCqNYhz7O9tkjM"
|
|
22
|
+
TIMEOUT = 8
|
|
23
|
+
|
|
24
|
+
DEFAULT_PROFILE = os.path.expanduser("~/.datadiff.toml")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_profile():
|
|
28
|
+
try:
|
|
29
|
+
with open(DEFAULT_PROFILE) as f:
|
|
30
|
+
conf = toml.load(f)
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
conf = {}
|
|
33
|
+
|
|
34
|
+
if "anonymous_id" not in conf:
|
|
35
|
+
conf["anonymous_id"] = str(uuid4())
|
|
36
|
+
with open(DEFAULT_PROFILE, "w") as f:
|
|
37
|
+
toml.dump(conf, f)
|
|
38
|
+
return conf
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bool_ask_for_email() -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Checks the .datadiff.toml profile file for the asked_for_email key
|
|
44
|
+
|
|
45
|
+
Returns False immediately if --no-tracking or not in an interactive terminal
|
|
46
|
+
|
|
47
|
+
If found, return False (already asked for email)
|
|
48
|
+
|
|
49
|
+
If not found, add a key "asked_for_email", and return True (we should ask for email)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
bool: decision on whether to prompt the user for their email
|
|
53
|
+
"""
|
|
54
|
+
console = get_console()
|
|
55
|
+
if g_tracking_enabled and console.is_interactive:
|
|
56
|
+
profile = _load_profile()
|
|
57
|
+
|
|
58
|
+
if "asked_for_email" not in profile:
|
|
59
|
+
profile["asked_for_email"] = ""
|
|
60
|
+
with open(DEFAULT_PROFILE, "w") as conf:
|
|
61
|
+
toml.dump(profile, conf)
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def bool_notify_about_extension() -> bool:
|
|
67
|
+
profile = _load_profile()
|
|
68
|
+
console = get_console()
|
|
69
|
+
if "notified_about_extension" not in profile and console.is_interactive:
|
|
70
|
+
profile["notified_about_extension"] = ""
|
|
71
|
+
with open(DEFAULT_PROFILE, "w") as conf:
|
|
72
|
+
toml.dump(profile, conf)
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
g_tracking_enabled = True
|
|
78
|
+
g_anonymous_id = None
|
|
79
|
+
|
|
80
|
+
entrypoint_name = "Python API"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def disable_tracking() -> None:
|
|
84
|
+
global g_tracking_enabled
|
|
85
|
+
g_tracking_enabled = False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_tracking_enabled() -> bool:
|
|
89
|
+
return g_tracking_enabled
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_entrypoint_name(s) -> None:
|
|
93
|
+
global entrypoint_name
|
|
94
|
+
entrypoint_name = s
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
dbt_user_id = None
|
|
98
|
+
dbt_version = None
|
|
99
|
+
dbt_project_id = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_dbt_user_id(s) -> None:
|
|
103
|
+
global dbt_user_id
|
|
104
|
+
dbt_user_id = s
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def set_dbt_version(s) -> None:
|
|
108
|
+
global dbt_version
|
|
109
|
+
dbt_version = s
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def set_dbt_project_id(s) -> None:
|
|
113
|
+
global dbt_project_id
|
|
114
|
+
dbt_project_id = s
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_anonymous_id() -> str:
|
|
118
|
+
global g_anonymous_id
|
|
119
|
+
if g_anonymous_id is None:
|
|
120
|
+
profile = _load_profile()
|
|
121
|
+
g_anonymous_id = profile["anonymous_id"]
|
|
122
|
+
return g_anonymous_id
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_start_event_json(diff_options: Dict[str, Any]):
|
|
126
|
+
return {
|
|
127
|
+
"event": "os_diff_run_start",
|
|
128
|
+
"properties": {
|
|
129
|
+
"distinct_id": get_anonymous_id(),
|
|
130
|
+
"token": TOKEN,
|
|
131
|
+
"time": time(),
|
|
132
|
+
"os_type": os.name,
|
|
133
|
+
"os_version": platform.platform(),
|
|
134
|
+
"python_version": f"{platform.python_version()}/{platform.python_implementation()}",
|
|
135
|
+
"diff_options": diff_options,
|
|
136
|
+
"data_diff_version:": __version__,
|
|
137
|
+
"entrypoint_name": entrypoint_name,
|
|
138
|
+
"dbt_user_id": dbt_user_id,
|
|
139
|
+
"dbt_version": dbt_version,
|
|
140
|
+
"dbt_project_id": dbt_project_id,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_end_event_json(
|
|
146
|
+
is_success: bool,
|
|
147
|
+
runtime_seconds: float,
|
|
148
|
+
data_source_1_type: str,
|
|
149
|
+
data_source_2_type: str,
|
|
150
|
+
table1_count: int,
|
|
151
|
+
table2_count: int,
|
|
152
|
+
diff_count: int,
|
|
153
|
+
error: Optional[str],
|
|
154
|
+
diff_id: Optional[int] = None,
|
|
155
|
+
is_cloud: bool = False,
|
|
156
|
+
org_id: Optional[int] = None,
|
|
157
|
+
org_name: Optional[str] = None,
|
|
158
|
+
user_id: Optional[int] = None,
|
|
159
|
+
):
|
|
160
|
+
return {
|
|
161
|
+
"event": "os_diff_run_end",
|
|
162
|
+
"properties": {
|
|
163
|
+
"distinct_id": get_anonymous_id(),
|
|
164
|
+
"token": TOKEN,
|
|
165
|
+
"time": time(),
|
|
166
|
+
"is_success": is_success,
|
|
167
|
+
"runtime_seconds": runtime_seconds,
|
|
168
|
+
"data_source_1_type": data_source_1_type,
|
|
169
|
+
"data_source_2_type": data_source_2_type,
|
|
170
|
+
"table_1_rows_cnt": table1_count,
|
|
171
|
+
"table_2_rows_cnt": table2_count,
|
|
172
|
+
"diff_rows_cnt": diff_count,
|
|
173
|
+
"error_message": error,
|
|
174
|
+
"data_diff_version:": __version__,
|
|
175
|
+
"entrypoint_name": entrypoint_name,
|
|
176
|
+
"is_cloud": is_cloud,
|
|
177
|
+
"diff_id": diff_id,
|
|
178
|
+
"dbt_user_id": dbt_user_id,
|
|
179
|
+
"dbt_version": dbt_version,
|
|
180
|
+
"dbt_project_id": dbt_project_id,
|
|
181
|
+
"org_id": org_id,
|
|
182
|
+
"org_name": org_name,
|
|
183
|
+
"user_id": user_id,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def create_email_signup_event_json(email: str) -> Dict[str, Any]:
|
|
189
|
+
return {
|
|
190
|
+
"event": "os_diff_email_opt_in",
|
|
191
|
+
"properties": {
|
|
192
|
+
"distinct_id": get_anonymous_id(),
|
|
193
|
+
"token": TOKEN,
|
|
194
|
+
"time": time(),
|
|
195
|
+
"data_diff_version:": __version__,
|
|
196
|
+
"entrypoint_name": entrypoint_name,
|
|
197
|
+
"email": email,
|
|
198
|
+
"dbt_user_id": dbt_user_id,
|
|
199
|
+
"dbt_project_id": dbt_project_id,
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def convert_sets_to_lists(obj):
|
|
205
|
+
"""
|
|
206
|
+
Recursively convert sets in the given object to lists.
|
|
207
|
+
"""
|
|
208
|
+
if isinstance(obj, set):
|
|
209
|
+
return list(obj)
|
|
210
|
+
elif isinstance(obj, dict):
|
|
211
|
+
return {k: convert_sets_to_lists(v) for k, v in obj.items()}
|
|
212
|
+
elif isinstance(obj, list):
|
|
213
|
+
return [convert_sets_to_lists(elem) for elem in obj]
|
|
214
|
+
else:
|
|
215
|
+
return obj
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def send_event_json(event_json) -> None:
|
|
219
|
+
if not g_tracking_enabled:
|
|
220
|
+
raise RuntimeError("Won't send; tracking is disabled!")
|
|
221
|
+
|
|
222
|
+
# Convert sets to lists in event_json
|
|
223
|
+
event_json = convert_sets_to_lists(event_json)
|
|
224
|
+
|
|
225
|
+
headers = {
|
|
226
|
+
"Content-Type": "application/json",
|
|
227
|
+
"Authorization": "Basic MkhndE00SGNxOUJtZWlDcU5ZaHo3Tzl0a2pNOg==",
|
|
228
|
+
}
|
|
229
|
+
data = json.dumps(event_json).encode()
|
|
230
|
+
try:
|
|
231
|
+
req = urllib.request.Request(TRACK_URL, data=data, headers=headers)
|
|
232
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as f:
|
|
233
|
+
res = f.read()
|
|
234
|
+
if f.code != 200:
|
|
235
|
+
raise RuntimeError(res)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logging.debug(f"Failed to post to Rudderstack: {e}")
|