castor-extractor 0.19.4__py3-none-any.whl → 0.19.7__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.
Potentially problematic release.
This version of castor-extractor might be problematic. Click here for more details.
- CHANGELOG.md +13 -0
- castor_extractor/quality/soda/client/pagination.py +1 -1
- castor_extractor/utils/__init__.py +1 -0
- castor_extractor/utils/client/__init__.py +1 -1
- castor_extractor/utils/client/api/__init__.py +1 -1
- castor_extractor/utils/client/api/client.py +33 -7
- castor_extractor/utils/client/api/pagination.py +23 -6
- castor_extractor/utils/pager/__init__.py +0 -1
- castor_extractor/utils/salesforce/client.py +45 -50
- castor_extractor/utils/salesforce/client_test.py +2 -2
- castor_extractor/utils/salesforce/pagination.py +33 -0
- castor_extractor/visualization/metabase/client/api/client.py +30 -11
- castor_extractor/visualization/salesforce_reporting/client/rest.py +4 -3
- castor_extractor/visualization/sigma/client/client.py +2 -1
- castor_extractor/visualization/tableau_revamp/assets.py +8 -0
- castor_extractor/visualization/tableau_revamp/client/client.py +6 -1
- castor_extractor/warehouse/databricks/api_client.py +239 -0
- castor_extractor/warehouse/databricks/api_client_test.py +15 -0
- castor_extractor/warehouse/databricks/client.py +37 -489
- castor_extractor/warehouse/databricks/client_test.py +1 -99
- castor_extractor/warehouse/databricks/endpoints.py +28 -0
- castor_extractor/warehouse/databricks/lineage.py +141 -0
- castor_extractor/warehouse/databricks/lineage_test.py +34 -0
- castor_extractor/warehouse/databricks/pagination.py +22 -0
- castor_extractor/warehouse/databricks/sql_client.py +90 -0
- castor_extractor/warehouse/databricks/utils.py +44 -1
- castor_extractor/warehouse/databricks/utils_test.py +58 -1
- castor_extractor/warehouse/mysql/client.py +0 -3
- castor_extractor/warehouse/salesforce/client.py +12 -59
- castor_extractor/warehouse/salesforce/pagination.py +34 -0
- castor_extractor/warehouse/sqlserver/client.py +0 -2
- {castor_extractor-0.19.4.dist-info → castor_extractor-0.19.7.dist-info}/METADATA +14 -1
- {castor_extractor-0.19.4.dist-info → castor_extractor-0.19.7.dist-info}/RECORD +36 -31
- castor_extractor/utils/client/api_deprecated.py +0 -89
- castor_extractor/utils/client/api_deprecated_test.py +0 -18
- castor_extractor/utils/pager/pager_on_token.py +0 -52
- castor_extractor/utils/pager/pager_on_token_test.py +0 -73
- {castor_extractor-0.19.4.dist-info → castor_extractor-0.19.7.dist-info}/LICENCE +0 -0
- {castor_extractor-0.19.4.dist-info → castor_extractor-0.19.7.dist-info}/WHEEL +0 -0
- {castor_extractor-0.19.4.dist-info → castor_extractor-0.19.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Iterator, List, Optional, Set, Tuple
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ...utils import (
|
|
8
|
+
APIClient,
|
|
9
|
+
BearerAuth,
|
|
10
|
+
SafeMode,
|
|
11
|
+
build_url,
|
|
12
|
+
fetch_all_pages,
|
|
13
|
+
handle_response,
|
|
14
|
+
retry,
|
|
15
|
+
safe_mode,
|
|
16
|
+
)
|
|
17
|
+
from ..abstract import TimeFilter
|
|
18
|
+
from .credentials import DatabricksCredentials
|
|
19
|
+
from .endpoints import DatabricksEndpointFactory
|
|
20
|
+
from .format import DatabricksFormatter, TagMapping
|
|
21
|
+
from .lineage import single_column_lineage_links, single_table_lineage_links
|
|
22
|
+
from .pagination import DATABRICKS_PAGE_SIZE, DatabricksPagination
|
|
23
|
+
from .types import TablesColumns, TimestampedLink
|
|
24
|
+
from .utils import hourly_time_filters
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_DATABRICKS_CLIENT_TIMEOUT_S = 90
|
|
29
|
+
_MAX_NUMBER_OF_LINEAGE_ERRORS = 1000
|
|
30
|
+
_MAX_NUMBER_OF_QUERY_ERRORS = 1000
|
|
31
|
+
_RETRY_ATTEMPTS = 3
|
|
32
|
+
_RETRY_BASE_MS = 1000
|
|
33
|
+
_RETRY_EXCEPTIONS = [
|
|
34
|
+
requests.exceptions.ConnectTimeout,
|
|
35
|
+
]
|
|
36
|
+
_WORKSPACE_ID_HEADER = "X-Databricks-Org-Id"
|
|
37
|
+
|
|
38
|
+
safe_lineage_params = SafeMode((BaseException,), _MAX_NUMBER_OF_LINEAGE_ERRORS)
|
|
39
|
+
safe_query_params = SafeMode((BaseException,), _MAX_NUMBER_OF_QUERY_ERRORS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DatabricksAuth(BearerAuth):
|
|
43
|
+
def __init__(self, credentials: DatabricksCredentials):
|
|
44
|
+
self.token = credentials.token
|
|
45
|
+
|
|
46
|
+
def fetch_token(self) -> Optional[str]:
|
|
47
|
+
return self.token
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DatabricksAPIClient(APIClient):
|
|
51
|
+
"""Databricks Client"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
credentials: DatabricksCredentials,
|
|
56
|
+
db_allowed: Optional[Set[str]] = None,
|
|
57
|
+
db_blocked: Optional[Set[str]] = None,
|
|
58
|
+
):
|
|
59
|
+
auth = DatabricksAuth(credentials)
|
|
60
|
+
super().__init__(
|
|
61
|
+
host=credentials.host,
|
|
62
|
+
auth=auth,
|
|
63
|
+
timeout=_DATABRICKS_CLIENT_TIMEOUT_S,
|
|
64
|
+
)
|
|
65
|
+
self._http_path = credentials.http_path
|
|
66
|
+
self._db_allowed = db_allowed
|
|
67
|
+
self._db_blocked = db_blocked
|
|
68
|
+
|
|
69
|
+
self.formatter = DatabricksFormatter()
|
|
70
|
+
|
|
71
|
+
def _keep_catalog(self, catalog: str) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Helper function to determine if we should keep the Databricks catalog
|
|
74
|
+
which is a CastorDoc database
|
|
75
|
+
"""
|
|
76
|
+
if self._db_allowed and catalog not in self._db_allowed:
|
|
77
|
+
return False
|
|
78
|
+
if self._db_blocked and catalog in self._db_blocked:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def databases(self) -> List[dict]:
|
|
83
|
+
content = self._get(DatabricksEndpointFactory.databases())
|
|
84
|
+
_databases = self.formatter.format_database(content.get("catalogs", []))
|
|
85
|
+
return [d for d in _databases if self._keep_catalog(d["database_name"])]
|
|
86
|
+
|
|
87
|
+
def _schemas_of_database(self, database: dict) -> List[dict]:
|
|
88
|
+
payload = {"catalog_name": database["database_name"]}
|
|
89
|
+
content = self._get(DatabricksEndpointFactory.schemas(), params=payload)
|
|
90
|
+
schemas = content.get("schemas", [])
|
|
91
|
+
return self.formatter.format_schema(schemas, database)
|
|
92
|
+
|
|
93
|
+
def schemas(self, databases: List[dict]) -> List[dict]:
|
|
94
|
+
"""
|
|
95
|
+
Get the databricks schemas (also sometimes called databases)
|
|
96
|
+
(which correspond to the schemas in Castor)
|
|
97
|
+
leveraging the unity catalog API
|
|
98
|
+
"""
|
|
99
|
+
return [
|
|
100
|
+
schema
|
|
101
|
+
for database in databases
|
|
102
|
+
for schema in self._schemas_of_database(database)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def tables_columns_of_schema(
|
|
106
|
+
self,
|
|
107
|
+
schema: dict,
|
|
108
|
+
table_tags: TagMapping,
|
|
109
|
+
column_tags: TagMapping,
|
|
110
|
+
) -> TablesColumns:
|
|
111
|
+
payload = {
|
|
112
|
+
"catalog_name": schema["database_id"],
|
|
113
|
+
"schema_name": schema["schema_name"],
|
|
114
|
+
}
|
|
115
|
+
response = self._call(
|
|
116
|
+
method="GET",
|
|
117
|
+
endpoint=DatabricksEndpointFactory.tables(),
|
|
118
|
+
params=payload,
|
|
119
|
+
)
|
|
120
|
+
workspace_id = response.headers[_WORKSPACE_ID_HEADER]
|
|
121
|
+
content = handle_response(response)
|
|
122
|
+
host = build_url(self._host, endpoint="")
|
|
123
|
+
return self.formatter.format_table_column(
|
|
124
|
+
raw_tables=content.get("tables", []),
|
|
125
|
+
schema=schema,
|
|
126
|
+
host=host,
|
|
127
|
+
workspace_id=workspace_id,
|
|
128
|
+
table_tags=table_tags,
|
|
129
|
+
column_tags=column_tags,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@safe_mode(safe_lineage_params, lambda: [])
|
|
133
|
+
@retry(
|
|
134
|
+
exceptions=_RETRY_EXCEPTIONS,
|
|
135
|
+
max_retries=_RETRY_ATTEMPTS,
|
|
136
|
+
base_ms=_RETRY_BASE_MS,
|
|
137
|
+
)
|
|
138
|
+
def get_single_column_lineage(
|
|
139
|
+
self,
|
|
140
|
+
names: Tuple[str, str],
|
|
141
|
+
) -> List[TimestampedLink]:
|
|
142
|
+
"""
|
|
143
|
+
Helper function used in get_lineage_links.
|
|
144
|
+
Call data lineage API and return the content of the result
|
|
145
|
+
|
|
146
|
+
eg table_path: broward_prd.bronze.account_adjustments
|
|
147
|
+
FYI: Maximum rate of 10 requests per SECOND
|
|
148
|
+
"""
|
|
149
|
+
table_path, column_name = names
|
|
150
|
+
payload = {
|
|
151
|
+
"table_name": table_path,
|
|
152
|
+
"column_name": column_name,
|
|
153
|
+
"include_entity_lineage": True,
|
|
154
|
+
}
|
|
155
|
+
content = self._get(
|
|
156
|
+
DatabricksEndpointFactory.column_lineage(), params=payload
|
|
157
|
+
)
|
|
158
|
+
column_path = f"{table_path}.{column_name}"
|
|
159
|
+
return single_column_lineage_links(column_path, content)
|
|
160
|
+
|
|
161
|
+
@safe_mode(safe_lineage_params, lambda: [])
|
|
162
|
+
@retry(
|
|
163
|
+
exceptions=_RETRY_EXCEPTIONS,
|
|
164
|
+
max_retries=_RETRY_ATTEMPTS,
|
|
165
|
+
base_ms=_RETRY_BASE_MS,
|
|
166
|
+
)
|
|
167
|
+
def get_single_table_lineage(
|
|
168
|
+
self, table_path: str
|
|
169
|
+
) -> List[TimestampedLink]:
|
|
170
|
+
"""
|
|
171
|
+
Helper function used in get_lineage_links.
|
|
172
|
+
Call data lineage API and return the content of the result
|
|
173
|
+
eg table_path: broward_prd.bronze.account_adjustments
|
|
174
|
+
FYI: Maximum rate of 50 requests per SECOND
|
|
175
|
+
"""
|
|
176
|
+
payload = {"table_name": table_path, "include_entity_lineage": True}
|
|
177
|
+
content = self._get(
|
|
178
|
+
DatabricksEndpointFactory.table_lineage(), params=payload
|
|
179
|
+
)
|
|
180
|
+
return single_table_lineage_links(table_path, content)
|
|
181
|
+
|
|
182
|
+
@safe_mode(safe_query_params, lambda: [])
|
|
183
|
+
@retry(
|
|
184
|
+
exceptions=_RETRY_EXCEPTIONS,
|
|
185
|
+
max_retries=_RETRY_ATTEMPTS,
|
|
186
|
+
base_ms=_RETRY_BASE_MS,
|
|
187
|
+
)
|
|
188
|
+
def _queries(
|
|
189
|
+
self,
|
|
190
|
+
filter_: dict,
|
|
191
|
+
) -> Iterator[dict]:
|
|
192
|
+
"""
|
|
193
|
+
Callback to scroll the queries api
|
|
194
|
+
https://docs.databricks.com/api/workspace/queryhistory/list
|
|
195
|
+
max_results: Limit the number of results returned in one page.
|
|
196
|
+
The default is 100. (both on our side and Databricks')
|
|
197
|
+
"""
|
|
198
|
+
payload = {**filter_, "max_results": DATABRICKS_PAGE_SIZE}
|
|
199
|
+
request = partial(
|
|
200
|
+
self._get,
|
|
201
|
+
endpoint=DatabricksEndpointFactory.queries(),
|
|
202
|
+
data=payload,
|
|
203
|
+
)
|
|
204
|
+
queries = fetch_all_pages(request, DatabricksPagination)
|
|
205
|
+
return queries
|
|
206
|
+
|
|
207
|
+
def queries(self, time_filter: Optional[TimeFilter] = None) -> List[dict]:
|
|
208
|
+
"""get all queries, hour per hour"""
|
|
209
|
+
time_range_filters = hourly_time_filters(time_filter)
|
|
210
|
+
raw_queries = []
|
|
211
|
+
for _filter in time_range_filters:
|
|
212
|
+
logger.info(f"Fetching queries for time filter {_filter}")
|
|
213
|
+
hourly = self._queries(_filter)
|
|
214
|
+
raw_queries.extend(hourly)
|
|
215
|
+
return self.formatter.format_query(raw_queries)
|
|
216
|
+
|
|
217
|
+
def users(self) -> List[dict]:
|
|
218
|
+
"""
|
|
219
|
+
retrieve user from api
|
|
220
|
+
"""
|
|
221
|
+
content = self._get(DatabricksEndpointFactory.users())
|
|
222
|
+
return self.formatter.format_user(content.get("Resources", []))
|
|
223
|
+
|
|
224
|
+
def _view_ddl_per_schema(self, schema: dict) -> List[dict]:
|
|
225
|
+
payload = {
|
|
226
|
+
"catalog_name": schema["database_id"],
|
|
227
|
+
"schema_name": schema["schema_name"],
|
|
228
|
+
"omit_columns": True,
|
|
229
|
+
}
|
|
230
|
+
content = self._get(DatabricksEndpointFactory.tables(), params=payload)
|
|
231
|
+
return self.formatter.format_view_ddl(content.get("tables", []), schema)
|
|
232
|
+
|
|
233
|
+
def view_ddl(self, schemas: List[dict]) -> List[dict]:
|
|
234
|
+
"""retrieve view ddl"""
|
|
235
|
+
view_ddl: List[dict] = []
|
|
236
|
+
for schema in schemas:
|
|
237
|
+
v_to_add = self._view_ddl_per_schema(schema)
|
|
238
|
+
view_ddl.extend(v_to_add)
|
|
239
|
+
return view_ddl
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .api_client import DatabricksAPIClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MockDatabricksClient(DatabricksAPIClient):
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self._db_allowed = ["prd", "staging"]
|
|
7
|
+
self._db_blocked = ["dev"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_DatabricksAPIClient__keep_catalog():
|
|
11
|
+
client = MockDatabricksClient()
|
|
12
|
+
assert client._keep_catalog("prd")
|
|
13
|
+
assert client._keep_catalog("staging")
|
|
14
|
+
assert not client._keep_catalog("dev")
|
|
15
|
+
assert not client._keep_catalog("something_unknown")
|