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.
@@ -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,124 @@
1
+ Metadata-Version: 2.2
2
+ Name: carconnectivity-connector-seatcupra
3
+ Version: 0.1a1
4
+ Summary: CarConnectivity connector for Seat and Cupra 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.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: carconnectivity>=0.3
41
+ Requires-Dist: oauthlib~=3.2.2
42
+ Requires-Dist: requests~=2.32.3
43
+ Requires-Dist: jwt~=1.3.1
44
+
45
+
46
+
47
+ # CarConnectivity Connector for Seat and Cupra Vehicles
48
+ [![GitHub sourcecode](https://img.shields.io/badge/Source-GitHub-green)](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/)
49
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/tillsteinbach/CarConnectivity-connector-seatcupra)](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/releases/latest)
50
+ [![GitHub](https://img.shields.io/github/license/tillsteinbach/CarConnectivity-connector-seatcupra)](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/blob/master/LICENSE)
51
+ [![GitHub issues](https://img.shields.io/github/issues/tillsteinbach/CarConnectivity-connector-seatcupra)](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/issues)
52
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/carconnectivity-connector-seatcupra?label=PyPI%20Downloads)](https://pypi.org/project/carconnectivity-connector-seatcupra/)
53
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/carconnectivity-connector-seatcupra)](https://pypi.org/project/carconnectivity-connector-seatcupra/)
54
+ [![Donate at PayPal](https://img.shields.io/badge/Donate-PayPal-2997d8)](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
55
+ [![Sponsor at Github](https://img.shields.io/badge/Sponsor-GitHub-28a745)](https://github.com/sponsors/tillsteinbach)
56
+
57
+
58
+ ## Due to lack of access to a Cupra car the development of this conenctor is currently stuck. If you want to help me with access to your account, please contact me!
59
+
60
+ [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) is a python API to connect to various car services. This connector enables the integration of seat and cupra vehicles through the MyCupra API. Look at [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) for other supported brands.
61
+
62
+ ## Configuration
63
+ In your carconnectivity.json configuration add a section for the seatcupra connector like this:
64
+ ```
65
+ {
66
+ "carConnectivity": {
67
+ "connectors": [
68
+ {
69
+ "type": "seatcupra",
70
+ "config": {
71
+ "username": "test@test.de",
72
+ "password": "testpassword123"
73
+ }
74
+ }
75
+ ]
76
+ }
77
+ }
78
+ ```
79
+ ### Credentials
80
+ 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):
81
+ ```
82
+ # For MyCupra
83
+ machine seatcupra
84
+ login test@test.de
85
+ password testpassword123
86
+ ```
87
+ In this case the configuration needs to look like this:
88
+ ```
89
+ {
90
+ "carConnectivity": {
91
+ "connectors": [
92
+ {
93
+ "type": "seatcupra",
94
+ "config": {
95
+ }
96
+ }
97
+ ]
98
+ }
99
+ }
100
+ ```
101
+
102
+ You can also provide the location of the netrc file in the configuration.
103
+ ```
104
+ {
105
+ "carConnectivity": {
106
+ "connectors": [
107
+ {
108
+ "type": "seatcupra",
109
+ "config": {
110
+ "netrc": "/some/path/on/your/filesystem"
111
+ }
112
+ }
113
+ ]
114
+ }
115
+ }
116
+ ```
117
+ The optional S-PIN needed for some commands can be provided in the account section of the netrc:
118
+ ```
119
+ # For MyCupra
120
+ machine seatcupra
121
+ login test@test.de
122
+ password testpassword123
123
+ account 1234
124
+ ```
@@ -0,0 +1,17 @@
1
+ carconnectivity_connectors/seatcupra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ carconnectivity_connectors/seatcupra/_version.py,sha256=_BIT6nGqXqfHgeHw9NwtWXgZnV8T_7fSo6lEqHbnf88,508
3
+ carconnectivity_connectors/seatcupra/charging.py,sha256=kcCJJddZxUXFoayYMpq3lzdnrPp5yexnGBfDB-zQrmE,3336
4
+ carconnectivity_connectors/seatcupra/connector.py,sha256=nyfIjft7pXPfQojU93BfNdXQ-fSmU7DFh9Xqw4ujfd4,44278
5
+ carconnectivity_connectors/seatcupra/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ carconnectivity_connectors/seatcupra/auth/auth_util.py,sha256=Y81h8fGOMSMgPtE4wI_TI9WgE_s43uaPjRLBBINhj4g,4433
7
+ carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=iK5SlankZqaeneC3SNad8nHHcrP0tTmYxToI_9cqwlo,10744
8
+ carconnectivity_connectors/seatcupra/auth/openid_session.py,sha256=dA0vE2YuckkMPeqJo2dEI0h8_XfohdCgdGkTyshPF7Q,16858
9
+ carconnectivity_connectors/seatcupra/auth/session_manager.py,sha256=NizIuY-pvkVBSwqYwFHKtjTU_02Nj4vMgjD_FjqCY6c,5377
10
+ carconnectivity_connectors/seatcupra/auth/vw_web_session.py,sha256=hgsCdXugVnSgvLta4hBNtoNgMhAA83paAYO2fUOOFyM,10657
11
+ carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
12
+ carconnectivity_connectors/seatcupra/ui/connector_ui.py,sha256=SNYnlcGJpbWhuLiIHD2l6H9IfSiMz3IgmvXsdossDnE,1412
13
+ carconnectivity_connector_seatcupra-0.1a1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
14
+ carconnectivity_connector_seatcupra-0.1a1.dist-info/METADATA,sha256=eG-9qfRLgK8bseYbWLxo_brIPtu02IrgXjW5ZiimCc0,5384
15
+ carconnectivity_connector_seatcupra-0.1a1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
16
+ carconnectivity_connector_seatcupra-0.1a1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
17
+ carconnectivity_connector_seatcupra-0.1a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ carconnectivity_connectors
File without changes
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.1a1'
21
+ __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 seatcupra 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,244 @@
1
+ """
2
+ Module implements the MyCupra Session handling.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ import json
8
+ import logging
9
+ import secrets
10
+
11
+ from urllib.parse import parse_qsl, urlparse
12
+
13
+ import requests
14
+ from requests.models import CaseInsensitiveDict
15
+
16
+ from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
17
+ from oauthlib.oauth2 import InsecureTransportError
18
+ from oauthlib.oauth2 import is_secure_transport
19
+
20
+ from carconnectivity.errors import AuthenticationError, RetrievalError, TemporaryAuthenticationError
21
+
22
+ from carconnectivity_connectors.seatcupra.auth.openid_session import AccessType
23
+ from carconnectivity_connectors.seatcupra.auth.vw_web_session import VWWebSession
24
+
25
+ if TYPE_CHECKING:
26
+ from typing import Tuple, Dict
27
+
28
+
29
+ LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.seatcupra.auth")
30
+
31
+
32
+ class MyCupraSession(VWWebSession):
33
+ """
34
+ MyCupraSession class handles the authentication and session management for Cupras's MyCupra service.
35
+ """
36
+ def __init__(self, session_user, **kwargs) -> None:
37
+ super(MyCupraSession, self).__init__(client_id='3c756d46-f1ba-4d78-9f9a-cff0d5292d51@apps_vw-dilab_com',
38
+ refresh_url='https://identity.vwgroup.io/oidc/v1/token',
39
+ scope='openid profile nickname birthdate phone',
40
+ redirect_uri='cupra://oauth-callback',
41
+ state=None,
42
+ session_user=session_user,
43
+ **kwargs)
44
+
45
+ self.headers = CaseInsensitiveDict({
46
+ 'accept': '*/*',
47
+ 'content-type': 'application/json',
48
+ 'user-agent': 'CUPRAApp%20-%20Store/20220503 CFNetwork/1333.0.4 Darwin/21.5.0',
49
+ 'accept-language': 'de-de',
50
+ 'accept-encoding': 'gzip, deflate, br'
51
+ })
52
+
53
+ def login(self):
54
+ super(MyCupraSession, self).login()
55
+ # retrieve authorization URL
56
+ authorization_url_str: str = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize')
57
+ # perform web authentication
58
+ response = self.do_web_auth(authorization_url_str)
59
+ # fetch tokens from web authentication response
60
+ self.fetch_tokens('https://identity.vwgroup.io/oidc/v1/token',
61
+ authorization_response=response)
62
+
63
+ def refresh(self) -> None:
64
+ # refresh tokens from refresh endpoint
65
+ self.refresh_tokens(
66
+ 'https://identity.vwgroup.io/oidc/v1/token',
67
+ )
68
+
69
+ def fetch_tokens(
70
+ self,
71
+ token_url,
72
+ authorization_response=None,
73
+ **_
74
+ ):
75
+ """
76
+ Fetches tokens using the given token URL using the tokens from authorization response.
77
+
78
+ Args:
79
+ token_url (str): The URL to request the tokens from.
80
+ authorization_response (str, optional): The authorization response containing the tokens. Defaults to None.
81
+ **_ : Additional keyword arguments.
82
+
83
+ Returns:
84
+ dict: A dictionary containing the fetched tokens if successful.
85
+ None: If the tokens could not be fetched.
86
+
87
+ Raises:
88
+ TemporaryAuthenticationError: If the token request fails due to a temporary MyCupra failure.
89
+ """
90
+ # take token from authorization response (those are stored in self.token now!)
91
+ self.parse_from_fragment(authorization_response)
92
+
93
+ if self.token is not None and all(key in self.token for key in ('state', 'id_token', 'access_token', 'code')):
94
+ # Generate json body for token request
95
+ body: Dict[str, str] = {
96
+ 'state': self.token['state'],
97
+ 'id_token': self.token['id_token'],
98
+ 'redirect_uri': self.redirect_uri,
99
+ 'client_id': self.client_id,
100
+ 'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
101
+ 'code': self.token['code'],
102
+ 'grant_type': 'authorization_code'
103
+ }
104
+
105
+ request_headers: CaseInsensitiveDict = dict(self.headers) # pyright: ignore reportAssignmentType
106
+ request_headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
107
+
108
+ # request tokens from token_url
109
+ token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
110
+ access_type=AccessType.NONE) # pyright: ignore reportCallIssue
111
+ if token_response.status_code != requests.codes['ok']:
112
+ raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MyCupra failure: {token_response.status_code}')
113
+ # parse token from response body
114
+ token = self.parse_from_body(token_response.text)
115
+
116
+ return token
117
+ return None
118
+
119
+ def parse_from_body(self, token_response, state=None):
120
+ """
121
+ Fix strange token naming before parsing it with OAuthlib.
122
+ """
123
+ try:
124
+ # Tokens are in body of response in json format
125
+ token = json.loads(token_response)
126
+ except json.decoder.JSONDecodeError as err:
127
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MyCupra failure: json could not be decoded') from err
128
+ # Fix token keys, we want access_token instead of accessToken
129
+ if 'accessToken' in token:
130
+ token['access_token'] = token.pop('accessToken')
131
+ # Fix token keys, we want id_token instead of idToken
132
+ if 'idToken' in token:
133
+ token['id_token'] = token.pop('idToken')
134
+ # Fix token keys, we want refresh_token instead of refreshToken
135
+ if 'refreshToken' in token:
136
+ token['refresh_token'] = token.pop('refreshToken')
137
+ # generate json from fixed dict
138
+ fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
139
+ # Let OAuthlib parse the token
140
+ return super(MyCupraSession, self).parse_from_body(token_response=fixed_token_response, state=state)
141
+
142
+ def refresh_tokens(
143
+ self,
144
+ token_url,
145
+ refresh_token=None,
146
+ auth=None,
147
+ timeout=None,
148
+ headers=None,
149
+ verify=True,
150
+ proxies=None,
151
+ **_
152
+ ):
153
+ """
154
+ Refreshes the authentication tokens using the provided refresh token.
155
+ Args:
156
+ token_url (str): The URL to request new tokens from.
157
+ refresh_token (str, optional): The refresh token to use. Defaults to None.
158
+ auth (tuple, optional): Authentication credentials. Defaults to None.
159
+ timeout (float or tuple, optional): How long to wait for the server to send data before giving up. Defaults to None.
160
+ headers (dict, optional): Headers to include in the request. Defaults to None.
161
+ verify (bool, optional): Whether to verify the server's TLS certificate. Defaults to True.
162
+ proxies (dict, optional): Proxies to use for the request. Defaults to None.
163
+ **_ (dict): Additional arguments.
164
+ Raises:
165
+ ValueError: If no token endpoint is set for auto_refresh.
166
+ InsecureTransportError: If the token URL is not secure.
167
+ AuthenticationError: If the server requests new authorization.
168
+ TemporaryAuthenticationError: If the token could not be refreshed due to a temporary server failure.
169
+ RetrievalError: If the status code from the server is not recognized.
170
+ Returns:
171
+ dict: The new tokens.
172
+ """
173
+ LOG.info('Refreshing tokens')
174
+ if not token_url:
175
+ raise ValueError("No token endpoint set for auto_refresh.")
176
+
177
+ if not is_secure_transport(token_url):
178
+ raise InsecureTransportError()
179
+
180
+ # Store old refresh token in case no new one is given
181
+ refresh_token = refresh_token or self.refresh_token
182
+ if refresh_token is None:
183
+ self.login()
184
+ return self.token
185
+
186
+ if headers is None:
187
+ headers = dict(self.headers)
188
+
189
+ body: Dict[str, str] = {
190
+ 'client_id': self.client_id,
191
+ 'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
192
+ 'grant_type': 'refresh_token',
193
+ 'refresh_token': self.refresh_token
194
+ }
195
+
196
+ headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
197
+
198
+ # Request new tokens using the refresh token
199
+ token_response = self.post(
200
+ token_url,
201
+ data=body,
202
+ auth=auth,
203
+ timeout=timeout,
204
+ headers=headers,
205
+ verify=verify,
206
+ withhold_token=False, # pyright: ignore reportCallIssue
207
+ proxies=proxies,
208
+ access_type=AccessType.NONE # pyright: ignore reportCallIssue
209
+ )
210
+ if token_response.status_code == requests.codes['unauthorized']:
211
+ raise AuthenticationError('Refreshing tokens failed: Server requests new authorization')
212
+ elif token_response.status_code in (requests.codes['internal_server_error'], requests.codes['service_unavailable'], requests.codes['gateway_timeout']):
213
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MyCupra failure: {tokenResponse.status_code}')
214
+ elif token_response.status_code == requests.codes['ok']:
215
+ # parse new tokens from response
216
+ self.parse_from_body(token_response.text)
217
+ if self.token is not None and "refresh_token" not in self.token:
218
+ LOG.debug("No new refresh token given. Re-using old.")
219
+ self.token["refresh_token"] = refresh_token
220
+ return self.token
221
+ else:
222
+ raise RetrievalError(f'Status Code from MyCupra while refreshing tokens was: {token_response.status_code}')
223
+
224
+ def request(
225
+ self,
226
+ method,
227
+ url,
228
+ data=None,
229
+ headers=None,
230
+ withhold_token=False,
231
+ access_type=AccessType.ACCESS,
232
+ token=None,
233
+ timeout=None,
234
+ **kwargs
235
+ ):
236
+ """Intercept all requests and add userId if present."""
237
+ if not is_secure_transport(url):
238
+ raise InsecureTransportError()
239
+ if self.user_id is not None:
240
+ headers = headers or {}
241
+ headers['user-id'] = self.user_id
242
+
243
+ return super(MyCupraSession, self).request(method, url, headers=headers, data=data, withhold_token=withhold_token, access_type=access_type, token=token,
244
+ timeout=timeout, **kwargs)