looker-sdk 24.18.0__py3-none-any.whl → 24.20.0__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.
- looker_sdk/rtl/__init__.py +22 -0
- looker_sdk/rtl/api_methods.py +247 -0
- looker_sdk/rtl/api_settings.py +194 -0
- looker_sdk/rtl/auth_session.py +353 -0
- looker_sdk/rtl/auth_token.py +101 -0
- looker_sdk/rtl/constants.py +31 -0
- looker_sdk/rtl/hooks.py +86 -0
- looker_sdk/rtl/model.py +230 -0
- looker_sdk/rtl/requests_transport.py +110 -0
- looker_sdk/rtl/serialize.py +120 -0
- looker_sdk/rtl/transport.py +137 -0
- looker_sdk/sdk/__init__.py +0 -0
- looker_sdk/sdk/api40/__init__.py +1 -0
- looker_sdk/sdk/api40/methods.py +13356 -0
- looker_sdk/sdk/api40/models.py +15616 -0
- looker_sdk/sdk/constants.py +24 -0
- looker_sdk/version.py +1 -1
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/METADATA +1 -1
- looker_sdk-24.20.0.dist-info/RECORD +36 -0
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/top_level.txt +1 -0
- tests/integration/__init__.py +2 -0
- tests/integration/test_methods.py +681 -0
- tests/integration/test_netrc.py +55 -0
- tests/rtl/__init__.py +2 -0
- tests/rtl/test_api_methods.py +216 -0
- tests/rtl/test_api_settings.py +252 -0
- tests/rtl/test_auth_session.py +284 -0
- tests/rtl/test_auth_token.py +70 -0
- tests/rtl/test_requests_transport.py +171 -0
- tests/rtl/test_serialize.py +770 -0
- tests/rtl/test_transport.py +34 -0
- looker_sdk-24.18.0.dist-info/RECORD +0 -9
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/LICENSE.txt +0 -0
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from operator import itemgetter
|
|
6
|
+
from typing import Any, cast, Dict, List, Optional, Union, Sequence
|
|
7
|
+
|
|
8
|
+
import pytest # type: ignore
|
|
9
|
+
from PIL import Image # type: ignore
|
|
10
|
+
|
|
11
|
+
from looker_sdk.sdk.api40 import methods as mtds
|
|
12
|
+
from looker_sdk.sdk.api40 import models as ml
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="module")
|
|
16
|
+
def sdk(sdk40) -> mtds.Looker40SDK:
|
|
17
|
+
return sdk40
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
TEST_FIRST_NAME = "Rudolphontronix"
|
|
21
|
+
TEST_LAST_NAME = "Doe"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_crud_user(sdk: mtds.Looker40SDK):
|
|
25
|
+
"""Test creating, retrieving, updating and deleting a user."""
|
|
26
|
+
|
|
27
|
+
# Create user
|
|
28
|
+
user = sdk.create_user(
|
|
29
|
+
ml.WriteUser(
|
|
30
|
+
first_name=TEST_FIRST_NAME,
|
|
31
|
+
last_name=TEST_LAST_NAME,
|
|
32
|
+
is_disabled=False,
|
|
33
|
+
locale="fr",
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
assert isinstance(user, ml.User)
|
|
37
|
+
assert isinstance(user.id, str)
|
|
38
|
+
assert user.first_name == TEST_FIRST_NAME
|
|
39
|
+
assert user.last_name == TEST_LAST_NAME
|
|
40
|
+
assert not user.is_disabled
|
|
41
|
+
assert user.locale == "fr"
|
|
42
|
+
|
|
43
|
+
# sudo checks
|
|
44
|
+
user_id = user.id
|
|
45
|
+
sudo_auth = sdk.login_user(user_id)
|
|
46
|
+
assert isinstance(sudo_auth.access_token, str)
|
|
47
|
+
assert sudo_auth.access_token != ""
|
|
48
|
+
sdk.auth.login_user(user_id)
|
|
49
|
+
user = sdk.me()
|
|
50
|
+
assert user.first_name == TEST_FIRST_NAME
|
|
51
|
+
assert user.last_name == TEST_LAST_NAME
|
|
52
|
+
sdk.auth.logout()
|
|
53
|
+
user = sdk.me()
|
|
54
|
+
assert user.first_name != TEST_FIRST_NAME
|
|
55
|
+
assert user.last_name != TEST_LAST_NAME
|
|
56
|
+
|
|
57
|
+
# Update user and check fields we didn't intend to change didn't change
|
|
58
|
+
update_user = ml.WriteUser(is_disabled=True, locale="uk")
|
|
59
|
+
sdk.update_user(user_id, update_user)
|
|
60
|
+
user = sdk.user(user_id)
|
|
61
|
+
assert user.first_name == TEST_FIRST_NAME
|
|
62
|
+
assert user.last_name == TEST_LAST_NAME
|
|
63
|
+
assert user.locale == "uk"
|
|
64
|
+
assert user.is_disabled
|
|
65
|
+
|
|
66
|
+
# Update user and check fields we intended to wipe out are now None
|
|
67
|
+
# first way to specify nulling out a field
|
|
68
|
+
update_user = ml.WriteUser(first_name=ml.EXPLICIT_NULL)
|
|
69
|
+
# second way
|
|
70
|
+
update_user.last_name = ml.EXPLICIT_NULL
|
|
71
|
+
sdk.update_user(user_id, update_user)
|
|
72
|
+
user = sdk.user(user_id)
|
|
73
|
+
assert user.first_name == ""
|
|
74
|
+
assert user.last_name == ""
|
|
75
|
+
|
|
76
|
+
# Try adding email creds
|
|
77
|
+
sdk.create_user_credentials_email(
|
|
78
|
+
user_id, ml.WriteCredentialsEmail(email="john.doe@looker.com")
|
|
79
|
+
)
|
|
80
|
+
user = sdk.user(user_id)
|
|
81
|
+
assert isinstance(user.credentials_email, ml.CredentialsEmail)
|
|
82
|
+
assert user.credentials_email.email == "john.doe@looker.com"
|
|
83
|
+
|
|
84
|
+
# Delete user
|
|
85
|
+
resp = sdk.delete_user(user_id)
|
|
86
|
+
assert resp == ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_crud_user_dict(sdk): # no typing
|
|
90
|
+
"""Test creating, retrieving, updating and deleting a user."""
|
|
91
|
+
|
|
92
|
+
# Create user
|
|
93
|
+
new_user = sdk.create_user(
|
|
94
|
+
dict(
|
|
95
|
+
first_name=TEST_FIRST_NAME,
|
|
96
|
+
last_name=TEST_LAST_NAME,
|
|
97
|
+
is_disabled=False,
|
|
98
|
+
locale="fr",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
assert new_user["first_name"] == TEST_FIRST_NAME
|
|
102
|
+
assert new_user["last_name"] == TEST_LAST_NAME
|
|
103
|
+
assert not new_user["is_disabled"]
|
|
104
|
+
assert new_user["locale"] == "fr"
|
|
105
|
+
|
|
106
|
+
# sudo checks
|
|
107
|
+
user_id = new_user["id"]
|
|
108
|
+
sdk.auth.login_user(user_id)
|
|
109
|
+
sudo_user = sdk.me()
|
|
110
|
+
assert sudo_user["first_name"] == TEST_FIRST_NAME
|
|
111
|
+
assert sudo_user["last_name"] == TEST_LAST_NAME
|
|
112
|
+
sdk.auth.logout()
|
|
113
|
+
me_user = sdk.me()
|
|
114
|
+
assert me_user["first_name"] != TEST_FIRST_NAME
|
|
115
|
+
assert me_user["last_name"] != TEST_LAST_NAME
|
|
116
|
+
|
|
117
|
+
# Update user and check fields we didn't intend to change didn't change
|
|
118
|
+
new_user["is_disabled"] = True
|
|
119
|
+
new_user["locale"] = "uk"
|
|
120
|
+
# sdk.update_user(user_id, update_user)
|
|
121
|
+
sdk.update_user(user_id, new_user)
|
|
122
|
+
updated_user = sdk.user(user_id)
|
|
123
|
+
assert updated_user["first_name"] == TEST_FIRST_NAME
|
|
124
|
+
assert updated_user["last_name"] == TEST_LAST_NAME
|
|
125
|
+
assert updated_user["locale"] == "uk"
|
|
126
|
+
assert updated_user["is_disabled"]
|
|
127
|
+
|
|
128
|
+
update_user = dict(first_name=None)
|
|
129
|
+
update_user["last_name"] = None
|
|
130
|
+
sdk.update_user(user_id, update_user)
|
|
131
|
+
user = sdk.user(user_id)
|
|
132
|
+
assert user["first_name"] == ""
|
|
133
|
+
assert user["last_name"] == ""
|
|
134
|
+
|
|
135
|
+
# Try adding email creds
|
|
136
|
+
sdk.create_user_credentials_email(user_id, dict(email="john.doe@looker.com"))
|
|
137
|
+
user = sdk.user(user_id)
|
|
138
|
+
assert user["credentials_email"]["email"] == "john.doe@looker.com"
|
|
139
|
+
|
|
140
|
+
# Delete user
|
|
141
|
+
resp = sdk.delete_user(user_id)
|
|
142
|
+
assert resp == ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_me_returns_correct_result(sdk: mtds.Looker40SDK):
|
|
146
|
+
"""me() should return the current authenticated user"""
|
|
147
|
+
me = sdk.me()
|
|
148
|
+
assert isinstance(me, ml.User)
|
|
149
|
+
assert isinstance(me.credentials_api3, list)
|
|
150
|
+
assert len(me.credentials_api3) > 0
|
|
151
|
+
assert isinstance(me.credentials_api3[0], ml.CredentialsApi3)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_me_field_filters(sdk: mtds.Looker40SDK):
|
|
155
|
+
"""me() should return only the requested fields."""
|
|
156
|
+
me = sdk.me("id, first_name, last_name")
|
|
157
|
+
assert isinstance(me, ml.User)
|
|
158
|
+
assert isinstance(me.id, str)
|
|
159
|
+
assert isinstance(me.first_name, str)
|
|
160
|
+
assert me.first_name != ""
|
|
161
|
+
assert isinstance(me.last_name, str)
|
|
162
|
+
assert me.last_name != ""
|
|
163
|
+
assert not me.display_name
|
|
164
|
+
assert not me.email
|
|
165
|
+
assert not me.personal_folder_id
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.usefixtures("test_users")
|
|
169
|
+
def test_bad_user_search_returns_no_results(sdk: mtds.Looker40SDK):
|
|
170
|
+
"""search_users() should return an empty list when no match is found."""
|
|
171
|
+
resp = sdk.search_users(first_name="Bad", last_name="News")
|
|
172
|
+
assert isinstance(resp, list)
|
|
173
|
+
assert len(resp) == 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.usefixtures("test_users")
|
|
177
|
+
def test_search_users_matches_pattern(
|
|
178
|
+
sdk: mtds.Looker40SDK, users: List[Dict[str, str]], email_domain: str
|
|
179
|
+
):
|
|
180
|
+
"""search_users should return a list of all matches."""
|
|
181
|
+
user = users[0]
|
|
182
|
+
|
|
183
|
+
# Search by full email
|
|
184
|
+
search_email = f'{user["first_name"]}.{user["last_name"]}{email_domain}'
|
|
185
|
+
search_results = sdk.search_users_names(pattern=search_email)
|
|
186
|
+
assert len(search_results) == 1
|
|
187
|
+
assert search_results[0].first_name == user["first_name"]
|
|
188
|
+
assert search_results[0].last_name == user["last_name"]
|
|
189
|
+
assert search_results[0].email == search_email
|
|
190
|
+
|
|
191
|
+
# Search by first name
|
|
192
|
+
search_results = sdk.search_users_names(pattern=user["first_name"])
|
|
193
|
+
assert len(search_results) > 0
|
|
194
|
+
assert search_results[0].first_name == user["first_name"]
|
|
195
|
+
|
|
196
|
+
# First name with spaces
|
|
197
|
+
u = sdk.create_user(ml.WriteUser(first_name="John Allen", last_name="Smith"))
|
|
198
|
+
if u.id:
|
|
199
|
+
search_results = sdk.search_users_names(pattern="John Allen")
|
|
200
|
+
assert len(search_results) == 1
|
|
201
|
+
assert search_results[0].first_name == "John Allen"
|
|
202
|
+
assert search_results[0].last_name == "Smith"
|
|
203
|
+
|
|
204
|
+
# Delete user
|
|
205
|
+
resp = sdk.delete_user(u.id)
|
|
206
|
+
assert resp == ""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_csv_user_id_list(sdk: mtds.Looker40SDK):
|
|
210
|
+
"""all_users() should accept a delimited array of ids."""
|
|
211
|
+
users = sdk.all_users()
|
|
212
|
+
assert len(users) > 1
|
|
213
|
+
ids = [user.id for user in users]
|
|
214
|
+
all_users = sdk.all_users(ids=ml.DelimSequence(cast(Sequence[int], ids)))
|
|
215
|
+
assert len(all_users) == len(users)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_enum(sdk: mtds.Looker40SDK):
|
|
219
|
+
# TODO: there is currently no example in the Looker API of a "bare"
|
|
220
|
+
# ForwardRef property on a model that is returned by the API. We
|
|
221
|
+
# have unittests deserializing into "bare" ForwardRef properties,
|
|
222
|
+
# that will have to do for now.
|
|
223
|
+
query = ml.WriteQuery(
|
|
224
|
+
model="system__activity",
|
|
225
|
+
view="dashboard",
|
|
226
|
+
fields=["dashboard.id", "dashboard.title", "dashboard.count"],
|
|
227
|
+
)
|
|
228
|
+
query_id = sdk.create_query(query).id
|
|
229
|
+
assert query_id
|
|
230
|
+
task = ml.WriteCreateQueryTask(
|
|
231
|
+
query_id=query_id, source="test", result_format=ml.ResultFormat.csv
|
|
232
|
+
)
|
|
233
|
+
created = sdk.create_query_task(task)
|
|
234
|
+
# created.result_format is type str, not ResultFormat.csv
|
|
235
|
+
assert ml.ResultFormat.csv.value == created.result_format
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.usefixtures("test_users")
|
|
239
|
+
def test_it_matches_email_domain_and_returns_sorted(
|
|
240
|
+
sdk: mtds.Looker40SDK, email_domain: str, users: List[Dict[str, str]]
|
|
241
|
+
):
|
|
242
|
+
"""search_users_names() should search users matching a given pattern and return
|
|
243
|
+
sorted results if sort fields are specified.
|
|
244
|
+
"""
|
|
245
|
+
search_results = sdk.search_users_names(
|
|
246
|
+
pattern=f"%{email_domain}", sorts="last_name, first_name"
|
|
247
|
+
)
|
|
248
|
+
assert len(search_results) == len(users)
|
|
249
|
+
sorted_test_data: List[Dict[str, str]] = sorted(
|
|
250
|
+
users, key=itemgetter("last_name", "first_name")
|
|
251
|
+
)
|
|
252
|
+
for actual, expected in zip(search_results, sorted_test_data):
|
|
253
|
+
assert actual.first_name == expected["first_name"]
|
|
254
|
+
assert actual.last_name == expected["last_name"]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.usefixtures("test_users")
|
|
258
|
+
def test_delim_sequence(
|
|
259
|
+
sdk: mtds.Looker40SDK, email_domain: str, users: List[Dict[str, str]]
|
|
260
|
+
):
|
|
261
|
+
search_results = sdk.search_users_names(pattern=f"%{email_domain}")
|
|
262
|
+
assert len(search_results) == len(users)
|
|
263
|
+
delim_ids = ml.DelimSequence([cast(int, u.id) for u in search_results])
|
|
264
|
+
all_users = sdk.all_users(ids=delim_ids)
|
|
265
|
+
assert len(all_users) == len(users)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_it_retrieves_session(sdk: mtds.Looker40SDK):
|
|
269
|
+
"""session() should return the current session."""
|
|
270
|
+
current_session = sdk.session()
|
|
271
|
+
assert current_session.workspace_id == "production"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_it_updates_session(sdk: mtds.Looker40SDK):
|
|
275
|
+
"""update_session() should allow us to change the current workspace."""
|
|
276
|
+
# Switch workspace to dev mode
|
|
277
|
+
sdk.update_session(ml.WriteApiSession(workspace_id="dev"))
|
|
278
|
+
current_session = sdk.session()
|
|
279
|
+
|
|
280
|
+
assert isinstance(current_session, ml.ApiSession)
|
|
281
|
+
assert current_session.workspace_id == "dev"
|
|
282
|
+
|
|
283
|
+
# Switch workspace back to production
|
|
284
|
+
current_session = sdk.update_session(ml.WriteApiSession(workspace_id="production"))
|
|
285
|
+
|
|
286
|
+
assert isinstance(current_session, ml.ApiSession)
|
|
287
|
+
assert current_session.workspace_id == "production"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
TQueries = List[Dict[str, Union[str, List[str], Dict[str, str]]]]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_it_creates_and_runs_query(
|
|
294
|
+
sdk: mtds.Looker40SDK, queries_system_activity: TQueries
|
|
295
|
+
):
|
|
296
|
+
"""create_query() creates a query and run_query() returns its result."""
|
|
297
|
+
for q in queries_system_activity:
|
|
298
|
+
limit = cast(str, q["limit"]) or "10"
|
|
299
|
+
request = create_query_request(q, limit)
|
|
300
|
+
query = sdk.create_query(request)
|
|
301
|
+
assert isinstance(query, ml.Query)
|
|
302
|
+
assert query.id
|
|
303
|
+
assert isinstance(query.id, str)
|
|
304
|
+
assert query.id != '0'
|
|
305
|
+
|
|
306
|
+
sql = sdk.run_query(query.id, "sql")
|
|
307
|
+
assert "SELECT" in sql
|
|
308
|
+
|
|
309
|
+
json_ = sdk.run_query(query.id, "json")
|
|
310
|
+
assert isinstance(json_, str)
|
|
311
|
+
json_ = json.loads(json_)
|
|
312
|
+
assert isinstance(json_, list)
|
|
313
|
+
assert len(json_) == int(limit)
|
|
314
|
+
row = json_[0]
|
|
315
|
+
if q.get("fields"):
|
|
316
|
+
for field in q["fields"]:
|
|
317
|
+
assert field in row.keys()
|
|
318
|
+
|
|
319
|
+
csv = sdk.run_query(query.id, "csv")
|
|
320
|
+
assert isinstance(csv, str)
|
|
321
|
+
assert len(re.findall(r"\n", csv)) == int(limit) + 1
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_it_runs_inline_query(sdk: mtds.Looker40SDK, queries_system_activity: TQueries):
|
|
325
|
+
"""run_inline_query() should run a query and return its results."""
|
|
326
|
+
for q in queries_system_activity:
|
|
327
|
+
limit = cast(str, q["limit"]) or "10"
|
|
328
|
+
request = create_query_request(q, limit)
|
|
329
|
+
|
|
330
|
+
json_resp = sdk.run_inline_query("json", request)
|
|
331
|
+
assert isinstance(json_resp, str)
|
|
332
|
+
json_: List[Dict[str, Any]] = json.loads(json_resp)
|
|
333
|
+
assert len(json_) == int(limit)
|
|
334
|
+
|
|
335
|
+
row = json_[0]
|
|
336
|
+
if q.get("fields"):
|
|
337
|
+
for field in q["fields"]:
|
|
338
|
+
assert field in row.keys()
|
|
339
|
+
|
|
340
|
+
csv = sdk.run_inline_query("csv", request)
|
|
341
|
+
assert isinstance(csv, str)
|
|
342
|
+
assert len(re.findall(r"\n", csv)) == int(limit) + 1
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.mark.usefixtures("remove_test_looks")
|
|
346
|
+
def test_crud_look(sdk: mtds.Looker40SDK, looks):
|
|
347
|
+
"""Test creating, retrieving, updating and deleting a look."""
|
|
348
|
+
for l in looks:
|
|
349
|
+
request = create_query_request(l["query"][0], "10")
|
|
350
|
+
query = sdk.create_query(request)
|
|
351
|
+
|
|
352
|
+
look = sdk.create_look(
|
|
353
|
+
ml.WriteLookWithQuery(
|
|
354
|
+
title=l.get("title"),
|
|
355
|
+
description=l.get("description"),
|
|
356
|
+
deleted=l.get("deleted"),
|
|
357
|
+
is_run_on_load=l.get("is_run_on_load"),
|
|
358
|
+
public=l.get("public"),
|
|
359
|
+
query_id=query.id,
|
|
360
|
+
folder_id=l.get("folder_id") or str(sdk.me().personal_folder_id),
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
assert isinstance(look, ml.LookWithQuery)
|
|
365
|
+
assert look.title == l.get("title")
|
|
366
|
+
assert look.description == l.get("description")
|
|
367
|
+
assert look.deleted == l.get("deleted")
|
|
368
|
+
assert look.is_run_on_load == l.get("is_run_on_load")
|
|
369
|
+
# TODO this is broken for local dev but works for CI...
|
|
370
|
+
# assert look.public == l.get("public")
|
|
371
|
+
assert look.query_id == query.id
|
|
372
|
+
assert look.folder_id == l.get("folder_id") or sdk.me().home_folder_id
|
|
373
|
+
assert look.user_id == l.get("user_id") or sdk.me().id
|
|
374
|
+
|
|
375
|
+
# Update
|
|
376
|
+
assert isinstance(look.id, str)
|
|
377
|
+
updated_look = sdk.update_look(look.id, ml.WriteLookWithQuery(deleted=True))
|
|
378
|
+
assert updated_look.deleted
|
|
379
|
+
assert updated_look.title == look.title
|
|
380
|
+
|
|
381
|
+
look = sdk.update_look(look.id, ml.WriteLookWithQuery(deleted=False))
|
|
382
|
+
assert not look.deleted
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_png_svg_downloads(sdk: mtds.Looker40SDK):
|
|
386
|
+
"""content_thumbnail() should return a binary or string response based on the specified format."""
|
|
387
|
+
looks = sdk.search_looks(limit=1)
|
|
388
|
+
resource_id: str
|
|
389
|
+
if looks:
|
|
390
|
+
resource_type = "look"
|
|
391
|
+
resource_id = str(looks[0].id)
|
|
392
|
+
else:
|
|
393
|
+
dashboards = sdk.search_dashboards(limit=1)
|
|
394
|
+
if dashboards:
|
|
395
|
+
resource_type = "dashboard"
|
|
396
|
+
resource_id = cast(str, dashboards[0].id)
|
|
397
|
+
|
|
398
|
+
png = sdk.content_thumbnail(
|
|
399
|
+
type=resource_type, resource_id=resource_id, format="png"
|
|
400
|
+
)
|
|
401
|
+
assert isinstance(png, bytes)
|
|
402
|
+
try:
|
|
403
|
+
Image.open(io.BytesIO(png))
|
|
404
|
+
except IOError:
|
|
405
|
+
raise AssertionError("png format failed to return an image")
|
|
406
|
+
|
|
407
|
+
svg = sdk.content_thumbnail(
|
|
408
|
+
type=resource_type, resource_id=resource_id, format="svg"
|
|
409
|
+
)
|
|
410
|
+
assert isinstance(svg, str)
|
|
411
|
+
assert "<?xml" in svg
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_setting_default_color_collection(sdk: mtds.Looker40SDK):
|
|
415
|
+
"""Given a color collection id, set_default_color_collection() should change the default collection."""
|
|
416
|
+
original = sdk.default_color_collection()
|
|
417
|
+
assert isinstance(original, ml.ColorCollection)
|
|
418
|
+
assert isinstance(original.id, str)
|
|
419
|
+
color_collections = sdk.all_color_collections()
|
|
420
|
+
other: ml.ColorCollection = next(
|
|
421
|
+
filter(lambda c: c.id != original.id, color_collections)
|
|
422
|
+
)
|
|
423
|
+
assert isinstance(other.id, str)
|
|
424
|
+
actual = sdk.set_default_color_collection(other.id)
|
|
425
|
+
assert actual.id == other.id
|
|
426
|
+
updated = sdk.set_default_color_collection(original.id)
|
|
427
|
+
assert updated.id == original.id
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_search_looks_returns_looks(sdk: mtds.Looker40SDK):
|
|
431
|
+
"""search_looks() should return a list of looks."""
|
|
432
|
+
search_results = sdk.search_looks()
|
|
433
|
+
assert isinstance(search_results, list)
|
|
434
|
+
assert len(search_results) > 0
|
|
435
|
+
look = search_results[0]
|
|
436
|
+
assert isinstance(look, ml.Look)
|
|
437
|
+
assert look.title != ""
|
|
438
|
+
assert look.created_at is not None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_search_looks_fields_filter(sdk: mtds.Looker40SDK):
|
|
442
|
+
"""search_looks() should only return the requested fields passed in the fields
|
|
443
|
+
argument of the request.
|
|
444
|
+
"""
|
|
445
|
+
search_results = sdk.search_looks(fields="id, title, description")
|
|
446
|
+
assert isinstance(search_results, list)
|
|
447
|
+
assert len(search_results) > 0
|
|
448
|
+
look = search_results[0]
|
|
449
|
+
assert isinstance(look, ml.Look)
|
|
450
|
+
assert look.title is not None
|
|
451
|
+
assert look.created_at is None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_search_looks_title_fields_filter(sdk: mtds.Looker40SDK):
|
|
455
|
+
"""search_looks() should be able to filter on title."""
|
|
456
|
+
search_results = sdk.search_looks(title="An SDK%", fields="id, title")
|
|
457
|
+
assert isinstance(search_results, list)
|
|
458
|
+
assert len(search_results) > 0
|
|
459
|
+
look = search_results[0]
|
|
460
|
+
assert isinstance(look.id, str)
|
|
461
|
+
assert look.id != "0"
|
|
462
|
+
assert "SDK" in look.title
|
|
463
|
+
assert look.description is None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_search_look_and_run(sdk: mtds.Looker40SDK):
|
|
467
|
+
"""run_look() should return CSV and JSON
|
|
468
|
+
CSV will use column descriptions
|
|
469
|
+
JSON will use column names
|
|
470
|
+
JSON_LABEL will use column descriptions
|
|
471
|
+
"""
|
|
472
|
+
search_results = sdk.search_looks(title="An SDK Look", fields="id, title")
|
|
473
|
+
assert isinstance(search_results, list)
|
|
474
|
+
assert len(search_results) > 0
|
|
475
|
+
look = search_results[0]
|
|
476
|
+
assert isinstance(look.id, str)
|
|
477
|
+
assert look.id != "0"
|
|
478
|
+
assert "SDK" in look.title
|
|
479
|
+
assert look.description is None
|
|
480
|
+
actual = sdk.run_look(look_id=look.id, result_format="csv")
|
|
481
|
+
assert "Dashboard Count" in actual
|
|
482
|
+
assert "Dashboard ID" in actual
|
|
483
|
+
actual = sdk.run_look(look_id=look.id, result_format="json")
|
|
484
|
+
assert "dashboard.count" in actual
|
|
485
|
+
assert "dashboard.id" in actual
|
|
486
|
+
actual = sdk.run_look(look_id=look.id, result_format="json_label")
|
|
487
|
+
assert "Dashboard Count" in actual
|
|
488
|
+
assert "Dashboard ID" in actual
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def create_query_request(q, limit: Optional[str] = None) -> ml.WriteQuery:
|
|
492
|
+
return ml.WriteQuery(
|
|
493
|
+
model=q.get("model"),
|
|
494
|
+
view=q.get("view"),
|
|
495
|
+
fields=q.get("fields"),
|
|
496
|
+
pivots=q.get("pivots"),
|
|
497
|
+
fill_fields=q.get("fill_fields"),
|
|
498
|
+
filters=q.get("filters"),
|
|
499
|
+
filter_expression=q.get("filter_expressions"),
|
|
500
|
+
sorts=q.get("sorts"),
|
|
501
|
+
limit=q.get("limit") or limit,
|
|
502
|
+
column_limit=q.get("column_limit"),
|
|
503
|
+
total=q.get("total"),
|
|
504
|
+
row_total=q.get("row_total"),
|
|
505
|
+
subtotals=q.get("subtotal"),
|
|
506
|
+
vis_config=q.get("vis_config"),
|
|
507
|
+
filter_config=q.get("filter_config"),
|
|
508
|
+
visible_ui_sections=q.get("visible_ui_sections"),
|
|
509
|
+
dynamic_fields=q.get("dynamic_fields"),
|
|
510
|
+
client_id=q.get("client_id"),
|
|
511
|
+
query_timezone=q.get("query_timezone"),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@pytest.mark.usefixtures("remove_test_dashboards")
|
|
516
|
+
def test_crud_dashboard(sdk: mtds.Looker40SDK, queries_system_activity, dashboards):
|
|
517
|
+
"""Test creating, retrieving, updating and deleting a dashboard."""
|
|
518
|
+
qhash: Dict[Union[str, int], ml.Query] = {}
|
|
519
|
+
for idx, q in enumerate(queries_system_activity):
|
|
520
|
+
limit = "10"
|
|
521
|
+
request = create_query_request(q, limit)
|
|
522
|
+
key = q.get("id") or str(idx)
|
|
523
|
+
qhash[key] = sdk.create_query(request)
|
|
524
|
+
|
|
525
|
+
for d in dashboards:
|
|
526
|
+
dashboard = sdk.create_dashboard(
|
|
527
|
+
ml.WriteDashboard(
|
|
528
|
+
description=d.get("description"),
|
|
529
|
+
hidden=d.get("hidden"),
|
|
530
|
+
query_timezone=d.get("query_timezone"),
|
|
531
|
+
refresh_interval=d.get("refresh_interval"),
|
|
532
|
+
title=d.get("title"),
|
|
533
|
+
background_color=d.get("background_color"),
|
|
534
|
+
load_configuration=d.get("load_configuration"),
|
|
535
|
+
lookml_link_id=d.get("lookml_link_id"),
|
|
536
|
+
show_filters_bar=d.get("show_filters_bar"),
|
|
537
|
+
show_title=d.get("show_title"),
|
|
538
|
+
slug=d.get("slug"),
|
|
539
|
+
folder_id=d.get("folder_id") or sdk.me().home_folder_id,
|
|
540
|
+
text_tile_text_color=d.get("text_tile_text_color"),
|
|
541
|
+
tile_background_color=d.get("tile_background_color"),
|
|
542
|
+
tile_text_color=d.get("tile_text_color"),
|
|
543
|
+
title_color=d.get("title_color"),
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
assert isinstance(dashboard, ml.Dashboard)
|
|
548
|
+
assert isinstance(dashboard.created_at, datetime.datetime)
|
|
549
|
+
|
|
550
|
+
if d.get("background_color"):
|
|
551
|
+
assert d["background_color"] == dashboard.background_color
|
|
552
|
+
|
|
553
|
+
if d.get("text_tile_text_color"):
|
|
554
|
+
assert d["text_tile_text_color"] == dashboard.text_tile_text_color
|
|
555
|
+
|
|
556
|
+
if d.get("tile_background_color"):
|
|
557
|
+
assert d["tile_background_color"] == dashboard.tile_background_color
|
|
558
|
+
|
|
559
|
+
if d.get("tile_text_color"):
|
|
560
|
+
assert d["tile_text_color"] == dashboard.tile_text_color
|
|
561
|
+
|
|
562
|
+
if d.get("title_color"):
|
|
563
|
+
assert d["title_color"] == dashboard.title_color
|
|
564
|
+
|
|
565
|
+
# Update dashboard
|
|
566
|
+
assert isinstance(dashboard.id, str)
|
|
567
|
+
update_response = sdk.update_dashboard(
|
|
568
|
+
dashboard.id, ml.WriteDashboard(deleted=True)
|
|
569
|
+
)
|
|
570
|
+
assert update_response.deleted
|
|
571
|
+
assert update_response.title == dashboard.title
|
|
572
|
+
|
|
573
|
+
dashboard = sdk.update_dashboard(dashboard.id, ml.WriteDashboard(deleted=False))
|
|
574
|
+
assert isinstance(dashboard.id, str)
|
|
575
|
+
assert not dashboard.deleted
|
|
576
|
+
|
|
577
|
+
if d.get("filters"):
|
|
578
|
+
for f in d["filters"]:
|
|
579
|
+
filter = sdk.create_dashboard_filter(
|
|
580
|
+
ml.WriteCreateDashboardFilter(
|
|
581
|
+
dashboard_id=dashboard.id,
|
|
582
|
+
name=f.get("name"),
|
|
583
|
+
title=f.get("title"),
|
|
584
|
+
type=f.get("type"),
|
|
585
|
+
default_value=f.get("default_value"),
|
|
586
|
+
model=f.get("model"),
|
|
587
|
+
explore=f.get("explore"),
|
|
588
|
+
dimension=f.get("dimension"),
|
|
589
|
+
row=f.get("row"),
|
|
590
|
+
listens_to_filters=f.get("listens_to_filters"),
|
|
591
|
+
allow_multiple_values=f.get("allow_multiple_values"),
|
|
592
|
+
required=f.get("required"),
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
assert isinstance(filter, ml.DashboardFilter)
|
|
596
|
+
assert filter.name == f.get("name")
|
|
597
|
+
assert filter.title == f.get("title")
|
|
598
|
+
assert filter.type == f.get("type")
|
|
599
|
+
assert filter.default_value == f.get("default_value")
|
|
600
|
+
assert filter.model == f.get("model")
|
|
601
|
+
assert filter.explore == f.get("explore")
|
|
602
|
+
assert filter.dimension == f.get("dimension")
|
|
603
|
+
assert filter.row == f.get("row")
|
|
604
|
+
assert filter.allow_multiple_values == f.get(
|
|
605
|
+
"allow_multiple_values", False
|
|
606
|
+
)
|
|
607
|
+
assert filter.required == f.get("required", False)
|
|
608
|
+
|
|
609
|
+
if d.get("tiles"):
|
|
610
|
+
for t in d["tiles"]:
|
|
611
|
+
tile = sdk.create_dashboard_element(
|
|
612
|
+
ml.WriteDashboardElement(
|
|
613
|
+
body_text=t.get("body_text"),
|
|
614
|
+
dashboard_id=dashboard.id,
|
|
615
|
+
look=t.get("look"),
|
|
616
|
+
look_id=t.get("look_id"),
|
|
617
|
+
merge_result_id=t.get("merge_result_id"),
|
|
618
|
+
note_display=t.get("note_display"),
|
|
619
|
+
note_state=t.get("note_state"),
|
|
620
|
+
note_text=t.get("note_text"),
|
|
621
|
+
query=t.get("query"),
|
|
622
|
+
query_id=get_query_id(qhash, t.get("query_id")),
|
|
623
|
+
refresh_interval=t.get("refresh_interval"),
|
|
624
|
+
subtitle_text=t.get("subtitle_text"),
|
|
625
|
+
title=t.get("title"),
|
|
626
|
+
title_hidden=t.get("title_hidden"),
|
|
627
|
+
type=t.get("type"),
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
assert isinstance(tile, ml.DashboardElement)
|
|
632
|
+
assert tile.dashboard_id == dashboard.id
|
|
633
|
+
assert tile.title == t.get("title")
|
|
634
|
+
assert tile.type == t.get("type")
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def get_query_id(
|
|
638
|
+
qhash: Dict[Union[str, int], ml.Query], id: Union[str, int]
|
|
639
|
+
) -> Optional[int]:
|
|
640
|
+
if isinstance(id, str) and id.startswith("#"):
|
|
641
|
+
id = id[1:]
|
|
642
|
+
# if id is invalid, default to first query. test data is bad
|
|
643
|
+
query = qhash.get(id) or list(qhash.values())[0]
|
|
644
|
+
query_id = query.id
|
|
645
|
+
elif (isinstance(id, str) and id.isdigit()) or isinstance(id, int):
|
|
646
|
+
query_id = int(id)
|
|
647
|
+
else:
|
|
648
|
+
query_id = None
|
|
649
|
+
return query_id
|
|
650
|
+
|
|
651
|
+
@pytest.mark.skip(reason="TODO: This breaks CI right now")
|
|
652
|
+
def test_validate_theme(sdk: mtds.Looker40SDK):
|
|
653
|
+
|
|
654
|
+
valid_theme_response = sdk.validate_theme(
|
|
655
|
+
body = ml.WriteTheme(
|
|
656
|
+
name = 'valid_theme',
|
|
657
|
+
settings = ml.ThemeSettings(
|
|
658
|
+
show_filters_bar = False,
|
|
659
|
+
show_title = False,
|
|
660
|
+
tile_shadow = False,
|
|
661
|
+
font_family = 'Arial',
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
assert valid_theme_response == ""
|
|
666
|
+
|
|
667
|
+
try:
|
|
668
|
+
sdk.validate_theme(
|
|
669
|
+
body = ml.WriteTheme(
|
|
670
|
+
settings = ml.ThemeSettings(
|
|
671
|
+
show_filters_bar = False,
|
|
672
|
+
show_title = False,
|
|
673
|
+
tile_shadow = False,
|
|
674
|
+
font_family = 'Arial;',
|
|
675
|
+
)
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
except Exception as e:
|
|
679
|
+
assert e.message is not None
|
|
680
|
+
assert e.message != ""
|
|
681
|
+
assert len(e.errors) == 3
|