Skip to content

Commit

Permalink
Better handling of exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
nanomad committed Oct 9, 2024
1 parent 2b9aa47 commit 055662b
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 182 deletions.
138 changes: 64 additions & 74 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "saic_ismart_client_ng"
homepage = "https://github.com/SAIC-iSmart-API/saic-python-client-ng"
version = "0.4.0"
version = "0.5.1"
description = "SAIC next gen client library (MG iSMART)"
authors = [
"Giovanni Condello <[email protected]>",
Expand Down Expand Up @@ -51,6 +51,7 @@ mock_use_standalone_module = true
addopts = [
"--import-mode=importlib",
]
asyncio_default_fixture_loop_scope="function"

[tool.coverage.run]
omit = [
Expand Down
106 changes: 65 additions & 41 deletions src/saic_ismart_client_ng/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@
import dacite
import httpx
import tenacity
from httpx import TimeoutException
from httpx._types import QueryParamTypes, HeaderTypes

from saic_ismart_client_ng.api.schema import LoginResp
from saic_ismart_client_ng.crypto_utils import sha1_hex_digest
from saic_ismart_client_ng.exceptions import SaicApiException, SaicApiRetryException, SaicLogoutException
from saic_ismart_client_ng.listener import SaicApiListener
from saic_ismart_client_ng.model import SaicApiConfiguration
from saic_ismart_client_ng.net.client.api import SaicApiClient
from saic_ismart_client_ng.net.client.login import SaicLoginClient
from saic_ismart_client_ng.net.client import SaicApiClient

logger = logging.getLogger(__name__)

Expand All @@ -29,49 +27,36 @@ def __init__(
listener: SaicApiListener = None,
):
self.__configuration = configuration
self.__login_client = SaicLoginClient(configuration, listener=listener)
self.__api_client = SaicApiClient(configuration, listener=listener)
self.__token_expiration: Optional[datetime.datetime] = None

@property
def configuration(self) -> SaicApiConfiguration:
return self.__configuration

@property
def login_client(self) -> SaicLoginClient:
return self.__login_client

@property
def api_client(self) -> SaicApiClient:
return self.__api_client

@property
def token_expiration(self) -> Optional[datetime.datetime]:
return self.__token_expiration

async def login(self) -> LoginResp:
url = f"{self.configuration.base_uri}oauth/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"Authorization": "Basic c3dvcmQ6c3dvcmRfc2VjcmV0"
}
firebase_device_id = "cqSHOMG1SmK4k-fzAeK6hr:APA91bGtGihOG5SEQ9hPx3Dtr9o9mQguNiKZrQzboa-1C_UBlRZYdFcMmdfLvh9Q_xA8A0dGFIjkMhZbdIXOYnKfHCeWafAfLXOrxBS3N18T4Slr-x9qpV6FHLMhE9s7I6s89k9lU7DD"
form_body = {
"grant_type": "password",
"username": self.configuration.username,
"password": sha1_hex_digest(self.configuration.password),
"username": self.__configuration.username,
"password": sha1_hex_digest(self.__configuration.password),
"scope": "all",
"deviceId": f"{firebase_device_id}###europecar",
"deviceType": "1", # 2 for huawei
"loginType": "2" if self.configuration.username_is_email else "1",
"countryCode": "" if self.configuration.username_is_email else self.configuration.phone_country_code,
"loginType": "2" if self.__configuration.username_is_email else "1",
"countryCode": "" if self.__configuration.username_is_email else self.__configuration.phone_country_code,
}

req = httpx.Request("POST", url, data=form_body, headers=headers)
response = await self.login_client.client.send(req)
result = await self.deserialize(req, response, LoginResp)
result = await self.execute_api_call(
"POST",
"/oauth/token",
form_body=form_body,
out_type=LoginResp,
headers=headers
)
# Update the user token
self.api_client.user_token = result.access_token
self.__api_client.user_token = result.access_token
self.__token_expiration = datetime.datetime.now() + datetime.timedelta(seconds=result.expires_in)
return result

Expand All @@ -81,15 +66,42 @@ async def execute_api_call(
path: str,
*,
body: Optional[Any] = None,
form_body: Optional[Any] = None,
out_type: Optional[Type[T]] = None,
params: Optional[QueryParamTypes] = None,
headers: Optional[HeaderTypes] = None,
) -> Optional[T]:
try:
return await self.__execute_api_call(
method,
path,
body=body,
form_body=form_body,
out_type=out_type,
params=params,
headers=headers
)
except SaicApiException as e:
raise e
except Exception as e:
raise SaicApiException(f"API call {method} {path} failed unexpectedly", return_code=500) from e

async def __execute_api_call(
self,
method: str,
path: str,
*,
body: Optional[Any] = None,
form_body: Optional[Any] = None,
out_type: Optional[Type[T]] = None,
params: Optional[QueryParamTypes] = None,
headers: Optional[HeaderTypes] = None,
) -> Optional[T]:
url = f"{self.__configuration.base_uri}{path[1:] if path.startswith('/') else path}"
json_body = asdict(body) if body else None
req = httpx.Request(method, url, params=params, headers=headers, json=json_body)
response = await self.api_client.client.send(req)
return await self.deserialize(req, response, out_type)
req = httpx.Request(method, url, params=params, headers=headers, data=form_body, json=json_body)
response = await self.__api_client.send(req)
return await self.__deserialize(req, response, out_type)

async def execute_api_call_with_event_id(
self,
Expand All @@ -112,7 +124,7 @@ async def execute_api_call_with_event_id(
async def execute_api_call_with_event_id_inner(*, event_id: str):
actual_headers = headers or dict()
actual_headers.update({'event-id': event_id})
return await self.execute_api_call(
return await self.__execute_api_call(
method,
path,
body=body,
Expand All @@ -123,7 +135,7 @@ async def execute_api_call_with_event_id_inner(*, event_id: str):

return await execute_api_call_with_event_id_inner(event_id='0')

async def deserialize(
async def __deserialize(
self,
request: httpx.Request,
response: httpx.Response,
Expand Down Expand Up @@ -184,23 +196,35 @@ async def deserialize(
if response.is_error:
if response.status_code in (401, 403):
logger.error(
f"API call failed due to an authentication failure: {response.status_code} {response.text}"
f"API call failed due to an authentication failure: {response.status_code} {response.text}",
exc_info=e
)
self.logout()
raise SaicLogoutException(response.text, response.status_code)
raise SaicLogoutException(response.text, response.status_code) from e
else:
logger.error(f"API call failed: {response.status_code} {response.text}")
raise SaicApiException(response.text, response.status_code)
logger.error(
f"API call failed: {response.status_code} {response.text}",
exc_info=e
)
raise SaicApiException(response.text, response.status_code) from e
else:
raise SaicApiException(f"Failed to deserialize response: {e}. Original json was {response.text}") from e

def logout(self):
self.api_client.user_token = None
self.__api_client.user_token = None
self.__token_expiration = None

@property
def is_logged_in(self) -> bool:
return self.__token_expiration is not None \
and self.__token_expiration > datetime.datetime.now()
return (
self.__api_client.user_token is not None and
self.__token_expiration is not None and
self.__token_expiration > datetime.datetime.now()
)

@property
def token_expiration(self) -> Optional[datetime.datetime]:
return self.__token_expiration


def saic_api_after_retry(retry_state):
Expand Down
36 changes: 17 additions & 19 deletions src/saic_ismart_client_ng/net/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import logging
from abc import ABC
from datetime import datetime

import httpx
from httpx import Request, Response

from saic_ismart_client_ng.listener import SaicApiListener
from saic_ismart_client_ng.model import SaicApiConfiguration
from saic_ismart_client_ng.net.httpx import decrypt_httpx_response, encrypt_httpx_request


class AbstractSaicClient(ABC):
class SaicApiClient:
def __init__(
self,
configuration: SaicApiConfiguration,
Expand All @@ -23,18 +23,16 @@ def __init__(
self.__class_name = ""
self.__client = httpx.AsyncClient(
event_hooks={
"request": [self.invoke_request_listener, self.encrypt_request],
"response": [decrypt_httpx_response, self.invoke_response_listener]
"request": [self.__invoke_request_listener, self.__encrypt_request],
"response": [decrypt_httpx_response, self.__invoke_response_listener]
}
)

@property
def client(self) -> httpx.AsyncClient:
return self.__client

@property
def configuration(self) -> SaicApiConfiguration:
return self.__configuration
async def send(
self,
request: Request
) -> Response:
return await self.__client.send(request)

@property
def user_token(self) -> str:
Expand All @@ -44,7 +42,7 @@ def user_token(self) -> str:
def user_token(self, new_token: str):
self.__user_token = new_token

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

await self.__listener.on_request(
path=str(request.url).replace(self.configuration.base_uri, "/"),
path=str(request.url).replace(self.__configuration.base_uri, "/"),
body=body,
headers=dict(request.headers),
)
except Exception as e:
self.__logger.warning(f"Error invoking request listener: {e}", exc_info=e)

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

await self.__listener.on_response(
path=str(response.url).replace(self.configuration.base_uri, "/"),
path=str(response.url).replace(self.__configuration.base_uri, "/"),
body=body,
headers=dict(response.headers),
)
except Exception as e:
self.__logger.warning(f"Error invoking request listener: {e}", exc_info=e)

async def encrypt_request(self, modified_request: httpx.Request):
async def __encrypt_request(self, modified_request: httpx.Request):
return await encrypt_httpx_request(
modified_request=modified_request,
request_timestamp=datetime.now(),
base_uri=self.configuration.base_uri,
region=self.configuration.region,
tenant_id=self.configuration.tenant_id,
base_uri=self.__configuration.base_uri,
region=self.__configuration.region,
tenant_id=self.__configuration.tenant_id,
user_token=self.user_token,
class_name=self.__class_name
)
24 changes: 0 additions & 24 deletions src/saic_ismart_client_ng/net/client/api.py

This file was deleted.

23 changes: 0 additions & 23 deletions src/saic_ismart_client_ng/net/client/login.py

This file was deleted.

0 comments on commit 055662b

Please sign in to comment.