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.
@@ -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)