carconnectivity-connector-skoda 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.

Potentially problematic release.


This version of carconnectivity-connector-skoda might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Till Steinbach
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.1
2
+ Name: carconnectivity-connector-skoda
3
+ Version: 0.1a1
4
+ Summary: CarConnectivity connector for Skoda services
5
+ Author: Till Steinbach
6
+ License: MIT License
7
+
8
+ Copyright (c) 2021 Till Steinbach
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: Programming Language :: Python :: 3.9
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Topic :: Software Development :: Libraries
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: carconnectivity
41
+
42
+
43
+
44
+ # CarConnectivity Connector for Volkswagen Vehicles
45
+ [![GitHub sourcecode](https://img.shields.io/badge/Source-GitHub-green)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/)
46
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/latest)
47
+ [![GitHub](https://img.shields.io/github/license/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/blob/master/LICENSE)
48
+ [![GitHub issues](https://img.shields.io/github/issues/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/issues)
49
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/carconnectivity-connector-skoda?label=PyPI%20Downloads)](https://pypi.org/project/carconnectivity-connector-skoda/)
50
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/carconnectivity-connector-skoda)](https://pypi.org/project/carconnectivity-connector-skoda/)
51
+ [![Donate at PayPal](https://img.shields.io/badge/Donate-PayPal-2997d8)](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
52
+ [![Sponsor at Github](https://img.shields.io/badge/Sponsor-GitHub-28a745)](https://github.com/sponsors/tillsteinbach)
53
+
54
+ ## CarConnectivity will become the successor of [WeConnect-python](https://github.com/tillsteinbach/WeConnect-python) in 2025 with similar functionality but support for other brands beyond Volkswagen!
55
+
56
+ [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) is a python API to connect to various car services. This connector enables the integration of skoda vehicles through the WeConnect API. Look at [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) for other supported brands.
57
+
58
+ ## Configuration
59
+ In your carconnectivity.json configuration add a section for the skoda connector like this:
60
+ ```
61
+ {
62
+ "carConnectivity": {
63
+ "connectors": [
64
+ {
65
+ "type": "skoda",
66
+ "config": {
67
+ "username": "test@test.de",
68
+ "password": "testpassword123"
69
+ }
70
+ }
71
+ ]
72
+ }
73
+ }
74
+ ```
75
+ ### Credentials
76
+ If you do not want to provide your username or password inside the configuration you have to create a ".netrc" file at the appropriate location (usually this is your home folder):
77
+ ```
78
+ # For WeConnect
79
+ machine skoda
80
+ login test@test.de
81
+ password testpassword123
82
+ ```
83
+ In this case the configuration needs to look like this:
84
+ ```
85
+ {
86
+ "carConnectivity": {
87
+ "connectors": [
88
+ {
89
+ "type": "skoda",
90
+ "config": {
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ }
96
+ ```
97
+
98
+ You can also provide the location of the netrc file in the configuration.
99
+ ```
100
+ {
101
+ "carConnectivity": {
102
+ "connectors": [
103
+ {
104
+ "type": "skoda",
105
+ "config": {
106
+ "netrc": "/some/path/on/your/filesystem"
107
+ }
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ ```
113
+ The optional S-PIN needed for some commands can be provided in the account section of the netrc:
114
+ ```
115
+ # For WeConnect
116
+ machine skoda
117
+ login test@test.de
118
+ password testpassword123
119
+ account 1234
120
+ ```
@@ -0,0 +1,17 @@
1
+ carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ carconnectivity_connectors/skoda/_version.py,sha256=ZWJN-Hz-vyH2zvnF8EJz3K-yUyljRo9TBKr3g0d3oik,408
3
+ carconnectivity_connectors/skoda/capability.py,sha256=JlNEaisVYF8qWv0wNDHTaas36uIpTIQ3NVR69wesiYQ,4513
4
+ carconnectivity_connectors/skoda/connector.py,sha256=QSOPmTuT_G4AQRnPvUZY1ZgcDdp-rz5YAZ1Mfi8jnmc,40597
5
+ carconnectivity_connectors/skoda/vehicle.py,sha256=H3GRDNimMghFwFi--y9BsgoSK3pMibNf_l6SsDN6gvQ,2759
6
+ carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
8
+ carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=UhCHpJqrTPa81Y2eQlWevr8NKRse207cboF7zqyH30w,10205
9
+ carconnectivity_connectors/skoda/auth/openid_session.py,sha256=U4LucKWNDZtPtWfiKV7mdSI4y_s5lTNzt6QZT_lZka4,15886
10
+ carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Kk2QoN7IeqBhkWlvIX_1SKMZuFn9VXwtEO2Yxj2uDaA,2807
11
+ carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
12
+ carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
13
+ carconnectivity_connector_skoda-0.1a1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
14
+ carconnectivity_connector_skoda-0.1a1.dist-info/METADATA,sha256=Z7SBwmeigu7X-FlomG5L3wpypFnRB_W10LxhvasNqsc,5237
15
+ carconnectivity_connector_skoda-0.1a1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
16
+ carconnectivity_connector_skoda-0.1a1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
17
+ carconnectivity_connector_skoda-0.1a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ carconnectivity_connectors
File without changes
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.1a1'
16
+ __version_tuple__ = version_tuple = (0, 1)
File without changes
@@ -0,0 +1,141 @@
1
+
2
+ """
3
+ This module provides utility functions and classes for handling authentication and parsing HTML forms
4
+ and scripts for the Volkswagen car connectivity connector.
5
+ """
6
+ from __future__ import annotations
7
+ from typing import TYPE_CHECKING
8
+
9
+ import json
10
+ import re
11
+ from html.parser import HTMLParser
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Optional, Dict
15
+
16
+
17
+ def add_bearer_auth_header(token, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
18
+ """
19
+ Adds a Bearer token to the Authorization header.
20
+
21
+ Args:
22
+ token (str): The Bearer token to be added to the headers.
23
+ headers (Optional[Dict[str, str]]): An optional dictionary of headers to which the Authorization header will be added.
24
+ If not provided, a new dictionary will be created.
25
+
26
+ Returns:
27
+ Dict[str, str]: The headers dictionary with the added Authorization header.
28
+ """
29
+ headers = headers or {}
30
+ headers['Authorization'] = f'Bearer {token}'
31
+ return headers
32
+
33
+
34
+ class HTMLFormParser(HTMLParser):
35
+ """
36
+ A custom HTML parser to extract form data from HTML content.
37
+ """
38
+ def __init__(self, form_id) -> None:
39
+ super().__init__()
40
+ self._form_id = form_id
41
+ self._inside_form: bool = False
42
+ self.target = None
43
+ self.data = {}
44
+
45
+ def _get_attr(self, attrs, name):
46
+ for attr in attrs:
47
+ if attr[0] == name:
48
+ return attr[1]
49
+ return None
50
+
51
+ def handle_starttag(self, tag, attrs) -> None:
52
+ if self._inside_form and tag == 'input':
53
+ self.handle_input(attrs)
54
+ return
55
+
56
+ if tag == 'form' and self._get_attr(attrs, 'id') == self._form_id:
57
+ self._inside_form = True
58
+ self.target = self._get_attr(attrs, 'action')
59
+
60
+ def handle_endtag(self, tag) -> None:
61
+ if tag == 'form' and self._inside_form:
62
+ self._inside_form = False
63
+
64
+ def handle_input(self, attrs) -> None:
65
+ if not self._inside_form:
66
+ return
67
+
68
+ name = self._get_attr(attrs, 'name')
69
+ value = self._get_attr(attrs, 'value')
70
+
71
+ if name:
72
+ self.data[name] = value
73
+
74
+
75
+ class ScriptFormParser(HTMLParser):
76
+ fields: list[str] = []
77
+ targetField: str = ''
78
+
79
+ def __init__(self):
80
+ super().__init__()
81
+ self._inside_script = False
82
+ self.data = {}
83
+ self.target = None
84
+
85
+ def handle_starttag(self, tag, attrs) -> None:
86
+ if not self._inside_script and tag == 'script':
87
+ self._inside_script = True
88
+
89
+ def handle_endtag(self, tag) -> None:
90
+ if self._inside_script and tag == 'script':
91
+ self._inside_script = False
92
+
93
+ def handle_data(self, data) -> None:
94
+ if not self._inside_script:
95
+ return
96
+
97
+ match: re.Match[str] | None = re.search(r'templateModel: (.*?),\n', data)
98
+ if not match:
99
+ return
100
+
101
+ result = json.loads(match.group(1))
102
+ self.target = result.get(self.targetField, None)
103
+ self.data = {k: v for k, v in result.items() if k in self.fields}
104
+
105
+ match2 = re.search(r'csrf_token: \'(.*?)\'', data)
106
+ if match2:
107
+ self.data['_csrf'] = match2.group(1)
108
+
109
+
110
+ class CredentialsFormParser(ScriptFormParser):
111
+ fields: list[str] = ['relayState', 'hmac', 'registerCredentialsPath', 'error', 'errorCode']
112
+ targetField: str = 'postAction'
113
+
114
+
115
+ class TermsAndConditionsFormParser(ScriptFormParser):
116
+ fields: list[str] = ['relayState', 'hmac', 'countryOfResidence', 'legalDocuments']
117
+ targetField: str = 'loginUrl'
118
+
119
+ def handle_data(self, data) -> None:
120
+ if not self._inside_script:
121
+ return
122
+
123
+ super().handle_data(data)
124
+
125
+ if 'countryOfResidence' in self.data:
126
+ self.data['countryOfResidence'] = self.data['countryOfResidence'].upper()
127
+
128
+ if 'legalDocuments' not in self.data:
129
+ return
130
+
131
+ for key in self.data['legalDocuments'][0]:
132
+ # Skip unnecessary keys
133
+ if key in ('skipLink', 'declineLink', 'majorVersion', 'minorVersion', 'changeSummary'):
134
+ continue
135
+
136
+ # Move values under a new key while converting boolean values to 'yes' or 'no'
137
+ v = self.data['legalDocuments'][0][key]
138
+ self.data[f'legalDocuments[0].{key}'] = ('yes' if v else 'no') if isinstance(v, bool) else v
139
+
140
+ # Remove the original object
141
+ del self.data['legalDocuments']
@@ -0,0 +1,29 @@
1
+ """Implements a custom Retry class that allows for blacklisting certain status codes that will not retry."""
2
+ from urllib3.util.retry import Retry
3
+
4
+
5
+ class BlacklistRetry(Retry):
6
+ """
7
+ BlacklistRetry class extends the Retry class to include a blacklist of status codes that should not be retried.
8
+ """
9
+ def __init__(self, status_blacklist=None, **kwargs) -> None:
10
+ self.status_blacklist = status_blacklist
11
+ super().__init__(**kwargs)
12
+
13
+ def is_retry(self, method, status_code, has_retry_after=False) -> bool:
14
+ """
15
+ Determines if a request should be retried based on the HTTP method, status code,
16
+ and the presence of a 'Retry-After' header.
17
+
18
+ Args:
19
+ method (str): The HTTP method of the request (e.g., 'GET', 'POST').
20
+ status_code (int): The HTTP status code of the response.
21
+ has_retry_after (bool): Indicates if the response contains a 'Retry-After' header.
22
+
23
+ Returns:
24
+ bool: True if the request should be retried, False otherwise.
25
+ """
26
+ if self.status_blacklist is not None and status_code in self.status_blacklist:
27
+ return False
28
+ else:
29
+ return super().is_retry(method, status_code, has_retry_after)
@@ -0,0 +1,217 @@
1
+ """
2
+ Module implements the WeConnect Session handling.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ import json
8
+ import logging
9
+ import base64
10
+ import hashlib
11
+ import random
12
+ import string
13
+
14
+ from urllib.parse import parse_qsl, urlparse
15
+
16
+ import requests
17
+ from requests.models import CaseInsensitiveDict
18
+
19
+ from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
20
+ from oauthlib.oauth2 import InsecureTransportError
21
+ from oauthlib.oauth2 import is_secure_transport
22
+
23
+ from carconnectivity.errors import AuthenticationError, RetrievalError, TemporaryAuthenticationError
24
+
25
+ from carconnectivity_connectors.skoda.auth.openid_session import AccessType
26
+ from carconnectivity_connectors.skoda.auth.skoda_web_session import SkodaWebSession
27
+
28
+ if TYPE_CHECKING:
29
+ from typing import Tuple, Dict
30
+
31
+
32
+ LOG: logging.Logger = logging.getLogger("carconnectivity-connector-skoda")
33
+
34
+
35
+ class MySkodaSession(SkodaWebSession):
36
+ """
37
+ MySkodaSession class handles the authentication and session management for Volkswagen's WeConnect service.
38
+ """
39
+ def __init__(self, session_user, **kwargs) -> None:
40
+ super(MySkodaSession, self).__init__(client_id='7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com',
41
+ refresh_url='https://tokenrefreshservice.apps.emea.vwapps.io/refreshTokens',
42
+ scope='address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin',
43
+ redirect_uri='myskoda://redirect/login/',
44
+ session_user=session_user,
45
+ **kwargs)
46
+
47
+ self.headers = CaseInsensitiveDict({
48
+ 'user-agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
49
+ 'Chrome/74.0.3729.185 Mobile Safari/537.36',
50
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,'
51
+ 'application/signed-exchange;v=b3',
52
+ 'accept-language': 'en-US,en;q=0.9',
53
+ 'accept-encoding': 'gzip, deflate',
54
+ 'x-requested-with': 'cz.skodaauto.connect',
55
+ 'upgrade-insecure-requests': '1',
56
+ })
57
+
58
+ def login(self):
59
+ super(MySkodaSession, self).login()
60
+
61
+ verifier = "".join(random.choices(string.ascii_uppercase + string.digits, k=16))
62
+ verifier_hash = hashlib.sha256(verifier.encode("utf-8")).digest()
63
+ code_challenge = base64.b64encode(verifier_hash).decode("utf-8").replace("+", "-").replace("/", "_").rstrip("=")
64
+ # retrieve authorization URL
65
+ authorization_url = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize', prompt='login', code_challenge=code_challenge,
66
+ code_challenge_method='s256')
67
+ # perform web authentication
68
+ response = self.do_web_auth(authorization_url)
69
+ # fetch tokens from web authentication response
70
+ self.fetch_tokens('https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT',
71
+ authorization_response=response, verifier=verifier)
72
+
73
+ def refresh(self) -> None:
74
+ # refresh tokens from refresh endpoint
75
+ self.refresh_tokens(
76
+ 'https://emea.bff.cariad.digital/user-login/refresh/v1',
77
+ )
78
+
79
+ def fetch_tokens(
80
+ self,
81
+ token_url,
82
+ authorization_response,
83
+ verifier,
84
+ **_
85
+ ):
86
+ """
87
+ Fetches tokens using the given token URL using the tokens from authorization response.
88
+
89
+ Args:
90
+ token_url (str): The URL to request the tokens from.
91
+ authorization_response (str, optional): The authorization response containing the tokens. Defaults to None.
92
+ **_ : Additional keyword arguments.
93
+
94
+ Returns:
95
+ dict: A dictionary containing the fetched tokens if successful.
96
+ None: If the tokens could not be fetched.
97
+
98
+ Raises:
99
+ TemporaryAuthenticationError: If the token request fails due to a temporary WeConnect failure.
100
+ """
101
+ # take token from authorization response (those are stored in self.token now!)
102
+ self.parse_from_fragment(authorization_response)
103
+
104
+ if self.token is not None and all(key in self.token for key in ('code', 'id_token')):
105
+ # Generate json body for token request
106
+ body: str = json.dumps(
107
+ {
108
+ 'redirectUri': 'myskoda://redirect/login/',
109
+ 'code': self.token['code'],
110
+ 'verifier': verifier
111
+ })
112
+
113
+ request_headers: CaseInsensitiveDict = self.headers # pyright: ignore reportAssignmentType
114
+ request_headers['accept'] = 'application/json'
115
+ request_headers['content-type'] = 'application/json'
116
+
117
+ # request tokens from token_url
118
+ token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
119
+ access_type=AccessType.NONE) # pyright: ignore reportCallIssue
120
+ if token_response.status_code != requests.codes['ok']:
121
+ raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary WeConnect failure: {token_response.status_code}')
122
+ # parse token from response body
123
+ token = self.parse_from_body(token_response.text)
124
+ return token
125
+ return None
126
+
127
+ def parse_from_body(self, token_response, state=None):
128
+ """
129
+ Fix strange token naming before parsing it with OAuthlib.
130
+ """
131
+ try:
132
+ # Tokens are in body of response in json format
133
+ token = json.loads(token_response)
134
+ except json.decoder.JSONDecodeError as err:
135
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary WeConnect failure: json could not be decoded') from err
136
+ # Fix token keys, we want access_token instead of accessToken
137
+ if 'accessToken' in token:
138
+ token['access_token'] = token.pop('accessToken')
139
+ # Fix token keys, we want id_token instead of idToken
140
+ if 'idToken' in token:
141
+ token['id_token'] = token.pop('idToken')
142
+ # Fix token keys, we want refresh_token instead of refreshToken
143
+ if 'refreshToken' in token:
144
+ token['refresh_token'] = token.pop('refreshToken')
145
+ # generate json from fixed dict
146
+ fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
147
+ # Let OAuthlib parse the token
148
+ return super(MySkodaSession, self).parse_from_body(token_response=fixed_token_response)
149
+
150
+ def refresh_tokens(
151
+ self,
152
+ token_url,
153
+ refresh_token=None,
154
+ auth=None,
155
+ timeout=None,
156
+ headers=None,
157
+ verify=True,
158
+ proxies=None,
159
+ **_
160
+ ):
161
+ """
162
+ Refreshes the authentication tokens using the provided refresh token.
163
+ Args:
164
+ token_url (str): The URL to request new tokens from.
165
+ refresh_token (str, optional): The refresh token to use. Defaults to None.
166
+ auth (tuple, optional): Authentication credentials. Defaults to None.
167
+ timeout (float or tuple, optional): How long to wait for the server to send data before giving up. Defaults to None.
168
+ headers (dict, optional): Headers to include in the request. Defaults to None.
169
+ verify (bool, optional): Whether to verify the server's TLS certificate. Defaults to True.
170
+ proxies (dict, optional): Proxies to use for the request. Defaults to None.
171
+ **_ (dict): Additional arguments.
172
+ Raises:
173
+ ValueError: If no token endpoint is set for auto_refresh.
174
+ InsecureTransportError: If the token URL is not secure.
175
+ AuthenticationError: If the server requests new authorization.
176
+ TemporaryAuthenticationError: If the token could not be refreshed due to a temporary server failure.
177
+ RetrievalError: If the status code from the server is not recognized.
178
+ Returns:
179
+ dict: The new tokens.
180
+ """
181
+ LOG.info('Refreshing tokens')
182
+ if not token_url:
183
+ raise ValueError("No token endpoint set for auto_refresh.")
184
+
185
+ if not is_secure_transport(token_url):
186
+ raise InsecureTransportError()
187
+
188
+ # Store old refresh token in case no new one is given
189
+ refresh_token = refresh_token or self.refresh_token
190
+
191
+ if headers is None:
192
+ headers = self.headers
193
+
194
+ # Request new tokens using the refresh token
195
+ token_response = self.get(
196
+ token_url,
197
+ auth=auth,
198
+ timeout=timeout,
199
+ headers=headers,
200
+ verify=verify,
201
+ withhold_token=False, # pyright: ignore reportCallIssue
202
+ proxies=proxies,
203
+ access_type=AccessType.REFRESH # pyright: ignore reportCallIssue
204
+ )
205
+ if token_response.status_code == requests.codes['unauthorized']:
206
+ raise AuthenticationError('Refreshing tokens failed: Server requests new authorization')
207
+ elif token_response.status_code in (requests.codes['internal_server_error'], requests.codes['service_unavailable'], requests.codes['gateway_timeout']):
208
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary WeConnect failure: {tokenResponse.status_code}')
209
+ elif token_response.status_code == requests.codes['ok']:
210
+ # parse new tokens from response
211
+ self.parse_from_body(token_response.text)
212
+ if self.token is not None and "refresh_token" not in self.token:
213
+ LOG.debug("No new refresh token given. Re-using old.")
214
+ self.token["refresh_token"] = refresh_token
215
+ return self.token
216
+ else:
217
+ raise RetrievalError(f'Status Code from WeConnect while refreshing tokens was: {token_response.status_code}')