castor-extractor 0.5.3__py3-none-any.whl → 0.5.6__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 +15 -0
- castor_extractor/commands/extract_bigquery.py +3 -1
- castor_extractor/commands/extract_looker.py +4 -1
- castor_extractor/commands/extract_metabase_api.py +3 -1
- castor_extractor/commands/extract_metabase_db.py +6 -2
- castor_extractor/commands/extract_mode.py +6 -2
- castor_extractor/commands/extract_powerbi.py +4 -1
- castor_extractor/commands/extract_snowflake.py +4 -2
- castor_extractor/commands/extract_tableau.py +4 -1
- castor_extractor/commands/file_check.py +3 -1
- castor_extractor/commands/upload.py +5 -2
- castor_extractor/file_checker/file_test.py +6 -3
- castor_extractor/file_checker/templates/generic_warehouse.py +4 -2
- castor_extractor/transformation/dbt/client/credentials.py +1 -1
- castor_extractor/types.py +2 -1
- castor_extractor/uploader/upload.py +4 -2
- castor_extractor/uploader/upload_test.py +0 -1
- castor_extractor/utils/deprecate.py +1 -1
- castor_extractor/utils/files_test.py +2 -2
- castor_extractor/utils/formatter_test.py +0 -1
- castor_extractor/utils/pager.py +4 -2
- castor_extractor/utils/pager_test.py +1 -1
- castor_extractor/utils/retry.py +1 -1
- castor_extractor/utils/safe.py +1 -1
- castor_extractor/utils/string_test.py +0 -1
- castor_extractor/utils/validation.py +4 -3
- castor_extractor/visualization/looker/api/client.py +26 -9
- castor_extractor/visualization/looker/api/client_test.py +3 -2
- castor_extractor/visualization/looker/api/constants.py +3 -1
- castor_extractor/visualization/looker/api/utils.py +3 -2
- castor_extractor/visualization/looker/assets.py +1 -0
- castor_extractor/visualization/looker/constant.py +1 -1
- castor_extractor/visualization/looker/extract.py +6 -1
- castor_extractor/visualization/metabase/client/api/client.py +2 -1
- castor_extractor/visualization/metabase/client/api/credentials.py +1 -1
- castor_extractor/visualization/metabase/client/db/client.py +4 -3
- castor_extractor/visualization/metabase/client/db/credentials.py +2 -2
- castor_extractor/visualization/metabase/client/decryption_test.py +0 -1
- castor_extractor/visualization/metabase/extract.py +4 -4
- castor_extractor/visualization/mode/client/client.py +2 -1
- castor_extractor/visualization/mode/client/client_test.py +4 -3
- castor_extractor/visualization/mode/client/credentials.py +2 -2
- castor_extractor/visualization/powerbi/client/constants.py +1 -1
- castor_extractor/visualization/powerbi/client/credentials.py +0 -1
- castor_extractor/visualization/powerbi/client/credentials_test.py +11 -3
- castor_extractor/visualization/powerbi/client/rest.py +15 -5
- castor_extractor/visualization/powerbi/client/rest_test.py +40 -13
- castor_extractor/visualization/powerbi/extract.py +4 -3
- castor_extractor/visualization/qlik/client/engine/client.py +3 -1
- castor_extractor/visualization/qlik/client/engine/json_rpc.py +4 -1
- castor_extractor/visualization/qlik/client/engine/json_rpc_test.py +0 -1
- castor_extractor/visualization/qlik/client/master.py +11 -4
- castor_extractor/visualization/qlik/client/rest_test.py +3 -2
- castor_extractor/visualization/sigma/client/client.py +7 -3
- castor_extractor/visualization/sigma/client/client_test.py +4 -2
- castor_extractor/visualization/sigma/client/credentials.py +2 -2
- castor_extractor/visualization/sigma/constants.py +1 -1
- castor_extractor/visualization/sigma/extract.py +3 -1
- castor_extractor/visualization/tableau/client/client.py +7 -5
- castor_extractor/visualization/tableau/client/client_utils.py +6 -3
- castor_extractor/visualization/tableau/client/credentials.py +6 -4
- castor_extractor/visualization/tableau/client/project.py +3 -1
- castor_extractor/visualization/tableau/client/safe_mode.py +2 -1
- castor_extractor/visualization/tableau/extract.py +7 -7
- castor_extractor/visualization/tableau/gql_fields.py +4 -4
- castor_extractor/visualization/tableau/tests/unit/graphql/paginated_object_test.py +2 -1
- castor_extractor/visualization/tableau/tests/unit/rest_api/auth_test.py +6 -3
- castor_extractor/visualization/tableau/tests/unit/rest_api/credentials_test.py +1 -1
- castor_extractor/visualization/tableau/tests/unit/rest_api/usages_test.py +2 -1
- castor_extractor/warehouse/abstract/extract.py +3 -2
- castor_extractor/warehouse/abstract/time_filter_test.py +0 -1
- castor_extractor/warehouse/bigquery/client_test.py +1 -1
- castor_extractor/warehouse/bigquery/extract.py +3 -2
- castor_extractor/warehouse/bigquery/query.py +4 -3
- castor_extractor/warehouse/postgres/extract.py +5 -3
- castor_extractor/warehouse/redshift/client_test.py +0 -1
- castor_extractor/warehouse/redshift/extract.py +5 -3
- castor_extractor/warehouse/snowflake/client.py +1 -1
- castor_extractor/warehouse/snowflake/client_test.py +1 -1
- castor_extractor/warehouse/snowflake/extract.py +5 -3
- castor_extractor/warehouse/synapse/extract.py +1 -1
- {castor_extractor-0.5.3.dist-info → castor_extractor-0.5.6.dist-info}/METADATA +2 -2
- {castor_extractor-0.5.3.dist-info → castor_extractor-0.5.6.dist-info}/RECORD +85 -85
- {castor_extractor-0.5.3.dist-info → castor_extractor-0.5.6.dist-info}/WHEEL +0 -0
- {castor_extractor-0.5.3.dist-info → castor_extractor-0.5.6.dist-info}/entry_points.txt +0 -0
|
@@ -2,7 +2,6 @@ from .decryption import decrypt
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def test_decrypt():
|
|
5
|
-
|
|
6
5
|
key = "8iY5gyHxPH0YYyBgOd2AvwT1pcHl3EGtvN5jAi9JwoA"
|
|
7
6
|
enc_message = "sIqpGK6UgzKOkThtlvBGqkb046EtB+HxcBsO3nDiKZAJcszfqqxTgSyH+SXAALznfuMSnZjdX9yzpGe77+ByYuCVlXHkMilkUe6tkFsFkBPW5CPirp0kqLdyp1yHXrv3NmXCtGZcef2fC0v89huRMSgFcm8M6Zf3JjSDEludLUo="
|
|
8
7
|
dec_message = '{"db":"zip:/app/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest"}'
|
|
@@ -25,18 +25,18 @@ def iterate_all_data(
|
|
|
25
25
|
|
|
26
26
|
yield MetabaseAsset.USER, deep_serialize(client.fetch(MetabaseAsset.USER))
|
|
27
27
|
yield MetabaseAsset.COLLECTION, deep_serialize(
|
|
28
|
-
client.fetch(MetabaseAsset.COLLECTION)
|
|
28
|
+
client.fetch(MetabaseAsset.COLLECTION),
|
|
29
29
|
)
|
|
30
30
|
yield MetabaseAsset.DATABASE, deep_serialize(
|
|
31
|
-
client.fetch(MetabaseAsset.DATABASE)
|
|
31
|
+
client.fetch(MetabaseAsset.DATABASE),
|
|
32
32
|
)
|
|
33
33
|
yield MetabaseAsset.TABLE, deep_serialize(client.fetch(MetabaseAsset.TABLE))
|
|
34
34
|
yield MetabaseAsset.CARD, deep_serialize(client.fetch(MetabaseAsset.CARD))
|
|
35
35
|
yield MetabaseAsset.DASHBOARD, deep_serialize(
|
|
36
|
-
client.fetch(MetabaseAsset.DASHBOARD)
|
|
36
|
+
client.fetch(MetabaseAsset.DASHBOARD),
|
|
37
37
|
)
|
|
38
38
|
yield MetabaseAsset.DASHBOARD_CARDS, deep_serialize(
|
|
39
|
-
client.fetch(MetabaseAsset.DASHBOARD_CARDS)
|
|
39
|
+
client.fetch(MetabaseAsset.DASHBOARD_CARDS),
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
|
|
@@ -168,7 +168,8 @@ class Client:
|
|
|
168
168
|
# why without workspace? because users can belong to several companies
|
|
169
169
|
# example: https://modeanalytics.com/api/john_doe
|
|
170
170
|
result = self._call(
|
|
171
|
-
resource_name=mb["member_username"],
|
|
171
|
+
resource_name=mb["member_username"],
|
|
172
|
+
with_workspace=False,
|
|
172
173
|
)
|
|
173
174
|
members.append(cast(Dict, result))
|
|
174
175
|
return members
|
|
@@ -9,7 +9,7 @@ _WORKSPACE = "castor"
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _dummy_client() -> Client:
|
|
12
|
-
return Client(
|
|
12
|
+
return Client( # noqa: S106
|
|
13
13
|
host=_HOST,
|
|
14
14
|
workspace=_WORKSPACE,
|
|
15
15
|
token="dummy-token",
|
|
@@ -41,10 +41,11 @@ def test__post_processing():
|
|
|
41
41
|
client = _dummy_client()
|
|
42
42
|
raw = load_file("client_test.json", __file__)
|
|
43
43
|
result = client._post_processing(
|
|
44
|
-
asset=ModeAnalyticsAsset.COLLECTION,
|
|
44
|
+
asset=ModeAnalyticsAsset.COLLECTION,
|
|
45
|
+
data=[json.loads(raw)],
|
|
45
46
|
)
|
|
46
47
|
collection = result[0]
|
|
47
48
|
assert set(collection.keys()) == set(
|
|
48
|
-
EXPORTED_FIELDS[ModeAnalyticsAsset.COLLECTION]
|
|
49
|
+
EXPORTED_FIELDS[ModeAnalyticsAsset.COLLECTION],
|
|
49
50
|
)
|
|
50
51
|
assert collection["creator"] == "john_doe"
|
|
@@ -9,8 +9,8 @@ from ....utils import from_env
|
|
|
9
9
|
class CredentialsKey(Enum):
|
|
10
10
|
HOST = "host"
|
|
11
11
|
WORKSPACE = "workspace"
|
|
12
|
-
TOKEN = "token"
|
|
13
|
-
SECRET = "secret"
|
|
12
|
+
TOKEN = "token" # noqa: S105
|
|
13
|
+
SECRET = "secret" # noqa: S105
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
CREDENTIALS_ENV: Dict[CredentialsKey, str] = {
|
|
@@ -9,19 +9,27 @@ def test_credentials():
|
|
|
9
9
|
|
|
10
10
|
# no scopes provided
|
|
11
11
|
credentials = Credentials(
|
|
12
|
-
tenant_id=tenant_id,
|
|
12
|
+
tenant_id=tenant_id,
|
|
13
|
+
client_id=client_id,
|
|
14
|
+
secret=secret,
|
|
13
15
|
)
|
|
14
16
|
assert credentials.scopes == [Urls.DEFAULT_SCOPE]
|
|
15
17
|
|
|
16
18
|
# empty scopes
|
|
17
19
|
credentials = Credentials(
|
|
18
|
-
tenant_id=tenant_id,
|
|
20
|
+
tenant_id=tenant_id,
|
|
21
|
+
client_id=client_id,
|
|
22
|
+
secret=secret,
|
|
23
|
+
scopes=[],
|
|
19
24
|
)
|
|
20
25
|
assert credentials.scopes == []
|
|
21
26
|
|
|
22
27
|
# with scopes
|
|
23
28
|
scopes = ["foo"]
|
|
24
29
|
credentials = Credentials(
|
|
25
|
-
tenant_id=tenant_id,
|
|
30
|
+
tenant_id=tenant_id,
|
|
31
|
+
client_id=client_id,
|
|
32
|
+
secret=secret,
|
|
33
|
+
scopes=scopes,
|
|
26
34
|
)
|
|
27
35
|
assert credentials.scopes == scopes
|
|
@@ -104,7 +104,11 @@ class Client:
|
|
|
104
104
|
"""
|
|
105
105
|
logger.debug(f"Calling {method} on {url}")
|
|
106
106
|
result = requests.request(
|
|
107
|
-
method,
|
|
107
|
+
method,
|
|
108
|
+
url,
|
|
109
|
+
headers=self._header(),
|
|
110
|
+
params=params,
|
|
111
|
+
data=data,
|
|
108
112
|
)
|
|
109
113
|
result.raise_for_status()
|
|
110
114
|
|
|
@@ -131,11 +135,16 @@ class Client:
|
|
|
131
135
|
processor: Optional[Callable] = None,
|
|
132
136
|
) -> Any:
|
|
133
137
|
return self._call(
|
|
134
|
-
url,
|
|
138
|
+
url,
|
|
139
|
+
POST,
|
|
140
|
+
params=params,
|
|
141
|
+
data=data,
|
|
142
|
+
processor=processor,
|
|
135
143
|
)
|
|
136
144
|
|
|
137
145
|
def _workspace_ids(
|
|
138
|
-
self,
|
|
146
|
+
self,
|
|
147
|
+
modified_since: Optional[datetime] = None,
|
|
139
148
|
) -> List[str]:
|
|
140
149
|
"""
|
|
141
150
|
Get workspaces ids from powerBI admin API.
|
|
@@ -191,7 +200,7 @@ class Client:
|
|
|
191
200
|
break
|
|
192
201
|
waiting_seconds += sleep_seconds
|
|
193
202
|
logger.info(
|
|
194
|
-
f"Waiting {sleep_seconds} sec for scan {scan_id} to be ready…"
|
|
203
|
+
f"Waiting {sleep_seconds} sec for scan {scan_id} to be ready…",
|
|
195
204
|
)
|
|
196
205
|
sleep(sleep_seconds)
|
|
197
206
|
return False
|
|
@@ -249,7 +258,8 @@ class Client:
|
|
|
249
258
|
return self._get(Urls.DASHBOARD)[Keys.VALUE]
|
|
250
259
|
|
|
251
260
|
def _metadata(
|
|
252
|
-
self,
|
|
261
|
+
self,
|
|
262
|
+
modified_since: Optional[datetime] = None,
|
|
253
263
|
) -> Iterator[List[Dict]]:
|
|
254
264
|
"""
|
|
255
265
|
Fetch metadata by workspace.
|
|
@@ -15,7 +15,9 @@ FAKE_SECRET = "MeThree"
|
|
|
15
15
|
|
|
16
16
|
def _client() -> Client:
|
|
17
17
|
creds = Credentials(
|
|
18
|
-
tenant_id=FAKE_TENANT_ID,
|
|
18
|
+
tenant_id=FAKE_TENANT_ID,
|
|
19
|
+
client_id=FAKE_CLIENT_ID,
|
|
20
|
+
secret=FAKE_SECRET,
|
|
19
21
|
)
|
|
20
22
|
return Client(creds)
|
|
21
23
|
|
|
@@ -26,7 +28,6 @@ def _raise_http_error() -> None:
|
|
|
26
28
|
|
|
27
29
|
@patch.object(msal, "ConfidentialClientApplication")
|
|
28
30
|
def test__access_token(mock_app):
|
|
29
|
-
|
|
30
31
|
# init mocks
|
|
31
32
|
valid_response = {"access_token": "mock_token"}
|
|
32
33
|
returning_valid_token = Mock(return_value=valid_response)
|
|
@@ -85,7 +86,7 @@ def test__get(mocked_access_token, mocked_request):
|
|
|
85
86
|
def test__workspace_ids(_, mocked_request):
|
|
86
87
|
client = _client()
|
|
87
88
|
mocked_request.return_value = Mock(
|
|
88
|
-
json=lambda: [{"id": 1000}, {"id": 1001}, {"id": 1003}]
|
|
89
|
+
json=lambda: [{"id": 1000}, {"id": 1001}, {"id": 1003}],
|
|
89
90
|
)
|
|
90
91
|
ids = client._workspace_ids()
|
|
91
92
|
assert ids == [1000, 1001, 1003]
|
|
@@ -103,7 +104,11 @@ def test__workspace_ids(_, mocked_request):
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
mocked_request.assert_called_with(
|
|
106
|
-
GET,
|
|
107
|
+
GET,
|
|
108
|
+
Urls.WORKSPACE_IDS,
|
|
109
|
+
data=None,
|
|
110
|
+
headers=ANY,
|
|
111
|
+
params=params,
|
|
107
112
|
)
|
|
108
113
|
|
|
109
114
|
|
|
@@ -116,7 +121,11 @@ def test__post_default(_, mocked_request):
|
|
|
116
121
|
data = {"bonjour": "hello"}
|
|
117
122
|
client._post(url, params=params, data=data)
|
|
118
123
|
mocked_request.assert_called_with(
|
|
119
|
-
POST,
|
|
124
|
+
POST,
|
|
125
|
+
url,
|
|
126
|
+
headers=ANY,
|
|
127
|
+
params=QueryParams.METADATA_SCAN,
|
|
128
|
+
data=data,
|
|
120
129
|
)
|
|
121
130
|
|
|
122
131
|
|
|
@@ -129,7 +138,10 @@ def test__post_with_processor(_, mocked_request):
|
|
|
129
138
|
data = {"bonjour": "hello"}
|
|
130
139
|
mocked_request.return_value = Mock(json=lambda: {"id": 1000})
|
|
131
140
|
result = client._post(
|
|
132
|
-
url,
|
|
141
|
+
url,
|
|
142
|
+
params=params,
|
|
143
|
+
data=data,
|
|
144
|
+
processor=lambda x: x.json()["id"],
|
|
133
145
|
)
|
|
134
146
|
assert result == 1000
|
|
135
147
|
|
|
@@ -139,11 +151,15 @@ def test__post_with_processor(_, mocked_request):
|
|
|
139
151
|
def test__datasets(_, mocked_request):
|
|
140
152
|
client = _client()
|
|
141
153
|
mocked_request.return_value = Mock(
|
|
142
|
-
json=lambda: {"value": [{"id": 1, "type": "dataset"}]}
|
|
154
|
+
json=lambda: {"value": [{"id": 1, "type": "dataset"}]},
|
|
143
155
|
)
|
|
144
156
|
datasets = client._datasets()
|
|
145
157
|
mocked_request.assert_called_with(
|
|
146
|
-
GET,
|
|
158
|
+
GET,
|
|
159
|
+
Urls.DATASETS,
|
|
160
|
+
data=None,
|
|
161
|
+
headers=ANY,
|
|
162
|
+
params=None,
|
|
147
163
|
)
|
|
148
164
|
assert datasets == [{"id": 1, "type": "dataset"}]
|
|
149
165
|
|
|
@@ -153,11 +169,15 @@ def test__datasets(_, mocked_request):
|
|
|
153
169
|
def test__reports(_, mocked_request):
|
|
154
170
|
client = _client()
|
|
155
171
|
mocked_request.return_value = Mock(
|
|
156
|
-
json=lambda: {"value": [{"id": 1, "type": "report"}]}
|
|
172
|
+
json=lambda: {"value": [{"id": 1, "type": "report"}]},
|
|
157
173
|
)
|
|
158
174
|
reports = client._reports()
|
|
159
175
|
mocked_request.assert_called_with(
|
|
160
|
-
GET,
|
|
176
|
+
GET,
|
|
177
|
+
Urls.REPORTS,
|
|
178
|
+
data=None,
|
|
179
|
+
headers=ANY,
|
|
180
|
+
params=None,
|
|
161
181
|
)
|
|
162
182
|
assert reports == [{"id": 1, "type": "report"}]
|
|
163
183
|
|
|
@@ -167,11 +187,15 @@ def test__reports(_, mocked_request):
|
|
|
167
187
|
def test__dashboards(_, mocked_request):
|
|
168
188
|
client = _client()
|
|
169
189
|
mocked_request.return_value = Mock(
|
|
170
|
-
json=lambda: {"value": [{"id": 1, "type": "dashboard"}]}
|
|
190
|
+
json=lambda: {"value": [{"id": 1, "type": "dashboard"}]},
|
|
171
191
|
)
|
|
172
192
|
dashboards = client._dashboards()
|
|
173
193
|
mocked_request.assert_called_with(
|
|
174
|
-
GET,
|
|
194
|
+
GET,
|
|
195
|
+
Urls.DASHBOARD,
|
|
196
|
+
data=None,
|
|
197
|
+
headers=ANY,
|
|
198
|
+
params=None,
|
|
175
199
|
)
|
|
176
200
|
assert dashboards == [{"id": 1, "type": "dashboard"}]
|
|
177
201
|
|
|
@@ -181,7 +205,10 @@ def test__dashboards(_, mocked_request):
|
|
|
181
205
|
@patch.object(Client, "_wait_for_scan_result")
|
|
182
206
|
@patch.object(Client, "_get_scan")
|
|
183
207
|
def test__metadata(
|
|
184
|
-
mocked_get_scan,
|
|
208
|
+
mocked_get_scan,
|
|
209
|
+
mocked_wait,
|
|
210
|
+
mocked_create_scan,
|
|
211
|
+
mocked_workspace_ids,
|
|
185
212
|
):
|
|
186
213
|
mocked_workspace_ids.return_value = list(range(200))
|
|
187
214
|
mocked_create_scan.return_value = 314
|
|
@@ -16,9 +16,7 @@ from .client import Client, Credentials
|
|
|
16
16
|
def iterate_all_data(
|
|
17
17
|
client: Client,
|
|
18
18
|
) -> Iterable[Tuple[PowerBiAsset, Union[List, dict]]]:
|
|
19
|
-
|
|
20
19
|
for asset in PowerBiAsset:
|
|
21
|
-
|
|
22
20
|
if asset in METADATA_ASSETS:
|
|
23
21
|
continue
|
|
24
22
|
|
|
@@ -39,7 +37,10 @@ def extract_all(
|
|
|
39
37
|
"""
|
|
40
38
|
_output_directory = output_directory or from_env(OUTPUT_DIR)
|
|
41
39
|
creds = Credentials(
|
|
42
|
-
tenant_id=tenant_id,
|
|
40
|
+
tenant_id=tenant_id,
|
|
41
|
+
client_id=client_id,
|
|
42
|
+
secret=secret,
|
|
43
|
+
scopes=scopes,
|
|
43
44
|
)
|
|
44
45
|
client = Client(creds)
|
|
45
46
|
ts = current_timestamp()
|
|
@@ -69,7 +69,9 @@ class EngineApiClient:
|
|
|
69
69
|
return _list_measures(client, app_id_)
|
|
70
70
|
|
|
71
71
|
with open_websocket(
|
|
72
|
-
app_id=app_id,
|
|
72
|
+
app_id=app_id,
|
|
73
|
+
server_url=self.server_url,
|
|
74
|
+
api_key=self.api_key,
|
|
73
75
|
) as websocket:
|
|
74
76
|
json_rpc_client = JsonRpcClient(websocket=websocket)
|
|
75
77
|
return _call(json_rpc_client, app_id)
|
|
@@ -25,7 +25,10 @@ class JsonRpcClient:
|
|
|
25
25
|
self.call_id += 1
|
|
26
26
|
|
|
27
27
|
def _format_message(
|
|
28
|
-
self,
|
|
28
|
+
self,
|
|
29
|
+
method: JsonRpcMethod,
|
|
30
|
+
handle: int,
|
|
31
|
+
params: Optional[list] = None,
|
|
29
32
|
) -> dict:
|
|
30
33
|
params = params or list()
|
|
31
34
|
message = {
|
|
@@ -21,7 +21,8 @@ class MissingAppsScopeError(Exception):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _include_app_external_id(
|
|
24
|
-
data: ListedData,
|
|
24
|
+
data: ListedData,
|
|
25
|
+
app_external_id: str,
|
|
25
26
|
) -> ListedData:
|
|
26
27
|
_data = data.copy()
|
|
27
28
|
for element in _data:
|
|
@@ -30,7 +31,9 @@ def _include_app_external_id(
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _fetch_children_on_apps(
|
|
33
|
-
apps: ListedData,
|
|
34
|
+
apps: ListedData,
|
|
35
|
+
fetch_callback: Callable,
|
|
36
|
+
display_progress: bool,
|
|
34
37
|
) -> ListedData:
|
|
35
38
|
all_apps_data: ListedData = list()
|
|
36
39
|
apps_iterator = apps if not display_progress else tqdm(apps)
|
|
@@ -66,7 +69,8 @@ class QlikMasterClient:
|
|
|
66
69
|
)
|
|
67
70
|
|
|
68
71
|
self.engine_api_client = EngineApiClient(
|
|
69
|
-
server_url=self._server_url,
|
|
72
|
+
server_url=self._server_url,
|
|
73
|
+
api_key=self._api_key,
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
def _fetch_lineage(self, apps: ListedData) -> ListedData:
|
|
@@ -78,7 +82,10 @@ class QlikMasterClient:
|
|
|
78
82
|
return _fetch_children_on_apps(apps, callback, self.display_progress)
|
|
79
83
|
|
|
80
84
|
def fetch(
|
|
81
|
-
self,
|
|
85
|
+
self,
|
|
86
|
+
asset: QlikAsset,
|
|
87
|
+
*,
|
|
88
|
+
apps: Optional[ListedData] = None,
|
|
82
89
|
) -> ListedData:
|
|
83
90
|
"""
|
|
84
91
|
Given a QlikAsset, returns the corresponding data using the
|
|
@@ -5,7 +5,9 @@ from .rest import RestApiClient
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _check_called_once(
|
|
8
|
-
client: RestApiClient,
|
|
8
|
+
client: RestApiClient,
|
|
9
|
+
first_page_url: str,
|
|
10
|
+
return_value: Optional[dict],
|
|
9
11
|
):
|
|
10
12
|
with patch.object(RestApiClient, "_call") as mock_call:
|
|
11
13
|
mock_call.return_value = return_value
|
|
@@ -18,7 +20,6 @@ def _check_called_once(
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def test_rest_api_client_pager():
|
|
21
|
-
|
|
22
23
|
dummy_server_url = "https://clic.kom"
|
|
23
24
|
dummy_api_key = "i-am-the-key-dont-let-others-know-about"
|
|
24
25
|
|
|
@@ -25,7 +25,8 @@ class SigmaClient:
|
|
|
25
25
|
self.headers: Optional[Dict[str, str]] = None
|
|
26
26
|
|
|
27
27
|
def _get_token(self, token_api_path: str) -> Dict[str, str]:
|
|
28
|
-
|
|
28
|
+
# TODO: ADD TIMEOUT
|
|
29
|
+
token_response = requests.post( # noqa: S113
|
|
29
30
|
token_api_path,
|
|
30
31
|
data={
|
|
31
32
|
CredentialsKey.GRANT_TYPE.value: "client_credentials",
|
|
@@ -52,7 +53,8 @@ class SigmaClient:
|
|
|
52
53
|
|
|
53
54
|
def _get(self, endpoint_url: str) -> dict:
|
|
54
55
|
url = urljoin(self.host, endpoint_url)
|
|
55
|
-
|
|
56
|
+
# TODO: add timeout
|
|
57
|
+
result = requests.get(url, headers=self._get_headers()) # noqa: S113
|
|
56
58
|
try:
|
|
57
59
|
return result.json()
|
|
58
60
|
except:
|
|
@@ -77,7 +79,9 @@ class SigmaClient:
|
|
|
77
79
|
yield from self._get_with_pagination(endpoint)
|
|
78
80
|
|
|
79
81
|
def _per_page_get_elements(
|
|
80
|
-
self,
|
|
82
|
+
self,
|
|
83
|
+
workbook_id: str,
|
|
84
|
+
page_id: str,
|
|
81
85
|
) -> Iterator[dict]:
|
|
82
86
|
endpoint = EndpointFactory.elements(workbook_id, page_id)
|
|
83
87
|
yield from self._get_with_pagination(endpoint)
|
|
@@ -2,8 +2,10 @@ from unittest.mock import Mock, patch
|
|
|
2
2
|
|
|
3
3
|
from .client import SigmaClient, SigmaCredentials
|
|
4
4
|
|
|
5
|
-
FAKE_CREDENTIALS = SigmaCredentials(
|
|
6
|
-
host="IamFake",
|
|
5
|
+
FAKE_CREDENTIALS = SigmaCredentials( # noqa: S106
|
|
6
|
+
host="IamFake",
|
|
7
|
+
client_id="MeTwo",
|
|
8
|
+
api_token="MeThree",
|
|
7
9
|
)
|
|
8
10
|
|
|
9
11
|
|
|
@@ -5,11 +5,11 @@ from enum import Enum
|
|
|
5
5
|
class CredentialsKey(Enum):
|
|
6
6
|
"""Value enum object for the credentials"""
|
|
7
7
|
|
|
8
|
-
CLIENT_SECRET = "client_secret"
|
|
8
|
+
CLIENT_SECRET = "client_secret" # noqa: S105
|
|
9
9
|
CLIENT_ID = "client_id"
|
|
10
10
|
HOST = "host"
|
|
11
11
|
GRANT_TYPE = "grant_type"
|
|
12
|
-
API_TOKEN = "api_token"
|
|
12
|
+
API_TOKEN = "api_token" # noqa: S105
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
CLIENT_ALLOWED_KEYS = (
|
|
@@ -68,7 +68,9 @@ def extract_all(
|
|
|
68
68
|
_api_token = api_token or from_env(API_TOKEN)
|
|
69
69
|
|
|
70
70
|
credentials = SigmaCredentials(
|
|
71
|
-
host=_host,
|
|
71
|
+
host=_host,
|
|
72
|
+
client_id=_client_id,
|
|
73
|
+
api_token=_api_token,
|
|
72
74
|
)
|
|
73
75
|
client = SigmaClient(credentials=credentials)
|
|
74
76
|
|
|
@@ -30,7 +30,9 @@ class ApiClient:
|
|
|
30
30
|
user=get_value(CredentialsKey.TABLEAU_USER, kwargs, True),
|
|
31
31
|
password=get_value(CredentialsKey.TABLEAU_PASSWORD, kwargs, True),
|
|
32
32
|
token_name=get_value(
|
|
33
|
-
CredentialsKey.TABLEAU_TOKEN_NAME,
|
|
33
|
+
CredentialsKey.TABLEAU_TOKEN_NAME,
|
|
34
|
+
kwargs,
|
|
35
|
+
True,
|
|
34
36
|
),
|
|
35
37
|
token=get_value(CredentialsKey.TABLEAU_TOKEN, kwargs, True),
|
|
36
38
|
server_url=get_value(CredentialsKey.TABLEAU_SERVER_URL, kwargs),
|
|
@@ -54,7 +56,7 @@ class ApiClient:
|
|
|
54
56
|
self._credentials.user,
|
|
55
57
|
self._credentials.password,
|
|
56
58
|
site_id=self._credentials.site_id,
|
|
57
|
-
)
|
|
59
|
+
),
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
def _pat_login(self) -> None:
|
|
@@ -64,7 +66,7 @@ class ApiClient:
|
|
|
64
66
|
self._credentials.token_name,
|
|
65
67
|
self._credentials.token,
|
|
66
68
|
site_id=self._credentials.site_id,
|
|
67
|
-
)
|
|
69
|
+
),
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
def login(self) -> None:
|
|
@@ -80,7 +82,7 @@ class ApiClient:
|
|
|
80
82
|
|
|
81
83
|
raise ValueError(
|
|
82
84
|
"""Wrong authentication: you should provide either user and password
|
|
83
|
-
or personal access token"""
|
|
85
|
+
or personal access token""",
|
|
84
86
|
)
|
|
85
87
|
|
|
86
88
|
def base_url(self) -> str:
|
|
@@ -119,7 +121,7 @@ class ApiClient:
|
|
|
119
121
|
[
|
|
120
122
|
extract_asset(project, TableauAsset.PROJECT)
|
|
121
123
|
for project in TSC.Pager(self._server.projects)
|
|
122
|
-
]
|
|
124
|
+
],
|
|
123
125
|
)
|
|
124
126
|
|
|
125
127
|
def _fetch_workbooks_to_datasource(self) -> SerializedAsset:
|
|
@@ -23,9 +23,10 @@ RESOURCE_TEMPLATE = "{resource}Connection"
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def get_paginated_objects(
|
|
26
|
-
server,
|
|
26
|
+
server,
|
|
27
|
+
asset: TableauAsset,
|
|
28
|
+
page_size: int,
|
|
27
29
|
) -> SerializedAsset:
|
|
28
|
-
|
|
29
30
|
assets: SerializedAsset = []
|
|
30
31
|
for query in QUERY_FIELDS[asset]:
|
|
31
32
|
fields = query["fields"].value
|
|
@@ -43,7 +44,9 @@ def get_paginated_objects(
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def query_scroll(
|
|
46
|
-
server,
|
|
47
|
+
server,
|
|
48
|
+
query: str,
|
|
49
|
+
resource: str,
|
|
47
50
|
) -> Iterator[SerializedAsset]:
|
|
48
51
|
"""build a tableau query iterator handling pagination and cursor"""
|
|
49
52
|
|
|
@@ -13,9 +13,9 @@ class CredentialsKey(Enum):
|
|
|
13
13
|
"""Value enum object for the credentials"""
|
|
14
14
|
|
|
15
15
|
TABLEAU_USER = "user"
|
|
16
|
-
TABLEAU_PASSWORD = "password"
|
|
17
|
-
TABLEAU_TOKEN_NAME = "token_name"
|
|
18
|
-
TABLEAU_TOKEN = "token"
|
|
16
|
+
TABLEAU_PASSWORD = "password" # noqa: S105
|
|
17
|
+
TABLEAU_TOKEN_NAME = "token_name" # noqa: S105
|
|
18
|
+
TABLEAU_TOKEN = "token" # noqa: S105
|
|
19
19
|
TABLEAU_SITE_ID = "site_id"
|
|
20
20
|
TABLEAU_SERVER_URL = "server_url"
|
|
21
21
|
|
|
@@ -31,7 +31,9 @@ CREDENTIALS_ENV: Dict[CredentialsKey, str] = {
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def get_value(
|
|
34
|
-
key: CredentialsKey,
|
|
34
|
+
key: CredentialsKey,
|
|
35
|
+
kwargs: dict,
|
|
36
|
+
optional: bool = False,
|
|
35
37
|
) -> Optional[str]:
|
|
36
38
|
"""
|
|
37
39
|
Returns the value of the given key:
|
|
@@ -4,7 +4,9 @@ from ....utils import SerializedAsset
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def _folder_path(
|
|
7
|
-
projects: SerializedAsset,
|
|
7
|
+
projects: SerializedAsset,
|
|
8
|
+
project: dict,
|
|
9
|
+
root: Optional[str] = "",
|
|
8
10
|
) -> str:
|
|
9
11
|
"""Recursive function to compute folder path with list of projects"""
|
|
10
12
|
path = "/" + str(project["name"]) + (root or "")
|
|
@@ -17,7 +17,8 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
def _paginated_option(page_number: int) -> TSC.RequestOptions:
|
|
18
18
|
"""Set up the Paginated option for TSC.RequestOptions"""
|
|
19
19
|
return TSC.RequestOptions(
|
|
20
|
-
pagesize=SAFE_MODE_PAGE_SIZE,
|
|
20
|
+
pagesize=SAFE_MODE_PAGE_SIZE,
|
|
21
|
+
pagenumber=page_number,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|