carconnectivity-connector-seatcupra 0.1a1__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.
- 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
|