castor-extractor 0.20.0__py3-none-any.whl → 0.20.4__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 +16 -0
- castor_extractor/commands/extract_thoughtspot.py +18 -0
- castor_extractor/utils/client/api/client.py +7 -2
- castor_extractor/utils/client/api/safe_request.py +6 -3
- castor_extractor/visualization/looker/api/constants.py +0 -4
- castor_extractor/visualization/powerbi/__init__.py +1 -1
- castor_extractor/visualization/powerbi/assets.py +7 -1
- castor_extractor/visualization/powerbi/client/__init__.py +2 -3
- castor_extractor/visualization/powerbi/client/authentication.py +27 -0
- castor_extractor/visualization/powerbi/client/client.py +207 -0
- castor_extractor/visualization/powerbi/client/client_test.py +173 -0
- castor_extractor/visualization/powerbi/client/constants.py +0 -67
- castor_extractor/visualization/powerbi/client/credentials.py +3 -4
- castor_extractor/visualization/powerbi/client/credentials_test.py +3 -4
- castor_extractor/visualization/powerbi/client/endpoints.py +65 -0
- castor_extractor/visualization/powerbi/client/pagination.py +32 -0
- castor_extractor/visualization/powerbi/extract.py +14 -9
- castor_extractor/visualization/thoughtspot/__init__.py +3 -0
- castor_extractor/visualization/thoughtspot/assets.py +9 -0
- castor_extractor/visualization/thoughtspot/client/__init__.py +2 -0
- castor_extractor/visualization/thoughtspot/client/client.py +120 -0
- castor_extractor/visualization/thoughtspot/client/credentials.py +18 -0
- castor_extractor/visualization/thoughtspot/client/endpoints.py +12 -0
- castor_extractor/visualization/thoughtspot/client/utils.py +25 -0
- castor_extractor/visualization/thoughtspot/client/utils_test.py +57 -0
- castor_extractor/visualization/thoughtspot/extract.py +49 -0
- castor_extractor/warehouse/salesforce/client.py +1 -1
- castor_extractor/warehouse/salesforce/format.py +40 -30
- castor_extractor/warehouse/salesforce/format_test.py +61 -24
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.4.dist-info}/METADATA +17 -1
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.4.dist-info}/RECORD +34 -23
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.4.dist-info}/entry_points.txt +1 -0
- castor_extractor/visualization/powerbi/client/rest.py +0 -305
- castor_extractor/visualization/powerbi/client/rest_test.py +0 -290
- castor_extractor/visualization/powerbi/client/utils.py +0 -19
- castor_extractor/visualization/powerbi/client/utils_test.py +0 -24
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.4.dist-info}/LICENCE +0 -0
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.4.dist-info}/WHEEL +0 -0
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
from unittest.mock import ANY, Mock, call, patch
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from requests import HTTPError
|
|
6
|
-
|
|
7
|
-
from .constants import GET, POST, Assertions, Keys, QueryParams, Urls
|
|
8
|
-
from .credentials import PowerbiCredentials
|
|
9
|
-
from .rest import Client, msal
|
|
10
|
-
|
|
11
|
-
FAKE_TENANT_ID = "IamFake"
|
|
12
|
-
FAKE_CLIENT_ID = "MeTwo"
|
|
13
|
-
FAKE_SECRET = "MeThree"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _client() -> Client:
|
|
17
|
-
creds = PowerbiCredentials(
|
|
18
|
-
tenant_id=FAKE_TENANT_ID,
|
|
19
|
-
client_id=FAKE_CLIENT_ID,
|
|
20
|
-
secret=FAKE_SECRET,
|
|
21
|
-
)
|
|
22
|
-
return Client(creds)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _raise_http_error() -> None:
|
|
26
|
-
raise HTTPError(request=Mock(), response=Mock())
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
30
|
-
def test__access_token(mock_app):
|
|
31
|
-
# init mocks
|
|
32
|
-
valid_response = {"access_token": "mock_token"}
|
|
33
|
-
returning_valid_token = Mock(return_value=valid_response)
|
|
34
|
-
mock_app.return_value.acquire_token_for_client = returning_valid_token
|
|
35
|
-
|
|
36
|
-
# init client
|
|
37
|
-
client = _client()
|
|
38
|
-
|
|
39
|
-
# generated token
|
|
40
|
-
assert client._access_token() == valid_response
|
|
41
|
-
|
|
42
|
-
# token missing in response
|
|
43
|
-
invalid_response = {"not_access_token": "666"}
|
|
44
|
-
returning_invalid_token = Mock(return_value=invalid_response)
|
|
45
|
-
mock_app.return_value.acquire_token_for_client = returning_invalid_token
|
|
46
|
-
|
|
47
|
-
with pytest.raises(ValueError):
|
|
48
|
-
client._access_token()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
52
|
-
@patch.object(Client, "_access_token")
|
|
53
|
-
def test__headers(mock_access_token, mock_app):
|
|
54
|
-
mock_app.return_value = None
|
|
55
|
-
client = _client()
|
|
56
|
-
mock_access_token.return_value = {Keys.ACCESS_TOKEN: "666"}
|
|
57
|
-
assert client._header() == {"Authorization": "Bearer 666"}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
61
|
-
@patch("requests.request")
|
|
62
|
-
@patch.object(Client, "_access_token")
|
|
63
|
-
def test__get(mocked_access_token, mocked_request, mock_app):
|
|
64
|
-
mock_app.return_value = None
|
|
65
|
-
client = _client()
|
|
66
|
-
mocked_access_token.return_value = {Keys.ACCESS_TOKEN: "666"}
|
|
67
|
-
fact = {"fact": "Approximately 24 cat skins can make a coat.", "length": 43}
|
|
68
|
-
mocked_request.return_value = Mock(json=lambda: fact)
|
|
69
|
-
|
|
70
|
-
result = client._get("https://catfact.ninja/fact")
|
|
71
|
-
assert result == fact
|
|
72
|
-
|
|
73
|
-
result = client._get("https://catfact.ninja/fact")["length"]
|
|
74
|
-
assert result == 43
|
|
75
|
-
|
|
76
|
-
mocked_request.return_value = Mock(raise_for_status=_raise_http_error)
|
|
77
|
-
|
|
78
|
-
with pytest.raises(HTTPError):
|
|
79
|
-
result = client._get("https/whatev.er")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
83
|
-
@patch("requests.request")
|
|
84
|
-
@patch.object(Client, "_access_token")
|
|
85
|
-
def test__workspace_ids(_, mocked_request, mock_app):
|
|
86
|
-
mock_app.return_value = None
|
|
87
|
-
client = _client()
|
|
88
|
-
mocked_request.return_value = Mock(
|
|
89
|
-
json=lambda: [{"id": 1000}, {"id": 1001}, {"id": 1003}],
|
|
90
|
-
)
|
|
91
|
-
ids = client._workspace_ids()
|
|
92
|
-
assert ids == [1000, 1001, 1003]
|
|
93
|
-
|
|
94
|
-
with pytest.raises(AssertionError, match=Assertions.DATETIME_TOO_OLD):
|
|
95
|
-
good_old_time = datetime(1998, 7, 12)
|
|
96
|
-
client._workspace_ids(modified_since=good_old_time)
|
|
97
|
-
|
|
98
|
-
yesterday = datetime.today() - timedelta(1)
|
|
99
|
-
ids = client._workspace_ids(modified_since=yesterday)
|
|
100
|
-
params = {
|
|
101
|
-
Keys.INACTIVE_WORKSPACES: True,
|
|
102
|
-
Keys.PERSONAL_WORKSPACES: True,
|
|
103
|
-
Keys.MODIFIED_SINCE: f"{yesterday.isoformat()}0Z",
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
mocked_request.assert_called_with(
|
|
107
|
-
GET,
|
|
108
|
-
Urls.WORKSPACE_IDS,
|
|
109
|
-
data=None,
|
|
110
|
-
headers=ANY,
|
|
111
|
-
params=params,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
116
|
-
@patch("requests.request")
|
|
117
|
-
@patch.object(Client, "_access_token")
|
|
118
|
-
def test__post_default(_, mocked_request, mock_app):
|
|
119
|
-
mock_app.return_value = None
|
|
120
|
-
client = _client()
|
|
121
|
-
url = "https://estcequecestbientotleweekend.fr/"
|
|
122
|
-
params = QueryParams.METADATA_SCAN
|
|
123
|
-
data = {"bonjour": "hello"}
|
|
124
|
-
client._post(url, params=params, data=data)
|
|
125
|
-
mocked_request.assert_called_with(
|
|
126
|
-
POST,
|
|
127
|
-
url,
|
|
128
|
-
headers=ANY,
|
|
129
|
-
params=QueryParams.METADATA_SCAN,
|
|
130
|
-
data=data,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
135
|
-
@patch("requests.request")
|
|
136
|
-
@patch.object(Client, "_access_token")
|
|
137
|
-
def test__post_with_processor(_, mocked_request, mock_app):
|
|
138
|
-
mock_app.return_value = None
|
|
139
|
-
client = _client()
|
|
140
|
-
url = "https://estcequecestbientotleweekend.fr/"
|
|
141
|
-
params = QueryParams.METADATA_SCAN
|
|
142
|
-
data = {"bonjour": "hello"}
|
|
143
|
-
mocked_request.return_value = Mock(json=lambda: {"id": 1000})
|
|
144
|
-
result = client._post(
|
|
145
|
-
url,
|
|
146
|
-
params=params,
|
|
147
|
-
data=data,
|
|
148
|
-
processor=lambda x: x.json()["id"],
|
|
149
|
-
)
|
|
150
|
-
assert result == 1000
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
154
|
-
@patch("requests.request")
|
|
155
|
-
@patch.object(Client, "_access_token")
|
|
156
|
-
def test__datasets(_, mocked_request, mock_app):
|
|
157
|
-
mock_app.return_value = None
|
|
158
|
-
client = _client()
|
|
159
|
-
mocked_request.return_value = Mock(
|
|
160
|
-
json=lambda: {"value": [{"id": 1, "type": "dataset"}]},
|
|
161
|
-
)
|
|
162
|
-
datasets = client._datasets()
|
|
163
|
-
mocked_request.assert_called_with(
|
|
164
|
-
GET,
|
|
165
|
-
Urls.DATASETS,
|
|
166
|
-
data=None,
|
|
167
|
-
headers=ANY,
|
|
168
|
-
params=None,
|
|
169
|
-
)
|
|
170
|
-
assert datasets == [{"id": 1, "type": "dataset"}]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
174
|
-
@patch("requests.request")
|
|
175
|
-
@patch.object(Client, "_access_token")
|
|
176
|
-
def test__reports(_, mocked_request, mock_app):
|
|
177
|
-
mock_app.return_value = None
|
|
178
|
-
client = _client()
|
|
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
|
-
]
|
|
200
|
-
reports = client._reports()
|
|
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
|
-
]
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
213
|
-
@patch("requests.request")
|
|
214
|
-
@patch.object(Client, "_access_token")
|
|
215
|
-
def test__dashboards(_, mocked_request, mock_app):
|
|
216
|
-
mock_app.return_value = None
|
|
217
|
-
client = _client()
|
|
218
|
-
mocked_request.return_value = Mock(
|
|
219
|
-
json=lambda: {"value": [{"id": 1, "type": "dashboard"}]},
|
|
220
|
-
)
|
|
221
|
-
dashboards = client._dashboards()
|
|
222
|
-
mocked_request.assert_called_with(
|
|
223
|
-
GET,
|
|
224
|
-
Urls.DASHBOARD,
|
|
225
|
-
data=None,
|
|
226
|
-
headers=ANY,
|
|
227
|
-
params=None,
|
|
228
|
-
)
|
|
229
|
-
assert dashboards == [{"id": 1, "type": "dashboard"}]
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
233
|
-
@patch.object(Client, "_workspace_ids")
|
|
234
|
-
@patch.object(Client, "_create_scan")
|
|
235
|
-
@patch.object(Client, "_wait_for_scan_result")
|
|
236
|
-
@patch.object(Client, "_get_scan")
|
|
237
|
-
def test__metadata(
|
|
238
|
-
mocked_get_scan,
|
|
239
|
-
mocked_wait,
|
|
240
|
-
mocked_create_scan,
|
|
241
|
-
mocked_workspace_ids,
|
|
242
|
-
mock_app,
|
|
243
|
-
):
|
|
244
|
-
mock_app.return_value = None
|
|
245
|
-
mocked_workspace_ids.return_value = list(range(200))
|
|
246
|
-
mocked_create_scan.return_value = 314
|
|
247
|
-
mocked_wait.return_value = True
|
|
248
|
-
mocked_get_scan.return_value = [{"workspace_id": 1871}]
|
|
249
|
-
|
|
250
|
-
client = _client()
|
|
251
|
-
result = client._metadata()
|
|
252
|
-
|
|
253
|
-
assert list(result) == [[{"workspace_id": 1871}], [{"workspace_id": 1871}]]
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
_CALLS = [
|
|
257
|
-
{
|
|
258
|
-
Keys.ACTIVITY_EVENT_ENTITIES: ["foo", "bar"],
|
|
259
|
-
Keys.LAST_RESULT_SET: False,
|
|
260
|
-
Keys.CONTINUATION_URI: "https://next-call-1",
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
Keys.ACTIVITY_EVENT_ENTITIES: ["baz"],
|
|
264
|
-
Keys.LAST_RESULT_SET: False,
|
|
265
|
-
Keys.CONTINUATION_URI: "https://next-call-2",
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
Keys.ACTIVITY_EVENT_ENTITIES: ["biz"],
|
|
269
|
-
Keys.LAST_RESULT_SET: True,
|
|
270
|
-
Keys.CONTINUATION_URI: None,
|
|
271
|
-
},
|
|
272
|
-
]
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
@patch.object(msal, "ConfidentialClientApplication")
|
|
276
|
-
@patch.object(Client, "_call")
|
|
277
|
-
def test__activity_events(mocked, mock_app):
|
|
278
|
-
mock_app.return_value = None
|
|
279
|
-
client = _client()
|
|
280
|
-
mocked.side_effect = _CALLS
|
|
281
|
-
|
|
282
|
-
result = client._activity_events()
|
|
283
|
-
assert result == ["foo", "bar", "baz", "biz"]
|
|
284
|
-
|
|
285
|
-
expected_calls = [
|
|
286
|
-
call(ANY, GET, params=None, processor=None),
|
|
287
|
-
call("https://next-call-1", GET, params=None, processor=None),
|
|
288
|
-
call("https://next-call-2", GET, params=None, processor=None),
|
|
289
|
-
]
|
|
290
|
-
mocked.assert_has_calls(expected_calls)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
from .constants import RECENT_DAYS, Assertions, Batches
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def batch_size_is_valid_or_assert(ids: List) -> None:
|
|
8
|
-
"""
|
|
9
|
-
assert that current batch is smaller than expected size
|
|
10
|
-
"""
|
|
11
|
-
assert len(ids) <= Batches.METADATA, Assertions.BATCH_TOO_BIG
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def datetime_is_recent_or_assert(dt: datetime) -> None:
|
|
15
|
-
"""
|
|
16
|
-
assert that given datetime is recent
|
|
17
|
-
"""
|
|
18
|
-
valid = dt > datetime.utcnow() - timedelta(RECENT_DAYS)
|
|
19
|
-
assert valid, Assertions.DATETIME_TOO_OLD
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from .constants import Assertions
|
|
6
|
-
from .utils import batch_size_is_valid_or_assert, datetime_is_recent_or_assert
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_batch_size_is_valid_or_assert():
|
|
10
|
-
valid = [1, 3, 4]
|
|
11
|
-
batch_size_is_valid_or_assert(valid)
|
|
12
|
-
|
|
13
|
-
invalid = list(range(8000))
|
|
14
|
-
with pytest.raises(AssertionError, match=Assertions.BATCH_TOO_BIG):
|
|
15
|
-
batch_size_is_valid_or_assert(invalid)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_datetime_is_recent_or_assert():
|
|
19
|
-
krach = datetime(1929, 10, 29)
|
|
20
|
-
with pytest.raises(AssertionError, match=Assertions.DATETIME_TOO_OLD):
|
|
21
|
-
datetime_is_recent_or_assert(krach)
|
|
22
|
-
|
|
23
|
-
yesterday = datetime.today() - timedelta(1)
|
|
24
|
-
datetime_is_recent_or_assert(yesterday)
|
|
File without changes
|
|
File without changes
|