carconnectivity-connector-seatcupra 0.1a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- carconnectivity_connector_seatcupra-0.1a1.dist-info/LICENSE +21 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/METADATA +124 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/RECORD +17 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/WHEEL +5 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/top_level.txt +1 -0
- carconnectivity_connectors/seatcupra/__init__.py +0 -0
- carconnectivity_connectors/seatcupra/_version.py +21 -0
- carconnectivity_connectors/seatcupra/auth/__init__.py +0 -0
- carconnectivity_connectors/seatcupra/auth/auth_util.py +141 -0
- carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py +29 -0
- carconnectivity_connectors/seatcupra/auth/my_cupra_session.py +244 -0
- carconnectivity_connectors/seatcupra/auth/openid_session.py +440 -0
- carconnectivity_connectors/seatcupra/auth/session_manager.py +150 -0
- carconnectivity_connectors/seatcupra/auth/vw_web_session.py +239 -0
- carconnectivity_connectors/seatcupra/charging.py +74 -0
- carconnectivity_connectors/seatcupra/connector.py +686 -0
- carconnectivity_connectors/seatcupra/ui/connector_ui.py +39 -0
@@ -0,0 +1,440 @@
|
|
1
|
+
"""Implements a session class that handles OpenID authentication."""
|
2
|
+
from __future__ import annotations
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
from enum import Enum, auto
|
6
|
+
import time
|
7
|
+
import logging
|
8
|
+
import requests
|
9
|
+
from jwt import JWT
|
10
|
+
from datetime import datetime, timezone
|
11
|
+
|
12
|
+
from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_nonce, generate_token
|
13
|
+
from oauthlib.oauth2.rfc6749.parameters import parse_authorization_code_response, parse_token_response, prepare_grant_uri
|
14
|
+
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError, TokenExpiredError, MissingTokenError
|
15
|
+
from oauthlib.oauth2.rfc6749.utils import is_secure_transport
|
16
|
+
|
17
|
+
from requests.adapters import HTTPAdapter
|
18
|
+
|
19
|
+
from carconnectivity.errors import AuthenticationError, RetrievalError
|
20
|
+
|
21
|
+
from carconnectivity_connectors.seatcupra.auth.auth_util import add_bearer_auth_header
|
22
|
+
from carconnectivity_connectors.seatcupra.auth.helpers.blacklist_retry import BlacklistRetry
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from typing import Dict
|
26
|
+
|
27
|
+
LOG = logging.getLogger("carconnectivity.connectors.seatcupra.auth")
|
28
|
+
|
29
|
+
|
30
|
+
class AccessType(Enum):
|
31
|
+
"""
|
32
|
+
Enum representing different types of access tokens used in the authentication process.
|
33
|
+
|
34
|
+
Attributes:
|
35
|
+
NONE (auto): No access token.
|
36
|
+
ACCESS (auto): Access token used for accessing resources.
|
37
|
+
ID (auto): ID token used for identifying the user.
|
38
|
+
REFRESH (auto): Refresh token used for obtaining new access tokens.
|
39
|
+
"""
|
40
|
+
NONE = auto()
|
41
|
+
ACCESS = auto()
|
42
|
+
ID = auto()
|
43
|
+
REFRESH = auto()
|
44
|
+
|
45
|
+
|
46
|
+
class OpenIDSession(requests.Session):
|
47
|
+
"""
|
48
|
+
OpenIDSession is a subclass of requests.Session that handles OpenID Connect authentication.
|
49
|
+
"""
|
50
|
+
def __init__(self, client_id=None, redirect_uri=None, refresh_url=None, scope=None, token=None, metadata=None, state=None, timeout=None,
|
51
|
+
force_relogin_after=None, **kwargs) -> None:
|
52
|
+
super(OpenIDSession, self).__init__(**kwargs)
|
53
|
+
self.client_id = client_id
|
54
|
+
self.redirect_uri = redirect_uri
|
55
|
+
self.refresh_url = refresh_url
|
56
|
+
self.scope = scope
|
57
|
+
self.state: str = state or generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET)
|
58
|
+
|
59
|
+
self.timeout = timeout
|
60
|
+
self._token = token
|
61
|
+
self.metadata = metadata or {}
|
62
|
+
self.last_login = None
|
63
|
+
self._force_relogin_after = force_relogin_after
|
64
|
+
|
65
|
+
self._retries: bool | int = False
|
66
|
+
|
67
|
+
@property
|
68
|
+
def force_relogin_after(self):
|
69
|
+
"""
|
70
|
+
Get the number of seconds after which a forced re-login is required.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
Number of seconds until a forced re-login is required.
|
74
|
+
"""
|
75
|
+
return self._force_relogin_after
|
76
|
+
|
77
|
+
@force_relogin_after.setter
|
78
|
+
def force_relogin_after(self, new_force_relogin_after_value):
|
79
|
+
"""
|
80
|
+
Sets the time after which a forced re-login should occur.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
new_force_relogin_after_value (float or None): The new value for the forced re-login time.
|
84
|
+
If None, no forced re-login will be set.
|
85
|
+
"""
|
86
|
+
self._force_relogin_after = new_force_relogin_after_value
|
87
|
+
if new_force_relogin_after_value is not None and self.last_login is None:
|
88
|
+
self.last_login = time.time()
|
89
|
+
|
90
|
+
@property
|
91
|
+
def retries(self) -> bool | int:
|
92
|
+
"""
|
93
|
+
Get the number of retries.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
bool | int: The number of retries. It can be a boolean or an integer.
|
97
|
+
"""
|
98
|
+
return self._retries
|
99
|
+
|
100
|
+
@retries.setter
|
101
|
+
def retries(self, new_retries_value):
|
102
|
+
"""
|
103
|
+
Set the number of retries for the session and configure retry behavior.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
new_retries_value (int): The new number of retries to set. If provided,
|
107
|
+
configures the session to retry on internal server
|
108
|
+
errors (HTTP status code 500) and blacklist status
|
109
|
+
code 429 with a backoff factor of 0.1.
|
110
|
+
|
111
|
+
"""
|
112
|
+
self._retries = new_retries_value
|
113
|
+
if new_retries_value:
|
114
|
+
# Retry on internal server error (500)
|
115
|
+
retries = BlacklistRetry(total=new_retries_value,
|
116
|
+
backoff_factor=0.1,
|
117
|
+
status_forcelist=[500],
|
118
|
+
status_blacklist=[429],
|
119
|
+
raise_on_status=False)
|
120
|
+
self.mount('https://', HTTPAdapter(max_retries=retries))
|
121
|
+
|
122
|
+
@property
|
123
|
+
def token(self):
|
124
|
+
"""
|
125
|
+
Retrieve the current token.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
str: The current token.
|
129
|
+
"""
|
130
|
+
return self._token
|
131
|
+
|
132
|
+
@token.setter
|
133
|
+
def token(self, new_token):
|
134
|
+
"""
|
135
|
+
Updates the current token with a new token and sets expiration details if not provided.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
new_token (dict): The new token to be set. If the token does not contain 'expires_in',
|
139
|
+
it will be set to the same value as the current token's 'expires_in'
|
140
|
+
or default to 3600 seconds. If 'expires_in' is provided but 'expires_at'
|
141
|
+
is not, 'expires_at' will be calculated based on the current time.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
None
|
145
|
+
"""
|
146
|
+
if new_token is not None:
|
147
|
+
# If new token e.g. after refresh is missing expires_in we assume it is the same than before
|
148
|
+
if 'expires_in' not in new_token:
|
149
|
+
if self._token is not None and 'expires_in' in self._token:
|
150
|
+
new_token['expires_in'] = self._token['expires_in']
|
151
|
+
else:
|
152
|
+
if 'id_token' in new_token:
|
153
|
+
jwt_instance = JWT()
|
154
|
+
meta_data = jwt_instance.decode(new_token['id_token'], do_verify=False)
|
155
|
+
if 'exp' in meta_data:
|
156
|
+
new_token['expires_at'] = meta_data['exp']
|
157
|
+
expires_at = datetime.fromtimestamp(meta_data['exp'], tz=timezone.utc)
|
158
|
+
new_token['expires_in'] = (expires_at - datetime.now(tz=timezone.utc)).total_seconds()
|
159
|
+
else:
|
160
|
+
new_token['expires_in'] = 3600
|
161
|
+
else:
|
162
|
+
new_token['expires_in'] = 3600
|
163
|
+
# If expires_in is set and expires_at is not set we calculate expires_at from expires_in using the current time
|
164
|
+
if 'expires_in' in new_token and 'expires_at' not in new_token:
|
165
|
+
new_token['expires_at'] = time.time() + int(new_token.get('expires_in'))
|
166
|
+
self._token = new_token
|
167
|
+
|
168
|
+
@property
|
169
|
+
def access_token(self):
|
170
|
+
"""
|
171
|
+
Retrieve the access token from the stored token.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
str: The access token if it exists in the stored token, otherwise None.
|
175
|
+
"""
|
176
|
+
if self._token is not None and 'access_token' in self._token:
|
177
|
+
return self._token.get('access_token')
|
178
|
+
return None
|
179
|
+
|
180
|
+
@access_token.setter
|
181
|
+
def access_token(self, new_access_token):
|
182
|
+
"""
|
183
|
+
Sets a new access token.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
new_access_token (str): The new access token to be set.
|
187
|
+
"""
|
188
|
+
if self._token is None:
|
189
|
+
self._token = {}
|
190
|
+
self._token['access_token'] = new_access_token
|
191
|
+
|
192
|
+
@property
|
193
|
+
def refresh_token(self):
|
194
|
+
"""
|
195
|
+
Retrieves the refresh token from the stored token.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
str or None: The refresh token if it exists in the stored token, otherwise None.
|
199
|
+
"""
|
200
|
+
if self._token is not None and 'refresh_token' in self._token:
|
201
|
+
return self._token.get('refresh_token')
|
202
|
+
return None
|
203
|
+
|
204
|
+
@property
|
205
|
+
def id_token(self):
|
206
|
+
"""
|
207
|
+
Retrieve the ID token from the stored token.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
str or None: The ID token if it exists in the stored token, otherwise None.
|
211
|
+
"""
|
212
|
+
if self._token is not None and 'id_token' in self._token:
|
213
|
+
return self._token.get('id_token')
|
214
|
+
return None
|
215
|
+
|
216
|
+
@property
|
217
|
+
def token_type(self):
|
218
|
+
"""
|
219
|
+
Retrieve the token type from the stored token.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
str: The type of the token if available, otherwise None.
|
223
|
+
"""
|
224
|
+
if self._token is not None and 'token_type' in self._token:
|
225
|
+
return self._token.get('token_type')
|
226
|
+
return None
|
227
|
+
|
228
|
+
@property
|
229
|
+
def expires_in(self):
|
230
|
+
"""
|
231
|
+
Retrieve the expiration time of the current token.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
int or None: The number of seconds until the token expires if available,
|
235
|
+
otherwise None.
|
236
|
+
"""
|
237
|
+
if self._token is not None and 'expires_in' in self._token:
|
238
|
+
return self._token.get('expires_in')
|
239
|
+
return None
|
240
|
+
|
241
|
+
@property
|
242
|
+
def expires_at(self):
|
243
|
+
"""
|
244
|
+
Retrieve the expiration time of the current token.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
int or None: The expiration time of the token in epoch time if available,
|
248
|
+
otherwise None.
|
249
|
+
"""
|
250
|
+
if self._token is not None and 'expires_at' in self._token:
|
251
|
+
return self._token.get('expires_at')
|
252
|
+
return None
|
253
|
+
|
254
|
+
@property
|
255
|
+
def authorized(self):
|
256
|
+
"""
|
257
|
+
Check if the session is authorized.
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
bool: True if the session has a valid access token, False otherwise.
|
261
|
+
"""
|
262
|
+
return bool(self.access_token)
|
263
|
+
|
264
|
+
@property
|
265
|
+
def expired(self):
|
266
|
+
"""
|
267
|
+
Check if the session has expired.
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
bool: True if the session has expired, False otherwise.
|
271
|
+
"""
|
272
|
+
return self.expires_at is not None and self.expires_at < time.time()
|
273
|
+
|
274
|
+
@property
|
275
|
+
def user_id(self):
|
276
|
+
"""
|
277
|
+
Retrieve the user ID from the metadata.
|
278
|
+
"""
|
279
|
+
if 'userId' in self.metadata:
|
280
|
+
return self.metadata['userId']
|
281
|
+
return None
|
282
|
+
|
283
|
+
@user_id.setter
|
284
|
+
def user_id(self, new_user_id):
|
285
|
+
"""
|
286
|
+
Sets the user ID in the metadata.
|
287
|
+
"""
|
288
|
+
self.metadata['userId'] = new_user_id
|
289
|
+
|
290
|
+
def login(self):
|
291
|
+
"""
|
292
|
+
Logs in the user, needs to be implemetned in subclass
|
293
|
+
|
294
|
+
This method sets the `last_login` attribute to the current time.
|
295
|
+
"""
|
296
|
+
self.last_login = time.time()
|
297
|
+
|
298
|
+
def refresh(self):
|
299
|
+
"""
|
300
|
+
Refresh the current session, needs to be implemetned in subclass
|
301
|
+
|
302
|
+
This method is intended to refresh the authentication session.
|
303
|
+
Currently, it is not implemented and does not perform any actions.
|
304
|
+
"""
|
305
|
+
|
306
|
+
def authorization_url(self, url, state=None, **kwargs):
|
307
|
+
"""
|
308
|
+
Generates the authorization URL for the OpenID Connect flow.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
url (str): The base URL for the authorization endpoint.
|
312
|
+
state (str, optional): An optional state parameter to maintain state between the request and callback. Defaults to None.
|
313
|
+
**kwargs: Additional parameters to include in the authorization URL.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
str: The complete authorization URL with the necessary query parameters.
|
317
|
+
"""
|
318
|
+
state = state or self.state
|
319
|
+
auth_url = prepare_grant_uri(uri=url, client_id=self.client_id, redirect_uri=self.redirect_uri, response_type='code id_token token', scope=self.scope,
|
320
|
+
state=state, nonce=generate_nonce(), **kwargs)
|
321
|
+
return auth_url
|
322
|
+
|
323
|
+
def parse_from_fragment(self, authorization_response, state=None):
|
324
|
+
"""
|
325
|
+
Parses the authorization response fragment and extracts the token.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
authorization_response (str): The authorization response fragment containing the token.
|
329
|
+
state (str, optional): The state parameter to validate the response. Defaults to None.
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
dict: The parsed token information.
|
333
|
+
"""
|
334
|
+
state = state or self.state
|
335
|
+
self.token = parse_authorization_code_response(authorization_response, state=state)
|
336
|
+
return self.token
|
337
|
+
|
338
|
+
def parse_from_body(self, token_response, state=None):
|
339
|
+
"""
|
340
|
+
Parse the JSON token response body into a dict.
|
341
|
+
"""
|
342
|
+
del state
|
343
|
+
self.token = parse_token_response(token_response, scope=self.scope)
|
344
|
+
return self.token
|
345
|
+
|
346
|
+
def request( # noqa: C901
|
347
|
+
self,
|
348
|
+
method,
|
349
|
+
url,
|
350
|
+
data=None,
|
351
|
+
headers=None,
|
352
|
+
timeout=None,
|
353
|
+
withhold_token=False,
|
354
|
+
access_type=AccessType.ACCESS,
|
355
|
+
token=None,
|
356
|
+
**kwargs
|
357
|
+
) -> requests.Response:
|
358
|
+
"""Intercept all requests and add the OAuth 2 token if present."""
|
359
|
+
if not is_secure_transport(url):
|
360
|
+
raise InsecureTransportError()
|
361
|
+
if access_type != AccessType.NONE and not withhold_token:
|
362
|
+
if self.force_relogin_after is not None and self.last_login is not None and (self.last_login + self.force_relogin_after) < time.time():
|
363
|
+
LOG.debug("Forced new login after %ds", self.force_relogin_after)
|
364
|
+
self.login()
|
365
|
+
try:
|
366
|
+
url, headers, data = self.add_token(url, body=data, headers=headers, access_type=access_type, token=token)
|
367
|
+
# Attempt to retrieve and save new access token if expired
|
368
|
+
except TokenExpiredError:
|
369
|
+
LOG.info('Token expired')
|
370
|
+
self.access_token = None
|
371
|
+
try:
|
372
|
+
self.refresh()
|
373
|
+
except AuthenticationError:
|
374
|
+
self.login()
|
375
|
+
except TokenExpiredError:
|
376
|
+
self.login()
|
377
|
+
except MissingTokenError:
|
378
|
+
self.login()
|
379
|
+
except RetrievalError:
|
380
|
+
LOG.error('Retrieval Error while refreshing token. Probably the token was invalidated. Trying to do a new login instead.')
|
381
|
+
self.login()
|
382
|
+
url, headers, data = self.add_token(url, body=data, headers=headers, access_type=access_type, token=token)
|
383
|
+
except MissingTokenError:
|
384
|
+
LOG.info('Missing token, need new login')
|
385
|
+
self.login()
|
386
|
+
url, headers, data = self.add_token(url, body=data, headers=headers, access_type=access_type, token=token)
|
387
|
+
|
388
|
+
if timeout is None:
|
389
|
+
timeout = self.timeout
|
390
|
+
|
391
|
+
return super(OpenIDSession, self).request(
|
392
|
+
method, url, headers=headers, data=data, timeout=timeout, **kwargs
|
393
|
+
)
|
394
|
+
|
395
|
+
def add_token(self, uri, body=None, headers=None, access_type=AccessType.ACCESS, token=None, **_): # pylint: disable=too-many-arguments
|
396
|
+
"""
|
397
|
+
Adds an authorization token to the request headers based on the specified access type.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
uri (str): The URI to which the request is being made.
|
401
|
+
body (Optional[Any]): The body of the request. Defaults to None.
|
402
|
+
headers (Optional[Dict[str, str]]): The headers of the request. Defaults to None.
|
403
|
+
access_type (AccessType): The type of access token to use (ID, REFRESH, or ACCESS). Defaults to AccessType.ACCESS.
|
404
|
+
token (Optional[str]): The token to use. If None, the method will use the appropriate token based on the access_type. Defaults to None.
|
405
|
+
**_ (Any): Additional keyword arguments.
|
406
|
+
|
407
|
+
Raises:
|
408
|
+
InsecureTransportError: If the URI does not use a secure transport (HTTPS).
|
409
|
+
MissingTokenError: If the required token (ID, REFRESH, or ACCESS) is missing.
|
410
|
+
TokenExpiredError: If the access token has expired.
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
Tuple[str, Dict[str, str], Optional[Any]]: The URI, updated headers with the authorization token, and the body of the request.
|
414
|
+
"""
|
415
|
+
# Check if the URI uses a secure transport
|
416
|
+
if not is_secure_transport(uri):
|
417
|
+
raise InsecureTransportError()
|
418
|
+
|
419
|
+
# Only add token if it is not explicitly withheld
|
420
|
+
if token is None:
|
421
|
+
if access_type == AccessType.ID:
|
422
|
+
if not self.id_token:
|
423
|
+
raise MissingTokenError(description="Missing id token.")
|
424
|
+
token = self.id_token
|
425
|
+
elif access_type == AccessType.REFRESH:
|
426
|
+
if not self.refresh_token:
|
427
|
+
raise MissingTokenError(description="Missing refresh token.")
|
428
|
+
token = self.refresh_token
|
429
|
+
else:
|
430
|
+
if not self.authorized:
|
431
|
+
self.login()
|
432
|
+
if not self.access_token:
|
433
|
+
raise MissingTokenError(description="Missing access token.")
|
434
|
+
if self.expired:
|
435
|
+
raise TokenExpiredError()
|
436
|
+
token = self.access_token
|
437
|
+
|
438
|
+
return_headers: Dict[str, str] = add_bearer_auth_header(token, headers)
|
439
|
+
|
440
|
+
return (uri, return_headers, body)
|
@@ -0,0 +1,150 @@
|
|
1
|
+
"""Module implementing the SessionManager class."""
|
2
|
+
from __future__ import annotations
|
3
|
+
from typing import TYPE_CHECKING, Tuple
|
4
|
+
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
import hashlib
|
8
|
+
|
9
|
+
import logging
|
10
|
+
|
11
|
+
from carconnectivity_connectors.seatcupra.auth.my_cupra_session import MyCupraSession
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from typing import Dict, Any
|
15
|
+
from carconnectivity_connectors.seatcupra.auth.vw_web_session import VWWebSession
|
16
|
+
|
17
|
+
LOG = logging.getLogger("carconnectivity.connectors.seatcupra.auth")
|
18
|
+
|
19
|
+
|
20
|
+
class SessionUser():
|
21
|
+
"""
|
22
|
+
A class to represent a session user with a username and password.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
----------
|
26
|
+
username : str
|
27
|
+
The username of the session user.
|
28
|
+
password : str
|
29
|
+
The password of the session user.
|
30
|
+
|
31
|
+
Methods:
|
32
|
+
-------
|
33
|
+
__str__():
|
34
|
+
Returns a string representation of the session user in the format 'username:password'.
|
35
|
+
"""
|
36
|
+
def __init__(self, username: str, password: str) -> None:
|
37
|
+
self.username: str = username
|
38
|
+
self.password: str = password
|
39
|
+
|
40
|
+
def __str__(self) -> str:
|
41
|
+
return f'{self.username}:{self.password}'
|
42
|
+
|
43
|
+
|
44
|
+
class Service(Enum):
|
45
|
+
"""
|
46
|
+
An enumeration representing different services.
|
47
|
+
|
48
|
+
Attributes:
|
49
|
+
MY_CUPRA (str): Represents the 'MyCupra' service.
|
50
|
+
|
51
|
+
Methods:
|
52
|
+
__str__() -> str: Returns the string representation of the service.
|
53
|
+
"""
|
54
|
+
MY_CUPRA = 'MyCupra'
|
55
|
+
|
56
|
+
def __str__(self) -> str:
|
57
|
+
return self.value
|
58
|
+
|
59
|
+
|
60
|
+
class SessionManager():
|
61
|
+
"""
|
62
|
+
Manages sessions for different services and users, handling token storage and caching.
|
63
|
+
"""
|
64
|
+
def __init__(self, tokenstore: Dict[str, Any], cache: Dict[str, Any]) -> None:
|
65
|
+
self.tokenstore: Dict[str, Any] = tokenstore
|
66
|
+
self.cache: Dict[str, Any] = cache
|
67
|
+
self.sessions: Dict[Tuple[Service, SessionUser], VWWebSession] = {}
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def generate_hash(service: Service, session_user: SessionUser) -> str:
|
71
|
+
"""
|
72
|
+
Generates a SHA-512 hash for the given service and session user.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
service (Service): The service for which the hash is being generated.
|
76
|
+
session_user (SessionUser): The session user for which the hash is being generated.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
str: The generated SHA-512 hash as a hexadecimal string.
|
80
|
+
"""
|
81
|
+
hash_str: str = service.value + str(session_user)
|
82
|
+
return hashlib.sha512(hash_str.encode()).hexdigest()
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
def generate_identifier(service: Service, session_user: SessionUser) -> str:
|
86
|
+
"""
|
87
|
+
Generate a unique identifier for a given service and session user.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
service (Service): The service for which the identifier is being generated.
|
91
|
+
session_user (SessionUser): The session user for whom the identifier is being generated.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
str: A unique identifier string.
|
95
|
+
"""
|
96
|
+
return 'CarConnectivity-connector-cupra:' + SessionManager.generate_hash(service, session_user)
|
97
|
+
|
98
|
+
def get_session(self, service: Service, session_user: SessionUser) -> VWWebSession:
|
99
|
+
"""
|
100
|
+
Retrieves a session for the given service and session user. If a session already exists in the sessions cache,
|
101
|
+
it is returned. Otherwise, a new session is created using the token, metadata, and cache from the tokenstore
|
102
|
+
and cache if available.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
service (Service): The service for which the session is being requested.
|
106
|
+
session_user (SessionUser): The user for whom the session is being requested.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Session: The session object for the given service and session user.
|
110
|
+
"""
|
111
|
+
session = None
|
112
|
+
if (service, session_user) in self.sessions:
|
113
|
+
return self.sessions[(service, session_user)]
|
114
|
+
|
115
|
+
identifier: str = SessionManager.generate_identifier(service, session_user)
|
116
|
+
token = None
|
117
|
+
cache = {}
|
118
|
+
metadata = {}
|
119
|
+
|
120
|
+
if identifier in self.tokenstore:
|
121
|
+
if 'token' in self.tokenstore[identifier]:
|
122
|
+
LOG.info('Reusing tokens from previous session')
|
123
|
+
token = self.tokenstore[identifier]['token']
|
124
|
+
if 'metadata' in self.tokenstore[identifier]:
|
125
|
+
metadata = self.tokenstore[identifier]['metadata']
|
126
|
+
if identifier in self.cache:
|
127
|
+
cache = self.cache[identifier]
|
128
|
+
|
129
|
+
if service == Service.MY_CUPRA:
|
130
|
+
session = MyCupraSession(session_user=session_user, token=token, metadata=metadata, cache=cache)
|
131
|
+
else:
|
132
|
+
raise ValueError(f"Unsupported service: {service}")
|
133
|
+
|
134
|
+
self.sessions[(service, session_user)] = session
|
135
|
+
return session
|
136
|
+
|
137
|
+
def persist(self) -> None:
|
138
|
+
"""
|
139
|
+
Persist the current sessions into the token store and cache.
|
140
|
+
|
141
|
+
This method iterates over the sessions and stores each session's token and metadata
|
142
|
+
in the token store using a generated identifier. It also stores the session's cache
|
143
|
+
in the cache.
|
144
|
+
"""
|
145
|
+
for (service, user), session in self.sessions.items():
|
146
|
+
identifier: str = SessionManager.generate_identifier(service, user)
|
147
|
+
self.tokenstore[identifier] = {}
|
148
|
+
self.tokenstore[identifier]['token'] = session.token
|
149
|
+
self.tokenstore[identifier]['metadata'] = session.metadata
|
150
|
+
self.cache[identifier] = session.cache
|