looker-sdk 24.16.2__py3-none-any.whl → 24.18.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/version.py +1 -1
- {looker_sdk-24.16.2.dist-info → looker_sdk-24.18.0.dist-info}/METADATA +1 -1
- looker_sdk-24.18.0.dist-info/RECORD +9 -0
- {looker_sdk-24.16.2.dist-info → looker_sdk-24.18.0.dist-info}/top_level.txt +0 -1
- looker_sdk/rtl/__init__.py +0 -22
- looker_sdk/rtl/api_methods.py +0 -247
- looker_sdk/rtl/api_settings.py +0 -194
- looker_sdk/rtl/auth_session.py +0 -353
- looker_sdk/rtl/auth_token.py +0 -101
- looker_sdk/rtl/constants.py +0 -31
- looker_sdk/rtl/hooks.py +0 -86
- looker_sdk/rtl/model.py +0 -230
- looker_sdk/rtl/requests_transport.py +0 -110
- looker_sdk/rtl/serialize.py +0 -120
- looker_sdk/rtl/transport.py +0 -137
- looker_sdk/sdk/__init__.py +0 -0
- looker_sdk/sdk/api40/__init__.py +0 -1
- looker_sdk/sdk/api40/methods.py +0 -13283
- looker_sdk/sdk/api40/models.py +0 -15641
- looker_sdk/sdk/constants.py +0 -24
- looker_sdk-24.16.2.dist-info/RECORD +0 -38
- tests/__init__.py +0 -0
- tests/conftest.py +0 -133
- tests/integration/__init__.py +0 -2
- tests/integration/test_methods.py +0 -681
- tests/integration/test_netrc.py +0 -55
- tests/rtl/__init__.py +0 -2
- tests/rtl/test_api_methods.py +0 -216
- tests/rtl/test_api_settings.py +0 -252
- tests/rtl/test_auth_session.py +0 -284
- tests/rtl/test_auth_token.py +0 -70
- tests/rtl/test_requests_transport.py +0 -171
- tests/rtl/test_serialize.py +0 -770
- tests/rtl/test_transport.py +0 -34
- {looker_sdk-24.16.2.dist-info → looker_sdk-24.18.0.dist-info}/LICENSE.txt +0 -0
- {looker_sdk-24.16.2.dist-info → looker_sdk-24.18.0.dist-info}/WHEEL +0 -0
looker_sdk/rtl/auth_session.py
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
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
|
-
"""AuthSession to provide automatic authentication
|
|
24
|
-
"""
|
|
25
|
-
import hashlib
|
|
26
|
-
import secrets
|
|
27
|
-
from typing import cast, Dict, Optional, Union
|
|
28
|
-
import urllib.parse
|
|
29
|
-
|
|
30
|
-
import attr
|
|
31
|
-
|
|
32
|
-
from looker_sdk import error
|
|
33
|
-
from looker_sdk.rtl import api_settings
|
|
34
|
-
from looker_sdk.rtl import auth_token
|
|
35
|
-
from looker_sdk.rtl import model
|
|
36
|
-
from looker_sdk.rtl import serialize
|
|
37
|
-
from looker_sdk.rtl import transport
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class AuthSession:
|
|
41
|
-
"""AuthSession to provide automatic authentication"""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
settings: api_settings.PApiSettings,
|
|
46
|
-
transport: transport.Transport,
|
|
47
|
-
deserialize: serialize.TDeserialize,
|
|
48
|
-
api_version: str,
|
|
49
|
-
):
|
|
50
|
-
settings.is_configured()
|
|
51
|
-
self.settings = settings
|
|
52
|
-
self.api_version = api_version
|
|
53
|
-
self.sudo_token: auth_token.AuthToken = auth_token.AuthToken()
|
|
54
|
-
self.token: auth_token.AuthToken = auth_token.AuthToken()
|
|
55
|
-
self._sudo_id: Optional[int] = None
|
|
56
|
-
self.transport = transport
|
|
57
|
-
self.deserialize = deserialize
|
|
58
|
-
self.token_model = auth_token.AccessToken
|
|
59
|
-
|
|
60
|
-
def _is_authenticated(self, token: auth_token.AuthToken) -> bool:
|
|
61
|
-
"""Determines if current token is active."""
|
|
62
|
-
if not (token.access_token):
|
|
63
|
-
return False
|
|
64
|
-
return token.is_active
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def is_sudo_authenticated(self) -> bool:
|
|
68
|
-
return self._is_authenticated(self.sudo_token)
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def is_authenticated(self) -> bool:
|
|
72
|
-
return self._is_authenticated(self.token)
|
|
73
|
-
|
|
74
|
-
def _get_sudo_token(
|
|
75
|
-
self, transport_options: transport.TransportOptions
|
|
76
|
-
) -> auth_token.AuthToken:
|
|
77
|
-
"""Returns an active sudo token."""
|
|
78
|
-
if not self.is_sudo_authenticated:
|
|
79
|
-
self._login_sudo(transport_options)
|
|
80
|
-
return self.sudo_token
|
|
81
|
-
|
|
82
|
-
def _get_token(
|
|
83
|
-
self, transport_options: transport.TransportOptions
|
|
84
|
-
) -> auth_token.AuthToken:
|
|
85
|
-
"""Returns an active token."""
|
|
86
|
-
if not self.is_authenticated:
|
|
87
|
-
self._login(transport_options)
|
|
88
|
-
return self.token
|
|
89
|
-
|
|
90
|
-
def authenticate(
|
|
91
|
-
self, transport_options: transport.TransportOptions
|
|
92
|
-
) -> Dict[str, str]:
|
|
93
|
-
"""Return the Authorization header to authenticate each API call.
|
|
94
|
-
|
|
95
|
-
Expired token renewal happens automatically.
|
|
96
|
-
"""
|
|
97
|
-
if self._sudo_id:
|
|
98
|
-
token = self._get_sudo_token(transport_options)
|
|
99
|
-
else:
|
|
100
|
-
token = self._get_token(transport_options)
|
|
101
|
-
|
|
102
|
-
return {"Authorization": f"Bearer {token.access_token}"}
|
|
103
|
-
|
|
104
|
-
def login_user(
|
|
105
|
-
self,
|
|
106
|
-
sudo_id: int,
|
|
107
|
-
transport_options: Optional[transport.TransportOptions] = None,
|
|
108
|
-
) -> None:
|
|
109
|
-
"""Authenticate using settings credentials and sudo as sudo_id.
|
|
110
|
-
|
|
111
|
-
Make API calls as if authenticated as sudo_id. The sudo_id
|
|
112
|
-
token is automatically renewed when it expires. In order to
|
|
113
|
-
subsequently login_user() as another user you must first logout()
|
|
114
|
-
"""
|
|
115
|
-
if self._sudo_id is None:
|
|
116
|
-
self._sudo_id = sudo_id
|
|
117
|
-
try:
|
|
118
|
-
self._login_sudo(transport_options or {})
|
|
119
|
-
except error.SDKError:
|
|
120
|
-
self._sudo_id = None
|
|
121
|
-
raise
|
|
122
|
-
|
|
123
|
-
else:
|
|
124
|
-
if self._sudo_id != sudo_id:
|
|
125
|
-
raise error.SDKError(
|
|
126
|
-
f"Another user ({self._sudo_id}) "
|
|
127
|
-
"is already logged in. Log them out first."
|
|
128
|
-
)
|
|
129
|
-
elif not self.is_sudo_authenticated:
|
|
130
|
-
self._login_sudo(transport_options or {})
|
|
131
|
-
|
|
132
|
-
def _login(self, transport_options: transport.TransportOptions) -> None:
|
|
133
|
-
client_id = self.settings.read_config().get("client_id")
|
|
134
|
-
client_secret = self.settings.read_config().get("client_secret")
|
|
135
|
-
if not (client_id and client_secret):
|
|
136
|
-
raise error.SDKError("Required auth credentials not found.")
|
|
137
|
-
|
|
138
|
-
login = {
|
|
139
|
-
"client_id": cast(str, client_id),
|
|
140
|
-
"client_secret": cast(str, client_secret),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
serialized = urllib.parse.urlencode(login).encode("utf-8")
|
|
144
|
-
|
|
145
|
-
transport_options.setdefault("headers", {}).update(
|
|
146
|
-
{"Content-Type": "application/x-www-form-urlencoded"}
|
|
147
|
-
)
|
|
148
|
-
response = self._ok(
|
|
149
|
-
self.transport.request(
|
|
150
|
-
transport.HttpMethod.POST,
|
|
151
|
-
f"{self.settings.base_url}/api/{self.api_version}/login",
|
|
152
|
-
body=serialized,
|
|
153
|
-
transport_options=transport_options,
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# ignore type: mypy bug doesn't recognized kwarg `structure` to partial func
|
|
158
|
-
access_token = self.deserialize(
|
|
159
|
-
data=response, structure=self.token_model
|
|
160
|
-
) # type: ignore
|
|
161
|
-
assert isinstance(access_token, auth_token.AccessToken)
|
|
162
|
-
self.token = auth_token.AuthToken(access_token)
|
|
163
|
-
|
|
164
|
-
def _login_sudo(self, transport_options: transport.TransportOptions) -> None:
|
|
165
|
-
def authenticator(
|
|
166
|
-
transport_options: transport.TransportOptions,
|
|
167
|
-
) -> Dict[str, str]:
|
|
168
|
-
return {
|
|
169
|
-
"Authorization": f"Bearer {self._get_token(transport_options).access_token}"
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
response = self._ok(
|
|
173
|
-
self.transport.request(
|
|
174
|
-
transport.HttpMethod.POST,
|
|
175
|
-
f"{self.settings.base_url}/api/{self.api_version}/login/{self._sudo_id}",
|
|
176
|
-
authenticator=authenticator,
|
|
177
|
-
transport_options=transport_options,
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
# ignore type: mypy bug doesn't recognized kwarg `structure` to partial func
|
|
181
|
-
access_token = self.deserialize(
|
|
182
|
-
data=response, structure=self.token_model
|
|
183
|
-
) # type: ignore
|
|
184
|
-
assert isinstance(access_token, auth_token.AccessToken)
|
|
185
|
-
self.sudo_token = auth_token.AuthToken(access_token)
|
|
186
|
-
|
|
187
|
-
def logout(
|
|
188
|
-
self,
|
|
189
|
-
full: bool = False,
|
|
190
|
-
transport_options: Optional[transport.TransportOptions] = None,
|
|
191
|
-
) -> None:
|
|
192
|
-
"""Logout of API.
|
|
193
|
-
|
|
194
|
-
If the session is authenticated as sudo_id, logout() "undoes"
|
|
195
|
-
the sudo and deactivates that sudo_id's current token. By default
|
|
196
|
-
the current api3credential session is active at which point
|
|
197
|
-
you can continue to make API calls as the api3credential user
|
|
198
|
-
or logout(). If you want to logout completely in one step pass
|
|
199
|
-
full=True
|
|
200
|
-
"""
|
|
201
|
-
if self._sudo_id:
|
|
202
|
-
self._sudo_id = None
|
|
203
|
-
if self.is_sudo_authenticated:
|
|
204
|
-
self._logout(sudo=True, transport_options=transport_options)
|
|
205
|
-
if full:
|
|
206
|
-
self._logout(transport_options=transport_options)
|
|
207
|
-
|
|
208
|
-
elif self.is_authenticated:
|
|
209
|
-
self._logout(transport_options=transport_options)
|
|
210
|
-
|
|
211
|
-
def _logout(
|
|
212
|
-
self,
|
|
213
|
-
sudo: bool = False,
|
|
214
|
-
transport_options: Optional[transport.TransportOptions] = None,
|
|
215
|
-
) -> None:
|
|
216
|
-
|
|
217
|
-
if sudo:
|
|
218
|
-
token = self.sudo_token.access_token
|
|
219
|
-
self.sudo_token = auth_token.AuthToken()
|
|
220
|
-
else:
|
|
221
|
-
token = self.token.access_token
|
|
222
|
-
self.token = auth_token.AuthToken()
|
|
223
|
-
|
|
224
|
-
def authenticator(
|
|
225
|
-
_transport_options: transport.TransportOptions,
|
|
226
|
-
) -> Dict[str, str]:
|
|
227
|
-
return {"Authorization": f"Bearer {token}"}
|
|
228
|
-
|
|
229
|
-
self._ok(
|
|
230
|
-
self.transport.request(
|
|
231
|
-
transport.HttpMethod.DELETE,
|
|
232
|
-
f"{self.settings.base_url}/api/logout",
|
|
233
|
-
authenticator=authenticator,
|
|
234
|
-
transport_options=transport_options,
|
|
235
|
-
)
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
def _ok(self, response: transport.Response) -> str:
|
|
239
|
-
if not response.ok:
|
|
240
|
-
raise error.SDKError(response.value.decode(encoding="utf-8"))
|
|
241
|
-
return response.value.decode(encoding="utf-8")
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
class CryptoHash:
|
|
245
|
-
def secure_random(self, byte_count: int) -> str:
|
|
246
|
-
return secrets.token_urlsafe(byte_count)
|
|
247
|
-
|
|
248
|
-
def sha256_hash(self, message: str) -> str:
|
|
249
|
-
value = hashlib.sha256()
|
|
250
|
-
value.update(bytes(message, "utf8"))
|
|
251
|
-
return value.hexdigest()
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class OAuthSession(AuthSession):
|
|
255
|
-
def __init__(
|
|
256
|
-
self,
|
|
257
|
-
*,
|
|
258
|
-
settings: api_settings.PApiSettings,
|
|
259
|
-
transport: transport.Transport,
|
|
260
|
-
deserialize: serialize.TDeserialize,
|
|
261
|
-
serialize: serialize.TSerialize,
|
|
262
|
-
crypto: CryptoHash,
|
|
263
|
-
version: str,
|
|
264
|
-
):
|
|
265
|
-
super().__init__(settings, transport, deserialize, version)
|
|
266
|
-
self.crypto = crypto
|
|
267
|
-
self.serialize = serialize
|
|
268
|
-
config_data = self.settings.read_config()
|
|
269
|
-
for required in ["client_id", "redirect_uri", "looker_url"]:
|
|
270
|
-
if required not in config_data:
|
|
271
|
-
raise error.SDKError(f"Missing required configuration value {required}")
|
|
272
|
-
|
|
273
|
-
# would have prefered using setattr(self, required, ...) in loop above
|
|
274
|
-
# but mypy can't follow it
|
|
275
|
-
self.client_id = config_data["client_id"]
|
|
276
|
-
self.redirect_uri = config_data.get("redirect_uri", "")
|
|
277
|
-
self.looker_url = config_data.get("looker_url", "")
|
|
278
|
-
self.code_verifier = ""
|
|
279
|
-
|
|
280
|
-
def create_auth_code_request_url(self, scope: str, state: str) -> str:
|
|
281
|
-
self.code_verifier = self.crypto.secure_random(32)
|
|
282
|
-
code_challenge = self.crypto.sha256_hash(self.code_verifier)
|
|
283
|
-
params: Dict[str, str] = {
|
|
284
|
-
"response_type": "code",
|
|
285
|
-
"client_id": self.client_id,
|
|
286
|
-
"redirect_uri": self.redirect_uri,
|
|
287
|
-
"scope": scope,
|
|
288
|
-
"state": state,
|
|
289
|
-
"code_challenge_method": "S256",
|
|
290
|
-
"code_challenge": code_challenge,
|
|
291
|
-
}
|
|
292
|
-
path = urllib.parse.urljoin(self.looker_url, "auth")
|
|
293
|
-
query = urllib.parse.urlencode(params)
|
|
294
|
-
return f"{path}?{query}"
|
|
295
|
-
|
|
296
|
-
@attr.s(auto_attribs=True, kw_only=True)
|
|
297
|
-
class GrantTypeParams(model.Model):
|
|
298
|
-
client_id: str
|
|
299
|
-
redirect_uri: str
|
|
300
|
-
|
|
301
|
-
@attr.s(auto_attribs=True, kw_only=True)
|
|
302
|
-
class AuthCodeGrantTypeParams(GrantTypeParams):
|
|
303
|
-
code: str
|
|
304
|
-
code_verifier: str
|
|
305
|
-
grant_type: str = "authorization_code"
|
|
306
|
-
|
|
307
|
-
@attr.s(auto_attribs=True, kw_only=True)
|
|
308
|
-
class RefreshTokenGrantTypeParams(GrantTypeParams):
|
|
309
|
-
refresh_token: str
|
|
310
|
-
grant_type: str = "refresh_token"
|
|
311
|
-
|
|
312
|
-
def _request_token(
|
|
313
|
-
self,
|
|
314
|
-
grant_type: Union[AuthCodeGrantTypeParams, RefreshTokenGrantTypeParams],
|
|
315
|
-
transport_options: transport.TransportOptions,
|
|
316
|
-
) -> auth_token.AccessToken:
|
|
317
|
-
response = self.transport.request(
|
|
318
|
-
transport.HttpMethod.POST,
|
|
319
|
-
urllib.parse.urljoin(self.settings.base_url, "/api/token"),
|
|
320
|
-
body=self.serialize(api_model=grant_type), # type: ignore
|
|
321
|
-
)
|
|
322
|
-
if not response.ok:
|
|
323
|
-
raise error.SDKError(response.value.decode(encoding=response.encoding))
|
|
324
|
-
|
|
325
|
-
# ignore type: mypy bug doesn't recognized kwarg `structure` to partial func
|
|
326
|
-
return self.deserialize(
|
|
327
|
-
data=response.value, structure=self.token_model
|
|
328
|
-
) # type: ignore
|
|
329
|
-
|
|
330
|
-
def redeem_auth_code(
|
|
331
|
-
self,
|
|
332
|
-
auth_code: str,
|
|
333
|
-
code_verifier: Optional[str] = None,
|
|
334
|
-
transport_options: Optional[transport.TransportOptions] = None,
|
|
335
|
-
) -> None:
|
|
336
|
-
params = self.AuthCodeGrantTypeParams(
|
|
337
|
-
client_id=self.client_id,
|
|
338
|
-
redirect_uri=self.redirect_uri,
|
|
339
|
-
code=auth_code,
|
|
340
|
-
code_verifier=code_verifier or self.code_verifier,
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
access_token = self._request_token(params, transport_options or {})
|
|
344
|
-
self.token = auth_token.AuthToken(access_token)
|
|
345
|
-
|
|
346
|
-
def _login(self, transport_options: transport.TransportOptions) -> None:
|
|
347
|
-
params = self.RefreshTokenGrantTypeParams(
|
|
348
|
-
client_id=self.client_id,
|
|
349
|
-
redirect_uri=self.redirect_uri,
|
|
350
|
-
refresh_token=self.token.refresh_token,
|
|
351
|
-
)
|
|
352
|
-
access_token = self._request_token(params, transport_options)
|
|
353
|
-
self.token = auth_token.AuthToken(access_token)
|
looker_sdk/rtl/auth_token.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
"""AuthToken
|
|
24
|
-
"""
|
|
25
|
-
from typing import Optional, Type, Union
|
|
26
|
-
import datetime
|
|
27
|
-
|
|
28
|
-
import attr
|
|
29
|
-
|
|
30
|
-
from looker_sdk.rtl import auth_session
|
|
31
|
-
from looker_sdk.rtl import model
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Same as the Looker API access token object
|
|
35
|
-
# Re-declared here to be independent of model generation
|
|
36
|
-
@attr.s(auto_attribs=True, init=False)
|
|
37
|
-
class AccessToken(model.Model):
|
|
38
|
-
"""
|
|
39
|
-
Attributes:
|
|
40
|
-
access_token: Access Token used for API calls
|
|
41
|
-
token_type: Type of Token
|
|
42
|
-
expires_in: Number of seconds before the token expires
|
|
43
|
-
refresh_token: Refresh token which can be used to obtain a new access token
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
access_token: Optional[str] = None
|
|
47
|
-
token_type: Optional[str] = None
|
|
48
|
-
expires_in: Optional[int] = None
|
|
49
|
-
refresh_token: Optional[str] = None
|
|
50
|
-
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
*,
|
|
54
|
-
access_token: Optional[str] = None,
|
|
55
|
-
token_type: Optional[str] = None,
|
|
56
|
-
expires_in: Optional[int] = None,
|
|
57
|
-
refresh_token: Optional[str] = None,
|
|
58
|
-
):
|
|
59
|
-
self.access_token = access_token
|
|
60
|
-
self.token_type = token_type
|
|
61
|
-
self.expires_in = expires_in
|
|
62
|
-
self.refresh_token = refresh_token
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class AuthToken:
|
|
66
|
-
"""Used to instantiate or check expiry of an AccessToken object"""
|
|
67
|
-
|
|
68
|
-
def __init__(
|
|
69
|
-
self, token: Optional[AccessToken] = None,
|
|
70
|
-
):
|
|
71
|
-
self.lag_time = 10
|
|
72
|
-
self.access_token: str = ""
|
|
73
|
-
self.refresh_token: str = ""
|
|
74
|
-
self.token_type: str = ""
|
|
75
|
-
self.expires_in: int = 0
|
|
76
|
-
self.expires_at = datetime.datetime.now() + datetime.timedelta(
|
|
77
|
-
seconds=-self.lag_time
|
|
78
|
-
)
|
|
79
|
-
if token is None:
|
|
80
|
-
token = AccessToken()
|
|
81
|
-
self.set_token(token)
|
|
82
|
-
|
|
83
|
-
def set_token(self, token: AccessToken):
|
|
84
|
-
"""Assign the token and set its expiration."""
|
|
85
|
-
self.access_token = token.access_token or ""
|
|
86
|
-
if isinstance(token, AccessToken):
|
|
87
|
-
self.refresh_token = token.refresh_token or ""
|
|
88
|
-
self.token_type = token.token_type or ""
|
|
89
|
-
self.expires_in = token.expires_in or 0
|
|
90
|
-
|
|
91
|
-
lag = datetime.timedelta(seconds=-self.lag_time)
|
|
92
|
-
if token.access_token and token.expires_in:
|
|
93
|
-
lag = datetime.timedelta(seconds=token.expires_in - self.lag_time)
|
|
94
|
-
self.expires_at = datetime.datetime.now() + lag
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def is_active(self) -> bool:
|
|
98
|
-
"""True if authentication token has not timed out"""
|
|
99
|
-
if not self.expires_at:
|
|
100
|
-
return False
|
|
101
|
-
return self.expires_at > datetime.datetime.now()
|
looker_sdk/rtl/constants.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
|
|
24
|
-
RESPONSE_STRING_MODE = (
|
|
25
|
-
r"(^application/.*"
|
|
26
|
-
r"(\bjson\b|\bxml\b|\bsql\b|\bgraphql\b|\bjavascript\b|\bx-www-form-urlencoded\b)"
|
|
27
|
-
r"|^text/|.*\+xml\b|;.*charset=)"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# note: string mode must be checked first
|
|
31
|
-
RESPONSE_BINARY_MODE = r"^image/|^audio/|^video/|^font/|^application/|^multipart/"
|
looker_sdk/rtl/hooks.py
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# The MIT License (MIT)
|
|
2
|
-
#
|
|
3
|
-
# Copyright (c) 2022 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 datetime
|
|
24
|
-
import enum
|
|
25
|
-
import keyword
|
|
26
|
-
import sys
|
|
27
|
-
from typing import Type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def unstructure_hook(converter, api_model):
|
|
31
|
-
"""cattr unstructure hook
|
|
32
|
-
|
|
33
|
-
Map reserved_ words in models to correct json field names.
|
|
34
|
-
Also handle stripping None fields from dict while setting
|
|
35
|
-
EXPLICIT_NULL fields to None so that we only send null
|
|
36
|
-
in the json for fields the caller set EXPLICIT_NULL on.
|
|
37
|
-
"""
|
|
38
|
-
data = converter.unstructure_attrs_asdict(api_model)
|
|
39
|
-
for key, value in data.copy().items():
|
|
40
|
-
if value is None:
|
|
41
|
-
del data[key]
|
|
42
|
-
elif value == "EXPLICIT_NULL":
|
|
43
|
-
data[key] = None
|
|
44
|
-
# bug here: in the unittests cattrs unstructures this correctly
|
|
45
|
-
# as an enum calling .value but in the integration tests we see
|
|
46
|
-
# it doesn't for WriteCreateQueryTask.result_format for some reason
|
|
47
|
-
# Haven't been able to debug it fully, so catching and processing
|
|
48
|
-
# it here.
|
|
49
|
-
elif isinstance(value, enum.Enum):
|
|
50
|
-
data[key] = value.value
|
|
51
|
-
for reserved in keyword.kwlist:
|
|
52
|
-
if f"{reserved}_" in data:
|
|
53
|
-
data[reserved] = data.pop(f"{reserved}_")
|
|
54
|
-
return data
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
DATETIME_FMT = "%Y-%m-%dT%H:%M:%S.%f%z"
|
|
58
|
-
if sys.version_info < (3, 7):
|
|
59
|
-
from dateutil import parser
|
|
60
|
-
|
|
61
|
-
def datetime_structure_hook(
|
|
62
|
-
d: str, t: Type[datetime.datetime]
|
|
63
|
-
) -> datetime.datetime:
|
|
64
|
-
return parser.isoparse(d)
|
|
65
|
-
|
|
66
|
-
else:
|
|
67
|
-
|
|
68
|
-
def datetime_structure_hook(
|
|
69
|
-
d: str, t: Type[datetime.datetime]
|
|
70
|
-
) -> datetime.datetime:
|
|
71
|
-
return datetime.datetime.strptime(d, DATETIME_FMT)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def datetime_unstructure_hook(dt):
|
|
75
|
-
return dt.strftime(DATETIME_FMT)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def tr_data_keys(data):
|
|
79
|
-
"""Map top level json keys to model property names.
|
|
80
|
-
|
|
81
|
-
Currently this translates reserved python keywords like "from" => "from_"
|
|
82
|
-
"""
|
|
83
|
-
for reserved in keyword.kwlist:
|
|
84
|
-
if reserved in data and isinstance(data, dict):
|
|
85
|
-
data[f"{reserved}_"] = data.pop(reserved)
|
|
86
|
-
return data
|