castor-extractor 0.20.0__py3-none-any.whl → 0.20.5__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 +20 -0
- castor_extractor/commands/extract_redshift.py +6 -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/redshift/extract.py +10 -1
- castor_extractor/warehouse/redshift/extract_test.py +26 -0
- castor_extractor/warehouse/redshift/queries/query_serverless.sql +69 -0
- castor_extractor/warehouse/redshift/query.py +12 -1
- 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.5.dist-info}/METADATA +21 -1
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/RECORD +39 -26
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.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.5.dist-info}/LICENCE +0 -0
- {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/WHEEL +0 -0
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from datetime import date, datetime
|
|
3
|
-
from time import sleep
|
|
4
|
-
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
5
|
-
|
|
6
|
-
import msal # type: ignore
|
|
7
|
-
import requests
|
|
8
|
-
|
|
9
|
-
from ....utils import at_midnight, format_date, yesterday
|
|
10
|
-
from ..assets import PowerBiAsset
|
|
11
|
-
from .constants import (
|
|
12
|
-
DEFAULT_TIMEOUT_IN_SECS,
|
|
13
|
-
GET,
|
|
14
|
-
POST,
|
|
15
|
-
SCAN_READY,
|
|
16
|
-
Batches,
|
|
17
|
-
Keys,
|
|
18
|
-
QueryParams,
|
|
19
|
-
Urls,
|
|
20
|
-
)
|
|
21
|
-
from .credentials import PowerbiCredentials
|
|
22
|
-
from .utils import batch_size_is_valid_or_assert, datetime_is_recent_or_assert
|
|
23
|
-
|
|
24
|
-
logger = logging.getLogger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _time_filter(day: Optional[date]) -> Tuple[datetime, datetime]:
|
|
28
|
-
target_day = day or yesterday()
|
|
29
|
-
start = at_midnight(target_day)
|
|
30
|
-
end = datetime.combine(target_day, datetime.max.time())
|
|
31
|
-
return start, end
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _url(
|
|
35
|
-
day: Optional[date],
|
|
36
|
-
continuation_uri: Optional[str],
|
|
37
|
-
) -> str:
|
|
38
|
-
if continuation_uri:
|
|
39
|
-
return continuation_uri
|
|
40
|
-
|
|
41
|
-
url = Urls.ACTIVITY_EVENTS
|
|
42
|
-
start, end = _time_filter(day)
|
|
43
|
-
url += "?$filter=Activity eq 'viewreport'"
|
|
44
|
-
url += f"&startDateTime='{format_date(start)}'"
|
|
45
|
-
url += f"&endDateTime='{format_date(end)}'"
|
|
46
|
-
return url
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class Client:
|
|
50
|
-
"""
|
|
51
|
-
PowerBI rest admin api
|
|
52
|
-
https://learn.microsoft.com/en-us/rest/api/power-bi/admin
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
def __init__(self, credentials: PowerbiCredentials):
|
|
56
|
-
self.creds = credentials
|
|
57
|
-
client_app = f"{Urls.CLIENT_APP_BASE}{self.creds.tenant_id}"
|
|
58
|
-
self.app = msal.ConfidentialClientApplication(
|
|
59
|
-
client_id=self.creds.client_id,
|
|
60
|
-
authority=client_app,
|
|
61
|
-
client_credential=self.creds.secret,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
def _access_token(self) -> dict:
|
|
65
|
-
token = self.app.acquire_token_for_client(scopes=self.creds.scopes)
|
|
66
|
-
|
|
67
|
-
if Keys.ACCESS_TOKEN not in token:
|
|
68
|
-
raise ValueError(f"No access token in token response: {token}")
|
|
69
|
-
|
|
70
|
-
return token
|
|
71
|
-
|
|
72
|
-
def _header(self) -> Dict:
|
|
73
|
-
"""Return header used in following rest api call"""
|
|
74
|
-
token = self._access_token()
|
|
75
|
-
return {"Authorization": f"Bearer {token[Keys.ACCESS_TOKEN]}"}
|
|
76
|
-
|
|
77
|
-
def _call(
|
|
78
|
-
self,
|
|
79
|
-
url: str,
|
|
80
|
-
method: str = GET,
|
|
81
|
-
*,
|
|
82
|
-
params: Optional[Dict] = None,
|
|
83
|
-
data: Optional[dict] = None,
|
|
84
|
-
processor: Optional[Callable] = None,
|
|
85
|
-
) -> Any:
|
|
86
|
-
"""
|
|
87
|
-
Make either a get or a post http request.Request, by default
|
|
88
|
-
result.json is returned. Optionally you can provide a processor callback
|
|
89
|
-
to transform the result.
|
|
90
|
-
"""
|
|
91
|
-
logger.debug(f"Calling {method} on {url}")
|
|
92
|
-
result = requests.request(
|
|
93
|
-
method,
|
|
94
|
-
url,
|
|
95
|
-
headers=self._header(),
|
|
96
|
-
params=params,
|
|
97
|
-
data=data,
|
|
98
|
-
)
|
|
99
|
-
result.raise_for_status()
|
|
100
|
-
|
|
101
|
-
if processor:
|
|
102
|
-
return processor(result)
|
|
103
|
-
|
|
104
|
-
return result.json()
|
|
105
|
-
|
|
106
|
-
def _get(
|
|
107
|
-
self,
|
|
108
|
-
url: str,
|
|
109
|
-
*,
|
|
110
|
-
params: Optional[Dict] = None,
|
|
111
|
-
processor: Optional[Callable] = None,
|
|
112
|
-
) -> Any:
|
|
113
|
-
return self._call(url, GET, params=params, processor=processor)
|
|
114
|
-
|
|
115
|
-
def _post(
|
|
116
|
-
self,
|
|
117
|
-
url: str,
|
|
118
|
-
*,
|
|
119
|
-
params: Optional[dict],
|
|
120
|
-
data: Optional[dict],
|
|
121
|
-
processor: Optional[Callable] = None,
|
|
122
|
-
) -> Any:
|
|
123
|
-
return self._call(
|
|
124
|
-
url,
|
|
125
|
-
POST,
|
|
126
|
-
params=params,
|
|
127
|
-
data=data,
|
|
128
|
-
processor=processor,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
def _workspace_ids(
|
|
132
|
-
self,
|
|
133
|
-
modified_since: Optional[datetime] = None,
|
|
134
|
-
) -> List[str]:
|
|
135
|
-
"""
|
|
136
|
-
Get workspaces ids from powerBI admin API.
|
|
137
|
-
If modified_since, take only workspaces that have been modified since
|
|
138
|
-
|
|
139
|
-
more: https://learn.microsoft.com/en-us/rest/api/power-bi/admin/workspace-info-get-modified-workspaces
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
def result_callback(call_result: requests.models.Response) -> List[str]:
|
|
143
|
-
return [x["id"] for x in call_result.json()]
|
|
144
|
-
|
|
145
|
-
params: Dict[str, Union[bool, str]] = {
|
|
146
|
-
Keys.INACTIVE_WORKSPACES: True,
|
|
147
|
-
Keys.PERSONAL_WORKSPACES: True,
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if modified_since:
|
|
151
|
-
datetime_is_recent_or_assert(modified_since)
|
|
152
|
-
modified_since_iso = f"{modified_since.isoformat()}0Z"
|
|
153
|
-
params[Keys.MODIFIED_SINCE] = modified_since_iso
|
|
154
|
-
|
|
155
|
-
result = self._get(
|
|
156
|
-
Urls.WORKSPACE_IDS,
|
|
157
|
-
params=params,
|
|
158
|
-
processor=result_callback,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
return result
|
|
162
|
-
|
|
163
|
-
def _create_scan(self, workspaces_ids: List[str]) -> int:
|
|
164
|
-
batch_size_is_valid_or_assert(workspaces_ids)
|
|
165
|
-
request_body = {"workspaces": workspaces_ids}
|
|
166
|
-
params = QueryParams.METADATA_SCAN
|
|
167
|
-
scan_id = self._post(
|
|
168
|
-
Urls.METADATA_POST,
|
|
169
|
-
params=params,
|
|
170
|
-
data=request_body,
|
|
171
|
-
)
|
|
172
|
-
return scan_id[Keys.ID]
|
|
173
|
-
|
|
174
|
-
def _wait_for_scan_result(self, scan_id: int) -> bool:
|
|
175
|
-
url = f"{Urls.METADATA_WAIT}/{scan_id}"
|
|
176
|
-
waiting_seconds = 0
|
|
177
|
-
sleep_seconds = 1
|
|
178
|
-
while True:
|
|
179
|
-
result = self._get(url, processor=lambda x: x)
|
|
180
|
-
if result.status_code != 200:
|
|
181
|
-
return False
|
|
182
|
-
if result.json()[Keys.STATUS] == SCAN_READY:
|
|
183
|
-
logger.info(f"scan {scan_id} ready")
|
|
184
|
-
return True
|
|
185
|
-
if waiting_seconds >= DEFAULT_TIMEOUT_IN_SECS:
|
|
186
|
-
break
|
|
187
|
-
waiting_seconds += sleep_seconds
|
|
188
|
-
logger.info(
|
|
189
|
-
f"Waiting {sleep_seconds} sec for scan {scan_id} to be ready…",
|
|
190
|
-
)
|
|
191
|
-
sleep(sleep_seconds)
|
|
192
|
-
return False
|
|
193
|
-
|
|
194
|
-
def _get_scan(self, scan_id: int) -> List[dict]:
|
|
195
|
-
url = f"{Urls.METADATA_GET}/{scan_id}"
|
|
196
|
-
return self._get(url)[Keys.WORKSPACES]
|
|
197
|
-
|
|
198
|
-
def _activity_events(
|
|
199
|
-
self,
|
|
200
|
-
*,
|
|
201
|
-
day: Optional[date] = None,
|
|
202
|
-
continuation_uri: Optional[str] = None,
|
|
203
|
-
) -> List[Dict]:
|
|
204
|
-
"""
|
|
205
|
-
Returns a list of activity events for the organization.
|
|
206
|
-
https://learn.microsoft.com/en-us/power-bi/admin/service-admin-auditing#activityevents-rest-api
|
|
207
|
-
- when no day is specified, fallback is yesterday
|
|
208
|
-
- continuation_uri allows to fetch paginated data (internal usage)
|
|
209
|
-
"""
|
|
210
|
-
url = _url(day, continuation_uri)
|
|
211
|
-
answer = self._get(url)
|
|
212
|
-
activity_events = answer[Keys.ACTIVITY_EVENT_ENTITIES]
|
|
213
|
-
is_last = answer[Keys.LAST_RESULT_SET]
|
|
214
|
-
assert isinstance(is_last, bool)
|
|
215
|
-
if is_last:
|
|
216
|
-
return activity_events
|
|
217
|
-
|
|
218
|
-
# there are more data to fetch
|
|
219
|
-
# https://learn.microsoft.com/en-us/rest/api/power-bi/admin/get-activity-events#get-the-next-set-of-audit-activity-events-by-sending-the-continuation-token-to-the-api-example
|
|
220
|
-
continuation_uri = answer[Keys.CONTINUATION_URI]
|
|
221
|
-
rest = self._activity_events(continuation_uri=continuation_uri)
|
|
222
|
-
activity_events.extend(rest)
|
|
223
|
-
return activity_events
|
|
224
|
-
|
|
225
|
-
def _datasets(self) -> List[Dict]:
|
|
226
|
-
"""
|
|
227
|
-
Returns a list of datasets for the organization.
|
|
228
|
-
https://learn.microsoft.com/en-us/rest/api/power-bi/admin/datasets-get-datasets-as-admin
|
|
229
|
-
"""
|
|
230
|
-
return self._get(Urls.DATASETS)[Keys.VALUE]
|
|
231
|
-
|
|
232
|
-
def _reports(self) -> List[Dict]:
|
|
233
|
-
"""
|
|
234
|
-
Returns a list of reports for the organization.
|
|
235
|
-
https://learn.microsoft.com/en-us/rest/api/power-bi/admin/reports-get-reports-as-admin
|
|
236
|
-
"""
|
|
237
|
-
reports = self._get(Urls.REPORTS)[Keys.VALUE]
|
|
238
|
-
for report in reports:
|
|
239
|
-
report_id = report.get("id")
|
|
240
|
-
try:
|
|
241
|
-
url = Urls.REPORTS + f"/{report_id}/pages"
|
|
242
|
-
pages = self._get(url)[Keys.VALUE]
|
|
243
|
-
report["pages"] = pages
|
|
244
|
-
except (requests.HTTPError, requests.exceptions.Timeout) as e:
|
|
245
|
-
logger.debug(e)
|
|
246
|
-
continue
|
|
247
|
-
return reports
|
|
248
|
-
|
|
249
|
-
def _dashboards(self) -> List[Dict]:
|
|
250
|
-
"""
|
|
251
|
-
Returns a list of dashboards for the organization.
|
|
252
|
-
https://learn.microsoft.com/en-us/rest/api/power-bi/admin/dashboards-get-dashboards-as-admin
|
|
253
|
-
"""
|
|
254
|
-
return self._get(Urls.DASHBOARD)[Keys.VALUE]
|
|
255
|
-
|
|
256
|
-
def _metadata(
|
|
257
|
-
self,
|
|
258
|
-
modified_since: Optional[datetime] = None,
|
|
259
|
-
) -> Iterator[List[Dict]]:
|
|
260
|
-
"""
|
|
261
|
-
Fetch metadata by workspace.
|
|
262
|
-
https://learn.microsoft.com/en-us/power-bi/enterprise/service-admin-metadata-scanning
|
|
263
|
-
"""
|
|
264
|
-
ids = self._workspace_ids(modified_since)
|
|
265
|
-
|
|
266
|
-
for ix in range(0, len(ids), Batches.METADATA):
|
|
267
|
-
batch_ids = [w_id for w_id in ids[ix : ix + Batches.METADATA]]
|
|
268
|
-
scan_id = self._create_scan(batch_ids)
|
|
269
|
-
self._wait_for_scan_result(scan_id)
|
|
270
|
-
yield self._get_scan(scan_id)
|
|
271
|
-
|
|
272
|
-
def test_connection(self) -> None:
|
|
273
|
-
"""Use credentials & verify requesting the API doesn't raise an error"""
|
|
274
|
-
self._header()
|
|
275
|
-
|
|
276
|
-
def fetch(
|
|
277
|
-
self,
|
|
278
|
-
asset: PowerBiAsset,
|
|
279
|
-
*,
|
|
280
|
-
modified_since: Optional[datetime] = None,
|
|
281
|
-
day: Optional[date] = None,
|
|
282
|
-
) -> List[Dict]:
|
|
283
|
-
"""
|
|
284
|
-
Given a PowerBi asset, returns the corresponding data using the
|
|
285
|
-
appropriate client.
|
|
286
|
-
"""
|
|
287
|
-
logger.info(f"Starting extraction of {asset}")
|
|
288
|
-
asset = PowerBiAsset(asset)
|
|
289
|
-
|
|
290
|
-
if asset == PowerBiAsset.ACTIVITY_EVENTS:
|
|
291
|
-
return self._activity_events(day=day)
|
|
292
|
-
|
|
293
|
-
if asset == PowerBiAsset.DATASETS:
|
|
294
|
-
return self._datasets()
|
|
295
|
-
|
|
296
|
-
if asset == PowerBiAsset.DASHBOARDS:
|
|
297
|
-
return self._dashboards()
|
|
298
|
-
|
|
299
|
-
if asset == PowerBiAsset.REPORTS:
|
|
300
|
-
return self._reports()
|
|
301
|
-
|
|
302
|
-
assert asset == PowerBiAsset.METADATA
|
|
303
|
-
return [
|
|
304
|
-
item for batch in self._metadata(modified_since) for item in batch
|
|
305
|
-
]
|
|
@@ -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
|