Skip to content

Commit 055662b

Browse files
committed
Better handling of exceptions
1 parent 2b9aa47 commit 055662b

File tree

6 files changed

+148
-182
lines changed

6 files changed

+148
-182
lines changed

poetry.lock

Lines changed: 64 additions & 74 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "saic_ismart_client_ng"
33
homepage = "https://github.com/SAIC-iSmart-API/saic-python-client-ng"
4-
version = "0.4.0"
4+
version = "0.5.1"
55
description = "SAIC next gen client library (MG iSMART)"
66
authors = [
77
"Giovanni Condello <[email protected]>",
@@ -51,6 +51,7 @@ mock_use_standalone_module = true
5151
addopts = [
5252
"--import-mode=importlib",
5353
]
54+
asyncio_default_fixture_loop_scope="function"
5455

5556
[tool.coverage.run]
5657
omit = [

src/saic_ismart_client_ng/api/base.py

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@
88
import dacite
99
import httpx
1010
import tenacity
11-
from httpx import TimeoutException
1211
from httpx._types import QueryParamTypes, HeaderTypes
1312

1413
from saic_ismart_client_ng.api.schema import LoginResp
1514
from saic_ismart_client_ng.crypto_utils import sha1_hex_digest
1615
from saic_ismart_client_ng.exceptions import SaicApiException, SaicApiRetryException, SaicLogoutException
1716
from saic_ismart_client_ng.listener import SaicApiListener
1817
from saic_ismart_client_ng.model import SaicApiConfiguration
19-
from saic_ismart_client_ng.net.client.api import SaicApiClient
20-
from saic_ismart_client_ng.net.client.login import SaicLoginClient
18+
from saic_ismart_client_ng.net.client import SaicApiClient
2119

2220
logger = logging.getLogger(__name__)
2321

@@ -29,49 +27,36 @@ def __init__(
2927
listener: SaicApiListener = None,
3028
):
3129
self.__configuration = configuration
32-
self.__login_client = SaicLoginClient(configuration, listener=listener)
3330
self.__api_client = SaicApiClient(configuration, listener=listener)
3431
self.__token_expiration: Optional[datetime.datetime] = None
3532

36-
@property
37-
def configuration(self) -> SaicApiConfiguration:
38-
return self.__configuration
39-
40-
@property
41-
def login_client(self) -> SaicLoginClient:
42-
return self.__login_client
43-
44-
@property
45-
def api_client(self) -> SaicApiClient:
46-
return self.__api_client
47-
48-
@property
49-
def token_expiration(self) -> Optional[datetime.datetime]:
50-
return self.__token_expiration
51-
5233
async def login(self) -> LoginResp:
53-
url = f"{self.configuration.base_uri}oauth/token"
5434
headers = {
5535
"Content-Type": "application/x-www-form-urlencoded",
5636
"Accept": "application/json",
37+
"Authorization": "Basic c3dvcmQ6c3dvcmRfc2VjcmV0"
5738
}
5839
firebase_device_id = "cqSHOMG1SmK4k-fzAeK6hr:APA91bGtGihOG5SEQ9hPx3Dtr9o9mQguNiKZrQzboa-1C_UBlRZYdFcMmdfLvh9Q_xA8A0dGFIjkMhZbdIXOYnKfHCeWafAfLXOrxBS3N18T4Slr-x9qpV6FHLMhE9s7I6s89k9lU7DD"
5940
form_body = {
6041
"grant_type": "password",
61-
"username": self.configuration.username,
62-
"password": sha1_hex_digest(self.configuration.password),
42+
"username": self.__configuration.username,
43+
"password": sha1_hex_digest(self.__configuration.password),
6344
"scope": "all",
6445
"deviceId": f"{firebase_device_id}###europecar",
6546
"deviceType": "1", # 2 for huawei
66-
"loginType": "2" if self.configuration.username_is_email else "1",
67-
"countryCode": "" if self.configuration.username_is_email else self.configuration.phone_country_code,
47+
"loginType": "2" if self.__configuration.username_is_email else "1",
48+
"countryCode": "" if self.__configuration.username_is_email else self.__configuration.phone_country_code,
6849
}
6950

70-
req = httpx.Request("POST", url, data=form_body, headers=headers)
71-
response = await self.login_client.client.send(req)
72-
result = await self.deserialize(req, response, LoginResp)
51+
result = await self.execute_api_call(
52+
"POST",
53+
"/oauth/token",
54+
form_body=form_body,
55+
out_type=LoginResp,
56+
headers=headers
57+
)
7358
# Update the user token
74-
self.api_client.user_token = result.access_token
59+
self.__api_client.user_token = result.access_token
7560
self.__token_expiration = datetime.datetime.now() + datetime.timedelta(seconds=result.expires_in)
7661
return result
7762

@@ -81,15 +66,42 @@ async def execute_api_call(
8166
path: str,
8267
*,
8368
body: Optional[Any] = None,
69+
form_body: Optional[Any] = None,
70+
out_type: Optional[Type[T]] = None,
71+
params: Optional[QueryParamTypes] = None,
72+
headers: Optional[HeaderTypes] = None,
73+
) -> Optional[T]:
74+
try:
75+
return await self.__execute_api_call(
76+
method,
77+
path,
78+
body=body,
79+
form_body=form_body,
80+
out_type=out_type,
81+
params=params,
82+
headers=headers
83+
)
84+
except SaicApiException as e:
85+
raise e
86+
except Exception as e:
87+
raise SaicApiException(f"API call {method} {path} failed unexpectedly", return_code=500) from e
88+
89+
async def __execute_api_call(
90+
self,
91+
method: str,
92+
path: str,
93+
*,
94+
body: Optional[Any] = None,
95+
form_body: Optional[Any] = None,
8496
out_type: Optional[Type[T]] = None,
8597
params: Optional[QueryParamTypes] = None,
8698
headers: Optional[HeaderTypes] = None,
8799
) -> Optional[T]:
88100
url = f"{self.__configuration.base_uri}{path[1:] if path.startswith('/') else path}"
89101
json_body = asdict(body) if body else None
90-
req = httpx.Request(method, url, params=params, headers=headers, json=json_body)
91-
response = await self.api_client.client.send(req)
92-
return await self.deserialize(req, response, out_type)
102+
req = httpx.Request(method, url, params=params, headers=headers, data=form_body, json=json_body)
103+
response = await self.__api_client.send(req)
104+
return await self.__deserialize(req, response, out_type)
93105

94106
async def execute_api_call_with_event_id(
95107
self,
@@ -112,7 +124,7 @@ async def execute_api_call_with_event_id(
112124
async def execute_api_call_with_event_id_inner(*, event_id: str):
113125
actual_headers = headers or dict()
114126
actual_headers.update({'event-id': event_id})
115-
return await self.execute_api_call(
127+
return await self.__execute_api_call(
116128
method,
117129
path,
118130
body=body,
@@ -123,7 +135,7 @@ async def execute_api_call_with_event_id_inner(*, event_id: str):
123135

124136
return await execute_api_call_with_event_id_inner(event_id='0')
125137

126-
async def deserialize(
138+
async def __deserialize(
127139
self,
128140
request: httpx.Request,
129141
response: httpx.Response,
@@ -184,23 +196,35 @@ async def deserialize(
184196
if response.is_error:
185197
if response.status_code in (401, 403):
186198
logger.error(
187-
f"API call failed due to an authentication failure: {response.status_code} {response.text}"
199+
f"API call failed due to an authentication failure: {response.status_code} {response.text}",
200+
exc_info=e
188201
)
189202
self.logout()
190-
raise SaicLogoutException(response.text, response.status_code)
203+
raise SaicLogoutException(response.text, response.status_code) from e
191204
else:
192-
logger.error(f"API call failed: {response.status_code} {response.text}")
193-
raise SaicApiException(response.text, response.status_code)
205+
logger.error(
206+
f"API call failed: {response.status_code} {response.text}",
207+
exc_info=e
208+
)
209+
raise SaicApiException(response.text, response.status_code) from e
194210
else:
195211
raise SaicApiException(f"Failed to deserialize response: {e}. Original json was {response.text}") from e
196212

197213
def logout(self):
198-
self.api_client.user_token = None
214+
self.__api_client.user_token = None
199215
self.__token_expiration = None
200216

217+
@property
201218
def is_logged_in(self) -> bool:
202-
return self.__token_expiration is not None \
203-
and self.__token_expiration > datetime.datetime.now()
219+
return (
220+
self.__api_client.user_token is not None and
221+
self.__token_expiration is not None and
222+
self.__token_expiration > datetime.datetime.now()
223+
)
224+
225+
@property
226+
def token_expiration(self) -> Optional[datetime.datetime]:
227+
return self.__token_expiration
204228

205229

206230
def saic_api_after_retry(retry_state):
Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import logging
2-
from abc import ABC
32
from datetime import datetime
43

54
import httpx
5+
from httpx import Request, Response
66

77
from saic_ismart_client_ng.listener import SaicApiListener
88
from saic_ismart_client_ng.model import SaicApiConfiguration
99
from saic_ismart_client_ng.net.httpx import decrypt_httpx_response, encrypt_httpx_request
1010

1111

12-
class AbstractSaicClient(ABC):
12+
class SaicApiClient:
1313
def __init__(
1414
self,
1515
configuration: SaicApiConfiguration,
@@ -23,18 +23,16 @@ def __init__(
2323
self.__class_name = ""
2424
self.__client = httpx.AsyncClient(
2525
event_hooks={
26-
"request": [self.invoke_request_listener, self.encrypt_request],
27-
"response": [decrypt_httpx_response, self.invoke_response_listener]
26+
"request": [self.__invoke_request_listener, self.__encrypt_request],
27+
"response": [decrypt_httpx_response, self.__invoke_response_listener]
2828
}
2929
)
3030

31-
@property
32-
def client(self) -> httpx.AsyncClient:
33-
return self.__client
34-
35-
@property
36-
def configuration(self) -> SaicApiConfiguration:
37-
return self.__configuration
31+
async def send(
32+
self,
33+
request: Request
34+
) -> Response:
35+
return await self.__client.send(request)
3836

3937
@property
4038
def user_token(self) -> str:
@@ -44,7 +42,7 @@ def user_token(self) -> str:
4442
def user_token(self, new_token: str):
4543
self.__user_token = new_token
4644

47-
async def invoke_request_listener(self, request: httpx.Request):
45+
async def __invoke_request_listener(self, request: httpx.Request):
4846
if not self.__listener:
4947
return
5048
try:
@@ -57,14 +55,14 @@ async def invoke_request_listener(self, request: httpx.Request):
5755
self.__logger.warning(f"Error decoding request content: {e}", exc_info=e)
5856

5957
await self.__listener.on_request(
60-
path=str(request.url).replace(self.configuration.base_uri, "/"),
58+
path=str(request.url).replace(self.__configuration.base_uri, "/"),
6159
body=body,
6260
headers=dict(request.headers),
6361
)
6462
except Exception as e:
6563
self.__logger.warning(f"Error invoking request listener: {e}", exc_info=e)
6664

67-
async def invoke_response_listener(self, response: httpx.Response):
65+
async def __invoke_response_listener(self, response: httpx.Response):
6866
if not self.__listener:
6967
return
7068
try:
@@ -76,20 +74,20 @@ async def invoke_response_listener(self, response: httpx.Response):
7674
self.__logger.warning(f"Error decoding request content: {e}", exc_info=e)
7775

7876
await self.__listener.on_response(
79-
path=str(response.url).replace(self.configuration.base_uri, "/"),
77+
path=str(response.url).replace(self.__configuration.base_uri, "/"),
8078
body=body,
8179
headers=dict(response.headers),
8280
)
8381
except Exception as e:
8482
self.__logger.warning(f"Error invoking request listener: {e}", exc_info=e)
8583

86-
async def encrypt_request(self, modified_request: httpx.Request):
84+
async def __encrypt_request(self, modified_request: httpx.Request):
8785
return await encrypt_httpx_request(
8886
modified_request=modified_request,
8987
request_timestamp=datetime.now(),
90-
base_uri=self.configuration.base_uri,
91-
region=self.configuration.region,
92-
tenant_id=self.configuration.tenant_id,
88+
base_uri=self.__configuration.base_uri,
89+
region=self.__configuration.region,
90+
tenant_id=self.__configuration.tenant_id,
9391
user_token=self.user_token,
9492
class_name=self.__class_name
9593
)

src/saic_ismart_client_ng/net/client/api.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/saic_ismart_client_ng/net/client/login.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)