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.

Files changed (43) hide show
  1. CHANGELOG.md +20 -0
  2. castor_extractor/commands/extract_redshift.py +6 -0
  3. castor_extractor/commands/extract_thoughtspot.py +18 -0
  4. castor_extractor/utils/client/api/client.py +7 -2
  5. castor_extractor/utils/client/api/safe_request.py +6 -3
  6. castor_extractor/visualization/looker/api/constants.py +0 -4
  7. castor_extractor/visualization/powerbi/__init__.py +1 -1
  8. castor_extractor/visualization/powerbi/assets.py +7 -1
  9. castor_extractor/visualization/powerbi/client/__init__.py +2 -3
  10. castor_extractor/visualization/powerbi/client/authentication.py +27 -0
  11. castor_extractor/visualization/powerbi/client/client.py +207 -0
  12. castor_extractor/visualization/powerbi/client/client_test.py +173 -0
  13. castor_extractor/visualization/powerbi/client/constants.py +0 -67
  14. castor_extractor/visualization/powerbi/client/credentials.py +3 -4
  15. castor_extractor/visualization/powerbi/client/credentials_test.py +3 -4
  16. castor_extractor/visualization/powerbi/client/endpoints.py +65 -0
  17. castor_extractor/visualization/powerbi/client/pagination.py +32 -0
  18. castor_extractor/visualization/powerbi/extract.py +14 -9
  19. castor_extractor/visualization/thoughtspot/__init__.py +3 -0
  20. castor_extractor/visualization/thoughtspot/assets.py +9 -0
  21. castor_extractor/visualization/thoughtspot/client/__init__.py +2 -0
  22. castor_extractor/visualization/thoughtspot/client/client.py +120 -0
  23. castor_extractor/visualization/thoughtspot/client/credentials.py +18 -0
  24. castor_extractor/visualization/thoughtspot/client/endpoints.py +12 -0
  25. castor_extractor/visualization/thoughtspot/client/utils.py +25 -0
  26. castor_extractor/visualization/thoughtspot/client/utils_test.py +57 -0
  27. castor_extractor/visualization/thoughtspot/extract.py +49 -0
  28. castor_extractor/warehouse/redshift/extract.py +10 -1
  29. castor_extractor/warehouse/redshift/extract_test.py +26 -0
  30. castor_extractor/warehouse/redshift/queries/query_serverless.sql +69 -0
  31. castor_extractor/warehouse/redshift/query.py +12 -1
  32. castor_extractor/warehouse/salesforce/client.py +1 -1
  33. castor_extractor/warehouse/salesforce/format.py +40 -30
  34. castor_extractor/warehouse/salesforce/format_test.py +61 -24
  35. {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/METADATA +21 -1
  36. {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/RECORD +39 -26
  37. {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/entry_points.txt +1 -0
  38. castor_extractor/visualization/powerbi/client/rest.py +0 -305
  39. castor_extractor/visualization/powerbi/client/rest_test.py +0 -290
  40. castor_extractor/visualization/powerbi/client/utils.py +0 -19
  41. castor_extractor/visualization/powerbi/client/utils_test.py +0 -24
  42. {castor_extractor-0.20.0.dist-info → castor_extractor-0.20.5.dist-info}/LICENCE +0 -0
  43. {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)