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,284 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2019 Looker Data Sciences, Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import pytest # type: ignore
|
|
25
|
+
import urllib.parse
|
|
26
|
+
|
|
27
|
+
from looker_sdk import error
|
|
28
|
+
from looker_sdk.rtl import auth_session as auth
|
|
29
|
+
from looker_sdk.rtl import api_settings
|
|
30
|
+
from looker_sdk.rtl import serialize
|
|
31
|
+
from looker_sdk.rtl import transport
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="function")
|
|
35
|
+
def config_file(tmpdir_factory, monkeypatch):
|
|
36
|
+
"""Creates a sample looker.ini file and returns it"""
|
|
37
|
+
# make sure test is only using these settings
|
|
38
|
+
for setting in ["BASE_URL", "CLIENT_ID", "CLIENT_SECRET"]:
|
|
39
|
+
monkeypatch.delenv(f"LOOKERSDK_{setting}", raising=False)
|
|
40
|
+
filename = tmpdir_factory.mktemp("settings").join("looker.ini")
|
|
41
|
+
filename.write(
|
|
42
|
+
"""
|
|
43
|
+
[Looker]
|
|
44
|
+
# Base URL for API. Do not include /api/* in the url
|
|
45
|
+
base_url=https://host1.looker.com:19999
|
|
46
|
+
# API 3 client id
|
|
47
|
+
client_id=your_API3_client_id
|
|
48
|
+
# API 3 client secret
|
|
49
|
+
client_secret=your_API3_client_secret
|
|
50
|
+
# Set to false if testing locally against self-signed certs. Otherwise leave True
|
|
51
|
+
verify_ssl=True
|
|
52
|
+
looker_url=https://webserver.looker.com:9999
|
|
53
|
+
redirect_uri=https://alice.com/auth
|
|
54
|
+
|
|
55
|
+
[NO_CREDENTIALS]
|
|
56
|
+
base_url=https://host1.looker.com:19999
|
|
57
|
+
|
|
58
|
+
[EMPTY_STRING_CREDENTIALS]
|
|
59
|
+
base_url=https://host1.looker.com:19999
|
|
60
|
+
client_id=
|
|
61
|
+
client_secret=
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
return filename
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture(scope="function")
|
|
68
|
+
def auth_session(config_file):
|
|
69
|
+
settings = api_settings.ApiSettings(filename=config_file, env_prefix="LOOKERSDK")
|
|
70
|
+
return auth.AuthSession(
|
|
71
|
+
settings, MockTransport.configure(settings), serialize.deserialize40, "4.0"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MockTransport(transport.Transport):
|
|
76
|
+
"""A mock transport layer used for testing purposes"""
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def configure(cls, settings):
|
|
80
|
+
return cls()
|
|
81
|
+
|
|
82
|
+
def request(
|
|
83
|
+
self,
|
|
84
|
+
method,
|
|
85
|
+
path,
|
|
86
|
+
query_params=None,
|
|
87
|
+
body=None,
|
|
88
|
+
authenticator=None,
|
|
89
|
+
transport_options=None,
|
|
90
|
+
):
|
|
91
|
+
if authenticator:
|
|
92
|
+
authenticator(transport_options)
|
|
93
|
+
if method == transport.HttpMethod.POST:
|
|
94
|
+
if path.endswith(("login", "login/5")):
|
|
95
|
+
if path.endswith("login"):
|
|
96
|
+
token = "AdminAccessToken"
|
|
97
|
+
expected_headers = {
|
|
98
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
99
|
+
}
|
|
100
|
+
expected_headers.update(transport_options.get("headers", {}))
|
|
101
|
+
if transport_options["headers"] != expected_headers:
|
|
102
|
+
raise TypeError(f"Must send {expected_headers}")
|
|
103
|
+
else:
|
|
104
|
+
token = "UserAccessToken"
|
|
105
|
+
access_token = json.dumps(
|
|
106
|
+
{"access_token": token, "token_type": "Bearer", "expires_in": 3600}
|
|
107
|
+
)
|
|
108
|
+
response = transport.Response(
|
|
109
|
+
ok=True,
|
|
110
|
+
value=bytes(access_token, encoding="utf-8"),
|
|
111
|
+
response_mode=transport.ResponseMode.STRING,
|
|
112
|
+
)
|
|
113
|
+
elif path.endswith("/api/token"):
|
|
114
|
+
access_token = {
|
|
115
|
+
"access_token": "anOauthToken",
|
|
116
|
+
"token_type": "Bearer",
|
|
117
|
+
"expires_in": 3600,
|
|
118
|
+
"refresh_token": "anOauthRefreshToken",
|
|
119
|
+
}
|
|
120
|
+
response = transport.Response(
|
|
121
|
+
ok=True,
|
|
122
|
+
value=json.dumps(access_token).encode("utf8"),
|
|
123
|
+
response_mode=transport.ResponseMode.STRING,
|
|
124
|
+
)
|
|
125
|
+
elif method == transport.HttpMethod.DELETE and path.endswith("logout"):
|
|
126
|
+
response = transport.Response(
|
|
127
|
+
ok=True, value=b"", response_mode=transport.ResponseMode.STRING
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
raise TypeError("Bad transport layer call")
|
|
131
|
+
return response
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_auto_login(auth_session: auth.AuthSession):
|
|
135
|
+
assert not auth_session.is_authenticated
|
|
136
|
+
auth_header = auth_session.authenticate({})
|
|
137
|
+
assert auth_header["Authorization"] == "Bearer AdminAccessToken"
|
|
138
|
+
assert auth_session.is_authenticated
|
|
139
|
+
|
|
140
|
+
# even after explicit logout
|
|
141
|
+
auth_session.logout()
|
|
142
|
+
assert not auth_session.is_authenticated
|
|
143
|
+
auth_header = auth_session.authenticate({})
|
|
144
|
+
assert isinstance(auth_header, dict)
|
|
145
|
+
assert auth_header["Authorization"] == "Bearer AdminAccessToken"
|
|
146
|
+
assert auth_session.is_authenticated
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_auto_login_with_transport_options(auth_session: auth.AuthSession):
|
|
150
|
+
assert not auth_session.is_authenticated
|
|
151
|
+
auth_header = auth_session.authenticate({"headers": {"foo": "bar"}})
|
|
152
|
+
assert auth_header["Authorization"] == "Bearer AdminAccessToken"
|
|
153
|
+
assert auth_session.is_authenticated
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_sudo_login_auto_logs_in(auth_session: auth.AuthSession):
|
|
157
|
+
assert not auth_session.is_authenticated
|
|
158
|
+
assert not auth_session.is_sudo_authenticated
|
|
159
|
+
auth_session.login_user(5)
|
|
160
|
+
assert auth_session.is_authenticated
|
|
161
|
+
assert auth_session.is_sudo_authenticated
|
|
162
|
+
auth_header = auth_session.authenticate({})
|
|
163
|
+
assert auth_header["Authorization"] == "Bearer UserAccessToken"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_sudo_logout_leaves_logged_in(auth_session: auth.AuthSession):
|
|
167
|
+
auth_session.login_user(5)
|
|
168
|
+
auth_session.logout()
|
|
169
|
+
assert not auth_session.is_sudo_authenticated
|
|
170
|
+
assert auth_session.is_authenticated
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_login_sudo_login_sudo(auth_session: auth.AuthSession):
|
|
174
|
+
auth_session.login_user(5)
|
|
175
|
+
with pytest.raises(error.SDKError):
|
|
176
|
+
auth_session.login_user(10)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.parametrize(
|
|
180
|
+
"test_section, test_env_client_id, test_env_client_secret",
|
|
181
|
+
[
|
|
182
|
+
("NO_CREDENTIALS", "", ""),
|
|
183
|
+
("NO_CREDENTIALS", "id123", ""),
|
|
184
|
+
("NO_CREDENTIALS", "", "secret123"),
|
|
185
|
+
("EMPTY_STRING_CREDENTIALS", "", ""),
|
|
186
|
+
("EMPTY_STRING_CREDENTIALS", "id123", ""),
|
|
187
|
+
("EMPTY_STRING_CREDENTIALS", "", "secret123"),
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
def test_it_fails_with_missing_credentials(
|
|
191
|
+
config_file, monkeypatch, test_section, test_env_client_id, test_env_client_secret
|
|
192
|
+
):
|
|
193
|
+
monkeypatch.setenv("LOOKERSDK_CLIENT_ID", test_env_client_id)
|
|
194
|
+
monkeypatch.setenv("LOOKERSDK_CLIENT_SECRET", test_env_client_secret)
|
|
195
|
+
|
|
196
|
+
settings = api_settings.ApiSettings(filename=config_file, section=test_section)
|
|
197
|
+
settings.api_version = "4.0"
|
|
198
|
+
|
|
199
|
+
auth_session = auth.AuthSession(
|
|
200
|
+
settings, MockTransport.configure(settings), serialize.deserialize40, "4.0"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
with pytest.raises(error.SDKError) as exc_info:
|
|
204
|
+
auth_session.authenticate({})
|
|
205
|
+
assert "auth credentials not found" in str(exc_info.value)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.parametrize(
|
|
209
|
+
"test_env_client_id, test_env_client_secret, expected_id, expected_secret",
|
|
210
|
+
[
|
|
211
|
+
("", "", "your_API3_client_id", "your_API3_client_secret"),
|
|
212
|
+
("id123", "secret123", "id123", "secret123"),
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
def test_env_variables_override_config_file_credentials(
|
|
216
|
+
auth_session: auth.AuthSession,
|
|
217
|
+
mocker,
|
|
218
|
+
monkeypatch,
|
|
219
|
+
test_env_client_id: str,
|
|
220
|
+
test_env_client_secret: str,
|
|
221
|
+
expected_id: str,
|
|
222
|
+
expected_secret: str,
|
|
223
|
+
):
|
|
224
|
+
monkeypatch.setenv("LOOKERSDK_CLIENT_ID", test_env_client_id)
|
|
225
|
+
monkeypatch.setenv("LOOKERSDK_CLIENT_SECRET", test_env_client_secret)
|
|
226
|
+
mocked_request = mocker.patch.object(MockTransport, "request")
|
|
227
|
+
mocked_request.return_value = transport.Response(
|
|
228
|
+
ok=True,
|
|
229
|
+
value=json.dumps(
|
|
230
|
+
{
|
|
231
|
+
"access_token": "AdminAccessToken",
|
|
232
|
+
"token_type": "Bearer",
|
|
233
|
+
"expires_in": 3600,
|
|
234
|
+
}
|
|
235
|
+
).encode("utf-8"),
|
|
236
|
+
response_mode=transport.ResponseMode.STRING,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
auth_session.authenticate({})
|
|
240
|
+
|
|
241
|
+
expected_body = urllib.parse.urlencode(
|
|
242
|
+
{"client_id": expected_id, "client_secret": expected_secret}
|
|
243
|
+
).encode("utf-8")
|
|
244
|
+
mocked_request.assert_called()
|
|
245
|
+
actual_request_body = mocked_request.call_args[1]["body"]
|
|
246
|
+
assert actual_request_body == expected_body
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@pytest.fixture(scope="function")
|
|
250
|
+
def oauth_session(config_file):
|
|
251
|
+
settings = api_settings.ApiSettings(filename=config_file)
|
|
252
|
+
return auth.OAuthSession(
|
|
253
|
+
settings=settings,
|
|
254
|
+
transport=MockTransport.configure(settings),
|
|
255
|
+
deserialize=serialize.deserialize40,
|
|
256
|
+
serialize=serialize.serialize40,
|
|
257
|
+
crypto=auth.CryptoHash(),
|
|
258
|
+
version="4.0",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_oauth_create_auth_code_request_url(oauth_session):
|
|
263
|
+
url = oauth_session.create_auth_code_request_url("api", "mystate")
|
|
264
|
+
url = urllib.parse.urlparse(url)
|
|
265
|
+
assert url.hostname == "webserver.looker.com"
|
|
266
|
+
assert url.port == 9999
|
|
267
|
+
assert url.path == "/auth"
|
|
268
|
+
query = urllib.parse.parse_qs(url.query)
|
|
269
|
+
assert query.get("response_type") == ["code"]
|
|
270
|
+
# Re-using client_id from config. For oauth this is not an API3Credentials
|
|
271
|
+
# client_id but rather the app client_id
|
|
272
|
+
assert query.get("client_id") == ["your_API3_client_id"]
|
|
273
|
+
assert query.get("redirect_uri") == ["https://alice.com/auth"]
|
|
274
|
+
assert query.get("scope") == ["api"]
|
|
275
|
+
assert query.get("state") == ["mystate"]
|
|
276
|
+
assert query.get("code_challenge_method") == ["S256"]
|
|
277
|
+
assert query.get("code_challenge")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_oauth_redeem_auth_code(oauth_session):
|
|
281
|
+
oauth_session.redeem_auth_code("foobar")
|
|
282
|
+
assert oauth_session.token.access_token == "anOauthToken"
|
|
283
|
+
assert oauth_session.token.refresh_token == "anOauthRefreshToken"
|
|
284
|
+
assert oauth_session.token.token_type == "Bearer"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2019 Looker Data Sciences, Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from looker_sdk.rtl import auth_token
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_defaults_with_empty_token():
|
|
27
|
+
"""Confirm the defaults when initializing AuthToken without arguments."""
|
|
28
|
+
actual = auth_token.AuthToken()
|
|
29
|
+
|
|
30
|
+
assert actual.access_token == ""
|
|
31
|
+
assert actual.token_type == ""
|
|
32
|
+
assert actual.expires_in == 0
|
|
33
|
+
assert actual.is_active is False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_is_active_with_full_token():
|
|
37
|
+
"""Confirm active token when AuthToken is initialized properly."""
|
|
38
|
+
actual = auth_token.AuthToken(
|
|
39
|
+
auth_token.AccessToken(
|
|
40
|
+
access_token="all-access", token_type="backstage", expires_in=3600
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert actual.access_token == "all-access"
|
|
45
|
+
assert actual.token_type == "backstage"
|
|
46
|
+
assert actual.expires_in == 3600
|
|
47
|
+
assert actual.is_active is True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_lag_time_is_used():
|
|
51
|
+
"""Confirm active token when expiration is > lag time."""
|
|
52
|
+
actual = auth_token.AuthToken(
|
|
53
|
+
auth_token.AccessToken(
|
|
54
|
+
access_token="all-access", token_type="backstage", expires_in=9
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert actual.access_token == "all-access"
|
|
59
|
+
assert actual.token_type == "backstage"
|
|
60
|
+
assert actual.expires_in == 9
|
|
61
|
+
assert actual.is_active is False
|
|
62
|
+
|
|
63
|
+
actual = auth_token.AuthToken(
|
|
64
|
+
auth_token.AccessToken(
|
|
65
|
+
access_token="all-access", token_type="backstage", expires_in=11
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert actual.expires_in == 11
|
|
70
|
+
assert actual.is_active is True
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2019 Looker Data Sciences, Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from typing import cast, MutableMapping, Optional
|
|
24
|
+
|
|
25
|
+
import attr
|
|
26
|
+
import pytest # type: ignore
|
|
27
|
+
import requests
|
|
28
|
+
|
|
29
|
+
from looker_sdk.rtl import requests_transport
|
|
30
|
+
from looker_sdk.rtl import transport
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@attr.s(auto_attribs=True)
|
|
34
|
+
class Response:
|
|
35
|
+
"""Fake requests.Response
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
ok: bool
|
|
39
|
+
content: bytes
|
|
40
|
+
headers: MutableMapping[str, str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Session:
|
|
44
|
+
"""Fake requests.Session
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, ret_val, error=False):
|
|
48
|
+
self.headers = {}
|
|
49
|
+
self.ret_val = ret_val
|
|
50
|
+
self.error = error
|
|
51
|
+
|
|
52
|
+
def request(self, method, url, auth, params, data, headers, timeout):
|
|
53
|
+
"""Fake request.Session.request
|
|
54
|
+
"""
|
|
55
|
+
if self.error:
|
|
56
|
+
raise IOError((54, "Connection reset by peer"))
|
|
57
|
+
return self.ret_val
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
|
61
|
+
class TransportSettings:
|
|
62
|
+
"""Fake TransportSettings
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
base_url: str = ""
|
|
66
|
+
verify_ssl: bool = True
|
|
67
|
+
timeout: int = 120
|
|
68
|
+
headers: Optional[MutableMapping[str, str]] = None
|
|
69
|
+
agent_tag: str = "foobar"
|
|
70
|
+
|
|
71
|
+
def is_configured(self) -> bool:
|
|
72
|
+
return bool(self.base_url)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def settings():
|
|
77
|
+
return TransportSettings(base_url="/some/path", headers=None, verify_ssl=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_configure(settings):
|
|
81
|
+
"""Test configuration creates instance.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
test = requests_transport.RequestsTransport.configure(settings)
|
|
85
|
+
assert isinstance(test, requests_transport.RequestsTransport)
|
|
86
|
+
assert test.session.headers.get("x-looker-appid") == f"foobar"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
parametrize = [
|
|
90
|
+
({"Content-Type": "application/json"}, "utf-8", transport.ResponseMode.STRING),
|
|
91
|
+
({"Content-Type": "image/png"}, "utf-8", transport.ResponseMode.BINARY),
|
|
92
|
+
(
|
|
93
|
+
{"Content-Type": "text/xml; charset=latin1"},
|
|
94
|
+
"latin1",
|
|
95
|
+
transport.ResponseMode.STRING,
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
{"Content-Type": "text/plain; charset=arabic"},
|
|
99
|
+
"arabic",
|
|
100
|
+
transport.ResponseMode.STRING,
|
|
101
|
+
),
|
|
102
|
+
(
|
|
103
|
+
{"Content-Type": "text/plain; charset=utf-8"},
|
|
104
|
+
"utf-8",
|
|
105
|
+
transport.ResponseMode.STRING,
|
|
106
|
+
),
|
|
107
|
+
({"Content-Type": "audio/gzip"}, "utf-8", transport.ResponseMode.BINARY),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize(
|
|
112
|
+
"headers, expected_encoding, expected_response_mode", parametrize
|
|
113
|
+
)
|
|
114
|
+
def test_request_ok(
|
|
115
|
+
settings: transport.PTransportSettings,
|
|
116
|
+
headers: MutableMapping[str, str],
|
|
117
|
+
expected_response_mode: transport.ResponseMode,
|
|
118
|
+
expected_encoding: str,
|
|
119
|
+
):
|
|
120
|
+
"""Test basic successful round trip
|
|
121
|
+
"""
|
|
122
|
+
value = b"yay!"
|
|
123
|
+
ret_val = Response(
|
|
124
|
+
ok=True, content=value, headers=requests.structures.CaseInsensitiveDict(headers)
|
|
125
|
+
)
|
|
126
|
+
session = cast(requests.Session, Session(ret_val))
|
|
127
|
+
test = requests_transport.RequestsTransport(settings, session)
|
|
128
|
+
resp = test.request(transport.HttpMethod.GET, "/some/path")
|
|
129
|
+
assert isinstance(resp, transport.Response)
|
|
130
|
+
assert resp.value == value
|
|
131
|
+
assert resp.ok is True
|
|
132
|
+
assert resp.response_mode == expected_response_mode
|
|
133
|
+
assert resp.encoding == expected_encoding
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.parametrize(
|
|
137
|
+
"headers, expected_encoding, expected_response_mode", parametrize
|
|
138
|
+
)
|
|
139
|
+
def test_request_not_ok(
|
|
140
|
+
settings: transport.PTransportSettings,
|
|
141
|
+
headers: MutableMapping[str, str],
|
|
142
|
+
expected_response_mode: transport.ResponseMode,
|
|
143
|
+
expected_encoding: str,
|
|
144
|
+
):
|
|
145
|
+
"""Test API error response
|
|
146
|
+
"""
|
|
147
|
+
value = b"Some API error"
|
|
148
|
+
ret_val = Response(
|
|
149
|
+
ok=False,
|
|
150
|
+
content=value,
|
|
151
|
+
headers=requests.structures.CaseInsensitiveDict(headers),
|
|
152
|
+
)
|
|
153
|
+
session = cast(requests.Session, Session(ret_val))
|
|
154
|
+
test = requests_transport.RequestsTransport(settings, session)
|
|
155
|
+
resp = test.request(transport.HttpMethod.GET, "/some/path")
|
|
156
|
+
assert isinstance(resp, transport.Response)
|
|
157
|
+
assert resp.value == value
|
|
158
|
+
assert resp.ok is False
|
|
159
|
+
assert resp.response_mode == expected_response_mode
|
|
160
|
+
assert resp.encoding == expected_encoding
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_request_error(settings):
|
|
164
|
+
"""Test network error response
|
|
165
|
+
"""
|
|
166
|
+
session = cast(requests.Session, Session(None, True))
|
|
167
|
+
test = requests_transport.RequestsTransport(settings, session)
|
|
168
|
+
resp = test.request(transport.HttpMethod.GET, "/some/path")
|
|
169
|
+
assert isinstance(resp, transport.Response)
|
|
170
|
+
assert resp.value == b"(54, 'Connection reset by peer')"
|
|
171
|
+
assert resp.ok is False
|