castor-extractor 0.16.1__py3-none-any.whl → 0.16.3__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 +8 -0
- castor_extractor/visualization/domo/client/client.py +1 -1
- castor_extractor/visualization/powerbi/client/constants.py +3 -3
- castor_extractor/visualization/powerbi/client/rest.py +14 -8
- castor_extractor/visualization/powerbi/client/rest_test.py +61 -27
- castor_extractor/visualization/tableau/assets.py +5 -0
- castor_extractor/visualization/tableau/client/client.py +10 -0
- castor_extractor/visualization/tableau/gql_fields.py +30 -9
- castor_extractor/warehouse/databricks/client.py +19 -2
- castor_extractor/warehouse/databricks/client_test.py +14 -0
- castor_extractor/warehouse/databricks/extract.py +2 -1
- castor_extractor/warehouse/databricks/format.py +5 -4
- castor_extractor-0.16.3.dist-info/LICENCE +86 -0
- {castor_extractor-0.16.1.dist-info → castor_extractor-0.16.3.dist-info}/METADATA +2 -3
- {castor_extractor-0.16.1.dist-info → castor_extractor-0.16.3.dist-info}/RECORD +17 -16
- {castor_extractor-0.16.1.dist-info → castor_extractor-0.16.3.dist-info}/WHEEL +1 -1
- {castor_extractor-0.16.1.dist-info → castor_extractor-0.16.3.dist-info}/entry_points.txt +0 -0
CHANGELOG.md
CHANGED
|
@@ -14,7 +14,7 @@ RawData = Iterator[dict]
|
|
|
14
14
|
|
|
15
15
|
DOMO_PUBLIC_URL = "https://api.domo.com"
|
|
16
16
|
FORMAT = "%Y-%m-%d %I:%M:%S %p"
|
|
17
|
-
DEFAULT_TIMEOUT =
|
|
17
|
+
DEFAULT_TIMEOUT = 500
|
|
18
18
|
TOKEN_EXPIRATION_SECONDS = timedelta(seconds=3000) # auth token lasts 1 hour
|
|
19
19
|
|
|
20
20
|
IGNORED_ERROR_CODES = (
|
|
@@ -14,18 +14,18 @@ POST = "POST"
|
|
|
14
14
|
class Urls:
|
|
15
15
|
"""PowerBi's urls"""
|
|
16
16
|
|
|
17
|
-
REST_API_BASE_PATH = "https://api.powerbi.com/v1.0/myorg"
|
|
18
17
|
CLIENT_APP_BASE = "https://login.microsoftonline.com/"
|
|
19
18
|
DEFAULT_SCOPE = "https://analysis.windows.net/powerbi/api/.default"
|
|
19
|
+
REST_API_BASE_PATH = "https://api.powerbi.com/v1.0/myorg"
|
|
20
20
|
|
|
21
21
|
# PBI rest API Routes
|
|
22
22
|
ACTIVITY_EVENTS = f"{REST_API_BASE_PATH}/admin/activityevents"
|
|
23
|
-
DATASETS = f"{REST_API_BASE_PATH}/admin/datasets"
|
|
24
23
|
DASHBOARD = f"{REST_API_BASE_PATH}/admin/dashboards"
|
|
24
|
+
DATASETS = f"{REST_API_BASE_PATH}/admin/datasets"
|
|
25
25
|
GROUPS = f"{REST_API_BASE_PATH}/admin/groups"
|
|
26
|
+
METADATA_GET = f"{REST_API_BASE_PATH}/admin/workspaces/scanResult"
|
|
26
27
|
METADATA_POST = f"{REST_API_BASE_PATH}/admin/workspaces/getInfo"
|
|
27
28
|
METADATA_WAIT = f"{REST_API_BASE_PATH}/admin/workspaces/scanStatus"
|
|
28
|
-
METADATA_GET = f"{REST_API_BASE_PATH}/admin/workspaces/scanResult"
|
|
29
29
|
REPORTS = f"{REST_API_BASE_PATH}/admin/reports"
|
|
30
30
|
WORKSPACE_IDS = (
|
|
31
31
|
"https://api.powerbi.com/v1.0/myorg/admin/workspaces/modified"
|
|
@@ -64,19 +64,15 @@ class Client:
|
|
|
64
64
|
|
|
65
65
|
def __init__(self, credentials: Credentials):
|
|
66
66
|
self.creds = credentials
|
|
67
|
-
|
|
68
|
-
def _access_token(self) -> dict:
|
|
69
67
|
client_app = f"{Urls.CLIENT_APP_BASE}{self.creds.tenant_id}"
|
|
70
|
-
app = msal.ConfidentialClientApplication(
|
|
68
|
+
self.app = msal.ConfidentialClientApplication(
|
|
71
69
|
client_id=self.creds.client_id,
|
|
72
70
|
authority=client_app,
|
|
73
71
|
client_credential=self.creds.secret,
|
|
74
72
|
)
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if not token:
|
|
79
|
-
token = app.acquire_token_for_client(scopes=self.creds.scopes)
|
|
74
|
+
def _access_token(self) -> dict:
|
|
75
|
+
token = self.app.acquire_token_for_client(scopes=self.creds.scopes)
|
|
80
76
|
|
|
81
77
|
if Keys.ACCESS_TOKEN not in token:
|
|
82
78
|
raise ValueError(f"No access token in token response: {token}")
|
|
@@ -248,7 +244,17 @@ class Client:
|
|
|
248
244
|
Returns a list of reports for the organization.
|
|
249
245
|
https://learn.microsoft.com/en-us/rest/api/power-bi/admin/reports-get-reports-as-admin
|
|
250
246
|
"""
|
|
251
|
-
|
|
247
|
+
reports = self._get(Urls.REPORTS)[Keys.VALUE]
|
|
248
|
+
for report in reports:
|
|
249
|
+
report_id = report.get("id")
|
|
250
|
+
try:
|
|
251
|
+
url = Urls.REPORTS + f"/{report_id}/pages"
|
|
252
|
+
pages = self._get(url)[Keys.VALUE]
|
|
253
|
+
report["pages"] = pages
|
|
254
|
+
except (requests.HTTPError, requests.exceptions.Timeout) as e:
|
|
255
|
+
logger.debug(e)
|
|
256
|
+
continue
|
|
257
|
+
return reports
|
|
252
258
|
|
|
253
259
|
def _dashboards(self) -> List[Dict]:
|
|
254
260
|
"""
|
|
@@ -31,7 +31,6 @@ def test__access_token(mock_app):
|
|
|
31
31
|
# init mocks
|
|
32
32
|
valid_response = {"access_token": "mock_token"}
|
|
33
33
|
returning_valid_token = Mock(return_value=valid_response)
|
|
34
|
-
mock_app.return_value.acquire_token_silent = Mock(return_value=None)
|
|
35
34
|
mock_app.return_value.acquire_token_for_client = returning_valid_token
|
|
36
35
|
|
|
37
36
|
# init client
|
|
@@ -40,30 +39,29 @@ def test__access_token(mock_app):
|
|
|
40
39
|
# generated token
|
|
41
40
|
assert client._access_token() == valid_response
|
|
42
41
|
|
|
43
|
-
# via silent endpoint token
|
|
44
|
-
mock_app.return_value.acquire_token_silent = returning_valid_token
|
|
45
|
-
mock_app.return_value.acquire_token_for_client = None
|
|
46
|
-
assert client._access_token() == valid_response
|
|
47
|
-
|
|
48
42
|
# token missing in response
|
|
49
43
|
invalid_response = {"not_access_token": "666"}
|
|
50
44
|
returning_invalid_token = Mock(return_value=invalid_response)
|
|
51
|
-
mock_app.return_value.
|
|
45
|
+
mock_app.return_value.acquire_token_for_client = returning_invalid_token
|
|
52
46
|
|
|
53
47
|
with pytest.raises(ValueError):
|
|
54
48
|
client._access_token()
|
|
55
49
|
|
|
56
50
|
|
|
51
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
57
52
|
@patch.object(Client, "_access_token")
|
|
58
|
-
def test__headers(mock_access_token):
|
|
53
|
+
def test__headers(mock_access_token, mock_app):
|
|
54
|
+
mock_app.return_value = None
|
|
59
55
|
client = _client()
|
|
60
56
|
mock_access_token.return_value = {Keys.ACCESS_TOKEN: "666"}
|
|
61
57
|
assert client._header() == {"Authorization": "Bearer 666"}
|
|
62
58
|
|
|
63
59
|
|
|
60
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
64
61
|
@patch("requests.request")
|
|
65
62
|
@patch.object(Client, "_access_token")
|
|
66
|
-
def test__get(mocked_access_token, mocked_request):
|
|
63
|
+
def test__get(mocked_access_token, mocked_request, mock_app):
|
|
64
|
+
mock_app.return_value = None
|
|
67
65
|
client = _client()
|
|
68
66
|
mocked_access_token.return_value = {Keys.ACCESS_TOKEN: "666"}
|
|
69
67
|
fact = {"fact": "Approximately 24 cat skins can make a coat.", "length": 43}
|
|
@@ -81,9 +79,11 @@ def test__get(mocked_access_token, mocked_request):
|
|
|
81
79
|
result = client._get("https/whatev.er")
|
|
82
80
|
|
|
83
81
|
|
|
82
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
84
83
|
@patch("requests.request")
|
|
85
84
|
@patch.object(Client, "_access_token")
|
|
86
|
-
def test__workspace_ids(_, mocked_request):
|
|
85
|
+
def test__workspace_ids(_, mocked_request, mock_app):
|
|
86
|
+
mock_app.return_value = None
|
|
87
87
|
client = _client()
|
|
88
88
|
mocked_request.return_value = Mock(
|
|
89
89
|
json=lambda: [{"id": 1000}, {"id": 1001}, {"id": 1003}],
|
|
@@ -112,9 +112,11 @@ def test__workspace_ids(_, mocked_request):
|
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
115
116
|
@patch("requests.request")
|
|
116
117
|
@patch.object(Client, "_access_token")
|
|
117
|
-
def test__post_default(_, mocked_request):
|
|
118
|
+
def test__post_default(_, mocked_request, mock_app):
|
|
119
|
+
mock_app.return_value = None
|
|
118
120
|
client = _client()
|
|
119
121
|
url = "https://estcequecestbientotleweekend.fr/"
|
|
120
122
|
params = QueryParams.METADATA_SCAN
|
|
@@ -129,9 +131,11 @@ def test__post_default(_, mocked_request):
|
|
|
129
131
|
)
|
|
130
132
|
|
|
131
133
|
|
|
134
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
132
135
|
@patch("requests.request")
|
|
133
136
|
@patch.object(Client, "_access_token")
|
|
134
|
-
def test__post_with_processor(_, mocked_request):
|
|
137
|
+
def test__post_with_processor(_, mocked_request, mock_app):
|
|
138
|
+
mock_app.return_value = None
|
|
135
139
|
client = _client()
|
|
136
140
|
url = "https://estcequecestbientotleweekend.fr/"
|
|
137
141
|
params = QueryParams.METADATA_SCAN
|
|
@@ -146,9 +150,11 @@ def test__post_with_processor(_, mocked_request):
|
|
|
146
150
|
assert result == 1000
|
|
147
151
|
|
|
148
152
|
|
|
153
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
149
154
|
@patch("requests.request")
|
|
150
155
|
@patch.object(Client, "_access_token")
|
|
151
|
-
def test__datasets(_, mocked_request):
|
|
156
|
+
def test__datasets(_, mocked_request, mock_app):
|
|
157
|
+
mock_app.return_value = None
|
|
152
158
|
client = _client()
|
|
153
159
|
mocked_request.return_value = Mock(
|
|
154
160
|
json=lambda: {"value": [{"id": 1, "type": "dataset"}]},
|
|
@@ -164,27 +170,50 @@ def test__datasets(_, mocked_request):
|
|
|
164
170
|
assert datasets == [{"id": 1, "type": "dataset"}]
|
|
165
171
|
|
|
166
172
|
|
|
173
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
167
174
|
@patch("requests.request")
|
|
168
175
|
@patch.object(Client, "_access_token")
|
|
169
|
-
def test__reports(_, mocked_request):
|
|
176
|
+
def test__reports(_, mocked_request, mock_app):
|
|
177
|
+
mock_app.return_value = None
|
|
170
178
|
client = _client()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
page_url = f"{Urls.REPORTS}/1/pages"
|
|
180
|
+
calls = [
|
|
181
|
+
call(GET, Urls.REPORTS, data=None, headers=ANY, params=None),
|
|
182
|
+
call(
|
|
183
|
+
GET,
|
|
184
|
+
page_url,
|
|
185
|
+
data=None,
|
|
186
|
+
headers=ANY,
|
|
187
|
+
params=None,
|
|
188
|
+
),
|
|
189
|
+
]
|
|
190
|
+
mocked_request.side_effect = [
|
|
191
|
+
Mock(json=lambda: {"value": [{"id": 1, "type": "report"}]}),
|
|
192
|
+
Mock(
|
|
193
|
+
json=lambda: {
|
|
194
|
+
"value": [
|
|
195
|
+
{"name": "page_name", "displayName": "page", "order": 0}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
]
|
|
174
200
|
reports = client._reports()
|
|
175
|
-
mocked_request.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
mocked_request.assert_has_calls(calls)
|
|
202
|
+
|
|
203
|
+
assert reports == [
|
|
204
|
+
{
|
|
205
|
+
"id": 1,
|
|
206
|
+
"type": "report",
|
|
207
|
+
"pages": [{"name": "page_name", "displayName": "page", "order": 0}],
|
|
208
|
+
}
|
|
209
|
+
]
|
|
183
210
|
|
|
184
211
|
|
|
212
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
185
213
|
@patch("requests.request")
|
|
186
214
|
@patch.object(Client, "_access_token")
|
|
187
|
-
def test__dashboards(_, mocked_request):
|
|
215
|
+
def test__dashboards(_, mocked_request, mock_app):
|
|
216
|
+
mock_app.return_value = None
|
|
188
217
|
client = _client()
|
|
189
218
|
mocked_request.return_value = Mock(
|
|
190
219
|
json=lambda: {"value": [{"id": 1, "type": "dashboard"}]},
|
|
@@ -200,6 +229,7 @@ def test__dashboards(_, mocked_request):
|
|
|
200
229
|
assert dashboards == [{"id": 1, "type": "dashboard"}]
|
|
201
230
|
|
|
202
231
|
|
|
232
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
203
233
|
@patch.object(Client, "_workspace_ids")
|
|
204
234
|
@patch.object(Client, "_create_scan")
|
|
205
235
|
@patch.object(Client, "_wait_for_scan_result")
|
|
@@ -209,7 +239,9 @@ def test__metadata(
|
|
|
209
239
|
mocked_wait,
|
|
210
240
|
mocked_create_scan,
|
|
211
241
|
mocked_workspace_ids,
|
|
242
|
+
mock_app,
|
|
212
243
|
):
|
|
244
|
+
mock_app.return_value = None
|
|
213
245
|
mocked_workspace_ids.return_value = list(range(200))
|
|
214
246
|
mocked_create_scan.return_value = 314
|
|
215
247
|
mocked_wait.return_value = True
|
|
@@ -240,8 +272,10 @@ _CALLS = [
|
|
|
240
272
|
]
|
|
241
273
|
|
|
242
274
|
|
|
275
|
+
@patch.object(msal, "ConfidentialClientApplication")
|
|
243
276
|
@patch.object(Client, "_call")
|
|
244
|
-
def test__activity_events(mocked):
|
|
277
|
+
def test__activity_events(mocked, mock_app):
|
|
278
|
+
mock_app.return_value = None
|
|
245
279
|
client = _client()
|
|
246
280
|
mocked.side_effect = _CALLS
|
|
247
281
|
|
|
@@ -12,10 +12,12 @@ class TableauAsset(ExternalAsset):
|
|
|
12
12
|
CUSTOM_SQL_TABLE = "custom_sql_tables"
|
|
13
13
|
CUSTOM_SQL_QUERY = "custom_sql_queries"
|
|
14
14
|
DASHBOARD = "dashboards"
|
|
15
|
+
DASHBOARD_SHEET = "dashboards_sheets"
|
|
15
16
|
DATASOURCE = "datasources"
|
|
16
17
|
FIELD = "fields"
|
|
17
18
|
PROJECT = "projects"
|
|
18
19
|
PUBLISHED_DATASOURCE = "published_datasources"
|
|
20
|
+
SHEET = "sheets"
|
|
19
21
|
USAGE = "views"
|
|
20
22
|
USER = "users"
|
|
21
23
|
WORKBOOK = "workbooks"
|
|
@@ -25,7 +27,9 @@ class TableauAsset(ExternalAsset):
|
|
|
25
27
|
def optional(cls) -> Set["TableauAsset"]:
|
|
26
28
|
return {
|
|
27
29
|
TableauAsset.DASHBOARD,
|
|
30
|
+
TableauAsset.DASHBOARD_SHEET,
|
|
28
31
|
TableauAsset.FIELD,
|
|
32
|
+
TableauAsset.SHEET,
|
|
29
33
|
TableauAsset.PUBLISHED_DATASOURCE,
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -42,4 +46,5 @@ class TableauGraphqlAsset(Enum):
|
|
|
42
46
|
DASHBOARD = "dashboards"
|
|
43
47
|
DATASOURCE = "datasources"
|
|
44
48
|
GROUP_FIELD = "groupFields"
|
|
49
|
+
SHEETS = "sheets"
|
|
45
50
|
WORKBOOK_TO_DATASOURCE = "workbooks"
|
|
@@ -173,6 +173,13 @@ class ApiClient:
|
|
|
173
173
|
TableauAsset.DASHBOARD,
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
+
def _fetch_sheets(self) -> SerializedAsset:
|
|
177
|
+
"""Fetches sheets"""
|
|
178
|
+
|
|
179
|
+
return self._fetch_paginated_objects(
|
|
180
|
+
TableauAsset.SHEET,
|
|
181
|
+
)
|
|
182
|
+
|
|
176
183
|
def _fetch_paginated_objects(self, asset: TableauAsset) -> SerializedAsset:
|
|
177
184
|
"""Fetches paginated objects"""
|
|
178
185
|
|
|
@@ -203,6 +210,9 @@ class ApiClient:
|
|
|
203
210
|
if asset == TableauAsset.PUBLISHED_DATASOURCE:
|
|
204
211
|
assets = self._fetch_published_datasources()
|
|
205
212
|
|
|
213
|
+
if asset == TableauAsset.SHEET:
|
|
214
|
+
assets = self._fetch_sheets()
|
|
215
|
+
|
|
206
216
|
if asset == TableauAsset.USAGE:
|
|
207
217
|
assets = self._fetch_usages(self._safe_mode)
|
|
208
218
|
|
|
@@ -111,15 +111,15 @@ class GQLQueryFields(Enum):
|
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
113
|
DASHBOARDS: str = """
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
id
|
|
115
|
+
name
|
|
116
|
+
path
|
|
117
|
+
tags {
|
|
118
|
+
name
|
|
119
|
+
}
|
|
120
|
+
workbook {
|
|
121
|
+
luid # to retrieve the parent
|
|
122
|
+
}
|
|
123
123
|
"""
|
|
124
124
|
|
|
125
125
|
DATASOURCE: str = """
|
|
@@ -160,6 +160,21 @@ class GQLQueryFields(Enum):
|
|
|
160
160
|
role
|
|
161
161
|
"""
|
|
162
162
|
|
|
163
|
+
SHEET: str = """
|
|
164
|
+
containedInDashboards {
|
|
165
|
+
id
|
|
166
|
+
}
|
|
167
|
+
id
|
|
168
|
+
index
|
|
169
|
+
name
|
|
170
|
+
upstreamFields{
|
|
171
|
+
name
|
|
172
|
+
}
|
|
173
|
+
workbook {
|
|
174
|
+
luid
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
|
|
163
178
|
WORKBOOK_TO_DATASOURCE: str = """
|
|
164
179
|
luid
|
|
165
180
|
id
|
|
@@ -219,6 +234,12 @@ QUERY_FIELDS: Dict[TableauAsset, QueryInfo] = {
|
|
|
219
234
|
OBJECT_TYPE: TableauGraphqlAsset.GROUP_FIELD,
|
|
220
235
|
},
|
|
221
236
|
],
|
|
237
|
+
TableauAsset.SHEET: [
|
|
238
|
+
{
|
|
239
|
+
FIELDS: GQLQueryFields.SHEET,
|
|
240
|
+
OBJECT_TYPE: TableauGraphqlAsset.SHEETS,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
222
243
|
TableauAsset.WORKBOOK_TO_DATASOURCE: [
|
|
223
244
|
{
|
|
224
245
|
FIELDS: GQLQueryFields.WORKBOOK_TO_DATASOURCE,
|
|
@@ -87,15 +87,32 @@ class DatabricksClient(APIClient):
|
|
|
87
87
|
content.get("tables", []), schema
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _match_table_with_user(table: dict, user_id_by_email: dict) -> dict:
|
|
92
|
+
table_owner_email = table.get("owner_email")
|
|
93
|
+
if not table_owner_email:
|
|
94
|
+
return table
|
|
95
|
+
owner_external_id = user_id_by_email.get(table_owner_email)
|
|
96
|
+
if not owner_external_id:
|
|
97
|
+
return table
|
|
98
|
+
return {**table, "owner_external_id": owner_external_id}
|
|
99
|
+
|
|
100
|
+
def tables_and_columns(
|
|
101
|
+
self, schemas: List[dict], users: List[dict]
|
|
102
|
+
) -> TablesColumns:
|
|
91
103
|
"""
|
|
92
104
|
Get the databricks tables & columns leveraging the unity catalog API
|
|
93
105
|
"""
|
|
94
106
|
tables: List[dict] = []
|
|
95
107
|
columns: List[dict] = []
|
|
108
|
+
user_id_by_email = {user.get("email"): user.get("id") for user in users}
|
|
96
109
|
for schema in schemas:
|
|
97
110
|
t_to_add, c_to_add = self._tables_columns_of_schema(schema)
|
|
98
|
-
|
|
111
|
+
t_with_owner = [
|
|
112
|
+
self._match_table_with_user(table, user_id_by_email)
|
|
113
|
+
for table in t_to_add
|
|
114
|
+
]
|
|
115
|
+
tables.extend(t_with_owner)
|
|
99
116
|
columns.extend(c_to_add)
|
|
100
117
|
return tables, columns
|
|
101
118
|
|
|
@@ -64,3 +64,17 @@ def test_DatabricksClient__keep_catalog():
|
|
|
64
64
|
assert client._keep_catalog("staging")
|
|
65
65
|
assert not client._keep_catalog("dev")
|
|
66
66
|
assert not client._keep_catalog("something_unknown")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_DatabricksClient__match_table_with_user():
|
|
70
|
+
client = MockDatabricksClient()
|
|
71
|
+
users_by_email = {"bob@castordoc.com": 3}
|
|
72
|
+
|
|
73
|
+
table = {"id": 1, "owner_email": "bob@castordoc.com"}
|
|
74
|
+
table_with_owner = client._match_table_with_user(table, users_by_email)
|
|
75
|
+
|
|
76
|
+
assert table_with_owner == {**table, "owner_external_id": 3}
|
|
77
|
+
|
|
78
|
+
table_without_owner = {"id": 1, "owner_email": None}
|
|
79
|
+
actual = client._match_table_with_user(table_without_owner, users_by_email)
|
|
80
|
+
assert actual == table_without_owner
|
|
@@ -82,7 +82,8 @@ class DatabricksExtractionProcessor:
|
|
|
82
82
|
|
|
83
83
|
del databases
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
users = self._client.users()
|
|
86
|
+
tables, columns = self._client.tables_and_columns(schemas, users)
|
|
86
87
|
|
|
87
88
|
location = self._storage.put(WarehouseAsset.TABLE.value, tables)
|
|
88
89
|
catalog_locations[WarehouseAsset.TABLE.value] = location
|
|
@@ -19,10 +19,11 @@ def _to_datetime_or_none(time_ms: Optional[int]) -> Optional[datetime]:
|
|
|
19
19
|
|
|
20
20
|
def _table_payload(schema: dict, table: dict) -> dict:
|
|
21
21
|
return {
|
|
22
|
+
"description": table.get("comment"),
|
|
22
23
|
"id": table["table_id"],
|
|
24
|
+
"owner_email": table.get("owner"),
|
|
23
25
|
"schema_id": f"{schema['id']}",
|
|
24
26
|
"table_name": table["name"],
|
|
25
|
-
"description": table.get("comment"),
|
|
26
27
|
"tags": [],
|
|
27
28
|
"type": table.get("table_type"),
|
|
28
29
|
}
|
|
@@ -30,12 +31,12 @@ def _table_payload(schema: dict, table: dict) -> dict:
|
|
|
30
31
|
|
|
31
32
|
def _column_payload(table: dict, column: dict) -> dict:
|
|
32
33
|
return {
|
|
33
|
-
"id": f"`{table['id']}`.`{column['name']}`",
|
|
34
34
|
"column_name": column["name"],
|
|
35
|
-
"table_id": table["id"],
|
|
36
|
-
"description": column.get("comment"),
|
|
37
35
|
"data_type": column["type_name"],
|
|
36
|
+
"description": column.get("comment"),
|
|
37
|
+
"id": f"`{table['id']}`.`{column['name']}`",
|
|
38
38
|
"ordinal_position": column["position"],
|
|
39
|
+
"table_id": table["id"],
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# End-User License Agreement (EULA) of "Castor Extractor"
|
|
2
|
+
|
|
3
|
+
This End-User License Agreement ("EULA") is a legal agreement between you and Castor. Our EULA was created by EULA Template (https://www.eulatemplate.com) for "Castor Extractor".
|
|
4
|
+
|
|
5
|
+
This EULA agreement governs your acquisition and use of our "Castor Extractor" software ("Software") directly from Castor or indirectly through a Castor authorized reseller or distributor (a "Reseller").
|
|
6
|
+
|
|
7
|
+
Please read this EULA agreement carefully before completing the installation process and using the "Castor Extractor" software. It provides a license to use the "Castor Extractor" software and contains warranty information and liability disclaimers.
|
|
8
|
+
|
|
9
|
+
If you register for a free trial of the "Castor Extractor" software, this EULA agreement will also govern that trial. By clicking "accept" or installing and/or using the "Castor Extractor" software, you are confirming your acceptance of the Software and agreeing to become bound by the terms of this EULA agreement.
|
|
10
|
+
|
|
11
|
+
If you are entering into this EULA agreement on behalf of a company or other legal entity, you represent that you have the authority to bind such entity and its affiliates to these terms and conditions. If you do not have such authority or if you do not agree with the terms and conditions of this EULA agreement, do not install or use the Software, and you must not accept this EULA agreement.
|
|
12
|
+
|
|
13
|
+
This EULA agreement shall apply only to the Software supplied by Castor herewith regardless of whether other software is referred to or described herein. The terms also apply to any Castor updates, supplements, Internet-based services, and support services for the Software, unless other terms accompany those items on delivery. If so, those terms apply.
|
|
14
|
+
|
|
15
|
+
## License Grant
|
|
16
|
+
|
|
17
|
+
Castor hereby grants you a personal, non-transferable, non-exclusive licence to use the "Castor Extractor" software on your devices in accordance with the terms of this EULA agreement.
|
|
18
|
+
|
|
19
|
+
You are permitted to load the "Castor Extractor" software (for example a PC, laptop, mobile or tablet) under your control. You are responsible for ensuring your device meets the minimum requirements of the "Castor Extractor" software.
|
|
20
|
+
|
|
21
|
+
You are not permitted to:
|
|
22
|
+
|
|
23
|
+
- Edit, alter, modify, adapt, translate or otherwise change the whole or any part of the Software nor permit the whole or any part of the Software to be combined with or become incorporated in any other software, nor decompile, disassemble or reverse engineer the Software or attempt to do any such things
|
|
24
|
+
- Reproduce, copy, distribute, resell or otherwise use the Software for any commercial purpose
|
|
25
|
+
- Allow any third party to use the Software on behalf of or for the benefit of any third party
|
|
26
|
+
- Use the Software in any way which breaches any applicable local, national or international law
|
|
27
|
+
- use the Software for any purpose that Castor considers is a breach of this EULA agreement
|
|
28
|
+
|
|
29
|
+
## Intellectual Property and Ownership
|
|
30
|
+
|
|
31
|
+
Castor shall at all times retain ownership of the Software as originally downloaded by you and all subsequent downloads of the Software by you. The Software (and the copyright, and other intellectual property rights of whatever nature in the Software, including any modifications made thereto) are and shall remain the property of Castor.
|
|
32
|
+
|
|
33
|
+
Castor reserves the right to grant licences to use the Software to third parties.
|
|
34
|
+
|
|
35
|
+
## Termination
|
|
36
|
+
|
|
37
|
+
This EULA agreement is effective from the date you first use the Software and shall continue until terminated. You may terminate it at any time upon written notice to Castor.
|
|
38
|
+
|
|
39
|
+
It will also terminate immediately if you fail to comply with any term of this EULA agreement. Upon such termination, the licenses granted by this EULA agreement will immediately terminate and you agree to stop all access and use of the Software. The provisions that by their nature continue and survive will survive any termination of this EULA agreement.
|
|
40
|
+
|
|
41
|
+
## Governing Law
|
|
42
|
+
|
|
43
|
+
This EULA agreement, and any dispute arising out of or in connection with this EULA agreement, shall be governed by and construed in accordance with the laws of France.
|
|
44
|
+
# End-User License Agreement (EULA) of "Castor Extractor"
|
|
45
|
+
|
|
46
|
+
This End-User License Agreement ("EULA") is a legal agreement between you and Castor. Our EULA was created by EULA Template (https://www.eulatemplate.com) for "Castor Extractor".
|
|
47
|
+
|
|
48
|
+
This EULA agreement governs your acquisition and use of our "Castor Extractor" software ("Software") directly from Castor or indirectly through a Castor authorized reseller or distributor (a "Reseller").
|
|
49
|
+
|
|
50
|
+
Please read this EULA agreement carefully before completing the installation process and using the "Castor Extractor" software. It provides a license to use the "Castor Extractor" software and contains warranty information and liability disclaimers.
|
|
51
|
+
|
|
52
|
+
If you register for a free trial of the "Castor Extractor" software, this EULA agreement will also govern that trial. By clicking "accept" or installing and/or using the "Castor Extractor" software, you are confirming your acceptance of the Software and agreeing to become bound by the terms of this EULA agreement.
|
|
53
|
+
|
|
54
|
+
If you are entering into this EULA agreement on behalf of a company or other legal entity, you represent that you have the authority to bind such entity and its affiliates to these terms and conditions. If you do not have such authority or if you do not agree with the terms and conditions of this EULA agreement, do not install or use the Software, and you must not accept this EULA agreement.
|
|
55
|
+
|
|
56
|
+
This EULA agreement shall apply only to the Software supplied by Castor herewith regardless of whether other software is referred to or described herein. The terms also apply to any Castor updates, supplements, Internet-based services, and support services for the Software, unless other terms accompany those items on delivery. If so, those terms apply.
|
|
57
|
+
|
|
58
|
+
## License Grant
|
|
59
|
+
|
|
60
|
+
Castor hereby grants you a personal, non-transferable, non-exclusive licence to use the "Castor Extractor" software on your devices in accordance with the terms of this EULA agreement.
|
|
61
|
+
|
|
62
|
+
You are permitted to load the "Castor Extractor" software (for example a PC, laptop, mobile or tablet) under your control. You are responsible for ensuring your device meets the minimum requirements of the "Castor Extractor" software.
|
|
63
|
+
|
|
64
|
+
You are not permitted to:
|
|
65
|
+
|
|
66
|
+
- Edit, alter, modify, adapt, translate or otherwise change the whole or any part of the Software nor permit the whole or any part of the Software to be combined with or become incorporated in any other software, nor decompile, disassemble or reverse engineer the Software or attempt to do any such things
|
|
67
|
+
- Reproduce, copy, distribute, resell or otherwise use the Software for any commercial purpose
|
|
68
|
+
- Allow any third party to use the Software on behalf of or for the benefit of any third party
|
|
69
|
+
- Use the Software in any way which breaches any applicable local, national or international law
|
|
70
|
+
- use the Software for any purpose that Castor considers is a breach of this EULA agreement
|
|
71
|
+
|
|
72
|
+
## Intellectual Property and Ownership
|
|
73
|
+
|
|
74
|
+
Castor shall at all times retain ownership of the Software as originally downloaded by you and all subsequent downloads of the Software by you. The Software (and the copyright, and other intellectual property rights of whatever nature in the Software, including any modifications made thereto) are and shall remain the property of Castor.
|
|
75
|
+
|
|
76
|
+
Castor reserves the right to grant licences to use the Software to third parties.
|
|
77
|
+
|
|
78
|
+
## Termination
|
|
79
|
+
|
|
80
|
+
This EULA agreement is effective from the date you first use the Software and shall continue until terminated. You may terminate it at any time upon written notice to Castor.
|
|
81
|
+
|
|
82
|
+
It will also terminate immediately if you fail to comply with any term of this EULA agreement. Upon such termination, the licenses granted by this EULA agreement will immediately terminate and you agree to stop all access and use of the Software. The provisions that by their nature continue and survive will survive any termination of this EULA agreement.
|
|
83
|
+
|
|
84
|
+
## Governing Law
|
|
85
|
+
|
|
86
|
+
This EULA agreement, and any dispute arising out of or in connection with this EULA agreement, shall be governed by and construed in accordance with the laws of France.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: castor-extractor
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.3
|
|
4
4
|
Summary: Extract your metadata assets.
|
|
5
5
|
Home-page: https://www.castordoc.com/
|
|
6
6
|
License: EULA
|
|
@@ -27,10 +27,9 @@ Provides-Extra: redshift
|
|
|
27
27
|
Provides-Extra: snowflake
|
|
28
28
|
Provides-Extra: sqlserver
|
|
29
29
|
Provides-Extra: tableau
|
|
30
|
-
Requires-Dist: click (>=8.1,<8.2)
|
|
31
30
|
Requires-Dist: cryptography (>=41.0.5) ; extra == "snowflake"
|
|
32
31
|
Requires-Dist: google-api-core (>=2.1.1,<3.0.0)
|
|
33
|
-
Requires-Dist: google-auth (>=
|
|
32
|
+
Requires-Dist: google-auth (>=2,<3)
|
|
34
33
|
Requires-Dist: google-cloud-core (>=2.1.0,<3.0.0)
|
|
35
34
|
Requires-Dist: google-cloud-storage (>=2,<3)
|
|
36
35
|
Requires-Dist: google-resumable-media (>=2.0.3,<3.0.0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=eRvmcZqJY1G4yDR2CzrA5wzf6xpeZM80HzVBw1tUynw,9959
|
|
2
2
|
Dockerfile,sha256=HcX5z8OpeSvkScQsN-Y7CNMUig_UB6vTMDl7uqzuLGE,303
|
|
3
3
|
LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
|
|
4
4
|
README.md,sha256=uF6PXm9ocPITlKVSh9afTakHmpLx3TvawLf-CbMP3wM,3578
|
|
@@ -95,7 +95,7 @@ castor_extractor/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
95
95
|
castor_extractor/visualization/domo/__init__.py,sha256=_mAYVfoVLizfLGF_f6ZiwBhdPpvoJY_diySf33dt3Jo,127
|
|
96
96
|
castor_extractor/visualization/domo/assets.py,sha256=bK1urFR2tnlWkVkkhR32mAKMoKbESNlop-CNGx-65PY,206
|
|
97
97
|
castor_extractor/visualization/domo/client/__init__.py,sha256=UDszV3IXNC9Wp_j55NZ-6ey2INo0TYtAg2QNIJOjglE,88
|
|
98
|
-
castor_extractor/visualization/domo/client/client.py,sha256=
|
|
98
|
+
castor_extractor/visualization/domo/client/client.py,sha256=ZJJh0ZWuh1X4_uW6FEcZUUAxgRRVFh9vWUtS5aJly4Q,11075
|
|
99
99
|
castor_extractor/visualization/domo/client/client_test.py,sha256=qGGmpJHnm-o2Ybko5J31nSM9xev5YS0yXjVNM9E92b4,2378
|
|
100
100
|
castor_extractor/visualization/domo/client/credentials.py,sha256=CksQ9W9X6IGjTlYN0okwGAmURMRJKAjctxODAvAJUAo,1148
|
|
101
101
|
castor_extractor/visualization/domo/client/endpoints.py,sha256=_D_gq99d77am4KHeRCkITM_XyYuAOscHagC7t3adscY,2019
|
|
@@ -157,11 +157,11 @@ castor_extractor/visualization/mode/extract.py,sha256=ZmIOmVZ8_fo4qXe15G5Sis_IzZ
|
|
|
157
157
|
castor_extractor/visualization/powerbi/__init__.py,sha256=XSr_fNSsR-EPuGOFo7Ai1r7SttiN7bzD3jyYRFXUWgQ,106
|
|
158
158
|
castor_extractor/visualization/powerbi/assets.py,sha256=SASUjxtoOMag3NAlZfhpCy0sLap7WfENEMaEZuBrw6o,801
|
|
159
159
|
castor_extractor/visualization/powerbi/client/__init__.py,sha256=hU8LE1gV9RttTGJiwVpEa9xDLR4IMkUdshQGthg4zzE,62
|
|
160
|
-
castor_extractor/visualization/powerbi/client/constants.py,sha256=
|
|
160
|
+
castor_extractor/visualization/powerbi/client/constants.py,sha256=gpcWE3Ov2xvZAZCqOsvzLtd3cWmfZBeQWvLnnt7-gac,2356
|
|
161
161
|
castor_extractor/visualization/powerbi/client/credentials.py,sha256=iiYaCa2FM1PBHv4YA0Z1LgdX9gnaQhvHGD0LQb7Tcxw,465
|
|
162
162
|
castor_extractor/visualization/powerbi/client/credentials_test.py,sha256=23ZlLCvsPB_fmqntnzULkv0mMRE8NCzBXtWS6wupJn4,787
|
|
163
|
-
castor_extractor/visualization/powerbi/client/rest.py,sha256=
|
|
164
|
-
castor_extractor/visualization/powerbi/client/rest_test.py,sha256=
|
|
163
|
+
castor_extractor/visualization/powerbi/client/rest.py,sha256=_MhJYa9dKla4bMv01GZLFdAMrr6gwHdSWuc_D63gMF0,9949
|
|
164
|
+
castor_extractor/visualization/powerbi/client/rest_test.py,sha256=yAfsksL-4SZ6gxRjAWGqIGUAX5Gz44j56r-jRlPGDro,8514
|
|
165
165
|
castor_extractor/visualization/powerbi/client/utils.py,sha256=0RcoWcKOdvIGH4f3lYDvufmiMo4tr_ABFlITSrvXjTs,541
|
|
166
166
|
castor_extractor/visualization/powerbi/client/utils_test.py,sha256=ULHL2JLrcv0xjW2r7QF_ce2OaGeeSzajkMDywJ8ZdVA,719
|
|
167
167
|
castor_extractor/visualization/powerbi/extract.py,sha256=0rTvI5CiWTpoJx6bGdpShdl4eMBWjuWbRpKvisuLPbw,1328
|
|
@@ -202,9 +202,9 @@ castor_extractor/visualization/sigma/client/pagination.py,sha256=EZGMaONTzZ15VIN
|
|
|
202
202
|
castor_extractor/visualization/sigma/constants.py,sha256=6oQKTKNQkHP_9GWvSOKeFaXd3pKJLhn9Mfod4nvOLEs,144
|
|
203
203
|
castor_extractor/visualization/sigma/extract.py,sha256=OgjUsc1o6lPPaO5XHgCrgQelBrqbdemxKggF4JBPBUI,2678
|
|
204
204
|
castor_extractor/visualization/tableau/__init__.py,sha256=hDohrWjkorrX01JMc154aa9vi3ZqBKmA1lkfQtMFfYE,114
|
|
205
|
-
castor_extractor/visualization/tableau/assets.py,sha256=
|
|
205
|
+
castor_extractor/visualization/tableau/assets.py,sha256=mfBUzcBCLyiU9gnTB_6rvtiB5yXSDU99nezhGC__HQo,1270
|
|
206
206
|
castor_extractor/visualization/tableau/client/__init__.py,sha256=FQX1MdxS8Opn3Oyq8eby7suk3ANbLlpzzCPQ3zqvk0I,78
|
|
207
|
-
castor_extractor/visualization/tableau/client/client.py,sha256=
|
|
207
|
+
castor_extractor/visualization/tableau/client/client.py,sha256=YqLumujwaX3XkIGSTQMlpxqg7z3_7rm2Qof1HfSbUcY,7381
|
|
208
208
|
castor_extractor/visualization/tableau/client/client_utils.py,sha256=taTCeK41nbwXTZeWCBbFxfCSlNnEq4Qfaxlle7yJVic,2094
|
|
209
209
|
castor_extractor/visualization/tableau/client/credentials.py,sha256=szq2tM6sOZqtdyHZgPCNUAddETrwTZaDizLqp-aVBEw,3386
|
|
210
210
|
castor_extractor/visualization/tableau/client/project.py,sha256=uLlZ5-eZI_4VxBmEB5d1gWy_X_w6uVt2EKoiX9cJ0UA,812
|
|
@@ -212,7 +212,7 @@ castor_extractor/visualization/tableau/client/safe_mode.py,sha256=MF_PTfR3oAA255
|
|
|
212
212
|
castor_extractor/visualization/tableau/constants.py,sha256=O2CqeviFz122BumNHoJ1N-e1lzyqIHF9OYnGQttg4hg,126
|
|
213
213
|
castor_extractor/visualization/tableau/errors.py,sha256=WWvmnp5pdxFJqanPKeDRADZc0URSPxkJqxDI6bwoifQ,91
|
|
214
214
|
castor_extractor/visualization/tableau/extract.py,sha256=tCa5g1_RyhS7OeeEMhLJReMIYfQCF1cwHt8KbZoHFyI,3040
|
|
215
|
-
castor_extractor/visualization/tableau/gql_fields.py,sha256=
|
|
215
|
+
castor_extractor/visualization/tableau/gql_fields.py,sha256=6Esb54CVd4j0Gw-kdCT4eVjwpPlnRF_GC4oxuNJpuas,5532
|
|
216
216
|
castor_extractor/visualization/tableau/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
217
217
|
castor_extractor/visualization/tableau/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
218
218
|
castor_extractor/visualization/tableau/tests/unit/assets/graphql/metadata/metadata_1_get.json,sha256=4iMvJ_VakDa67xN2ROraAccaz_DDxX6Y5Y1XnTU5F5Y,446
|
|
@@ -262,11 +262,11 @@ castor_extractor/warehouse/bigquery/queries/view_ddl.sql,sha256=obCm-IN9V8_YSZTw
|
|
|
262
262
|
castor_extractor/warehouse/bigquery/query.py,sha256=hrFfjd5jW2oQnZ6ozlkn-gDe6sCIzu5zSX19T9W6fIk,4162
|
|
263
263
|
castor_extractor/warehouse/bigquery/types.py,sha256=LZVWSmE57lOemNbB5hBRyYmDk9bFAU4nbRaJWALl6N8,140
|
|
264
264
|
castor_extractor/warehouse/databricks/__init__.py,sha256=bTvDxjGQGM2J3hOnVhfNmFP1y8DK0tySiD_EXe5_xWE,200
|
|
265
|
-
castor_extractor/warehouse/databricks/client.py,sha256=
|
|
266
|
-
castor_extractor/warehouse/databricks/client_test.py,sha256=
|
|
265
|
+
castor_extractor/warehouse/databricks/client.py,sha256=u1KpiG16IlFbaEVAIzBlxnzTk_bARGh-D0sZBXtgF4c,8043
|
|
266
|
+
castor_extractor/warehouse/databricks/client_test.py,sha256=ctOQnUXosuuFjWGJKgkxjUcV4vQUBWt2BQ_f0Tyzqe4,2717
|
|
267
267
|
castor_extractor/warehouse/databricks/credentials.py,sha256=sMpOAKhBklcmTpcr3mi3o8qLud__8PTZbQUT3K_TRY8,678
|
|
268
|
-
castor_extractor/warehouse/databricks/extract.py,sha256=
|
|
269
|
-
castor_extractor/warehouse/databricks/format.py,sha256=
|
|
268
|
+
castor_extractor/warehouse/databricks/extract.py,sha256=mgl1_b9Mlir9ZU3R5HV689YlhzzhlyVN8IaBHaNwY54,5752
|
|
269
|
+
castor_extractor/warehouse/databricks/format.py,sha256=LiPGCTPzL3gQQMMl1v6DvpcTk7BWxZFq03jnHdoYnuU,4968
|
|
270
270
|
castor_extractor/warehouse/databricks/format_test.py,sha256=iPmdJof43fBYL1Sa_fBrCWDQHCHgm7IWCZag1kWkj9E,1970
|
|
271
271
|
castor_extractor/warehouse/databricks/types.py,sha256=T2SyLy9pY_olLtstdC77moPxIiikVsuQLMxh92YMJQo,78
|
|
272
272
|
castor_extractor/warehouse/mysql/__init__.py,sha256=2KFDogo9GNbApHqw3Vm5t_uNmIRjdp76nmP_WQQMfQY,116
|
|
@@ -346,7 +346,8 @@ castor_extractor/warehouse/synapse/queries/schema.sql,sha256=aX9xNrBD_ydwl-znGSF
|
|
|
346
346
|
castor_extractor/warehouse/synapse/queries/table.sql,sha256=mCE8bR1Vb7j7SwZW2gafcXidQ2fo1HwxcybA8wP2Kfs,1049
|
|
347
347
|
castor_extractor/warehouse/synapse/queries/user.sql,sha256=sTb_SS7Zj3AXW1SggKPLNMCd0qoTpL7XI_BJRMaEpBg,67
|
|
348
348
|
castor_extractor/warehouse/synapse/queries/view_ddl.sql,sha256=3EVbp5_yTgdByHFIPLHmnoOnqqLE77SrjAwFDvu4e54,249
|
|
349
|
-
castor_extractor-0.16.
|
|
350
|
-
castor_extractor-0.16.
|
|
351
|
-
castor_extractor-0.16.
|
|
352
|
-
castor_extractor-0.16.
|
|
349
|
+
castor_extractor-0.16.3.dist-info/LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
|
|
350
|
+
castor_extractor-0.16.3.dist-info/METADATA,sha256=CaGi5itpnLSjjCv5PpKayJ2Oi859ewvcyrPFzHNIdYM,6370
|
|
351
|
+
castor_extractor-0.16.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
352
|
+
castor_extractor-0.16.3.dist-info/entry_points.txt,sha256=EQUCoNjSHevxmY5ZathX_fLZPcuBHng23rj0SSUrLtI,1345
|
|
353
|
+
castor_extractor-0.16.3.dist-info/RECORD,,
|
|
File without changes
|