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.

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