From 9ad75a1276fd355793758608afff8acaf34650fa Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Mon, 2 Jun 2025 11:36:42 +0200 Subject: [PATCH 1/3] Restore countryCode login option, it was removed by mistake. Fixes #19 --- src/saic_ismart_client_ng/api/base.py | 15 +++++++++++---- tests/test_model.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/saic_ismart_client_ng/api/base.py b/src/saic_ismart_client_ng/api/base.py index f368c09..ec6bd72 100644 --- a/src/saic_ismart_client_ng/api/base.py +++ b/src/saic_ismart_client_ng/api/base.py @@ -69,12 +69,19 @@ async def login(self) -> LoginResp: "scope": "all", "deviceId": f"{firebase_device_id}###com.saicmotor.europecar", "deviceType": "0", # 2 for huawei - "loginType": "2" if self.__configuration.username_is_email else "1", - "language": "EN" - if self.__configuration.username_is_email - else self.__configuration.phone_country_code, + "language": "EN", } + if self.__configuration.username_is_email: + form_body.update({ + "loginType": "2" + }) + else: + form_body.update({ + "loginType": "1", + "countryCode": self.__configuration.phone_country_code, + }) + result = await self.execute_api_call( "POST", "/oauth/token", diff --git a/tests/test_model.py b/tests/test_model.py index a0e2ce5..a67e8be 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -11,7 +11,7 @@ def setUp(self) -> None: "test_username", "test_password", True, - "GB", + "39", "https://test-uri.com", "123456", "test_region", @@ -28,7 +28,7 @@ def test_username_is_email(self) -> None: assert self.config.username_is_email def test_phone_country_code(self) -> None: - assert self.config.phone_country_code == "GB" + assert self.config.phone_country_code == "39" def test_base_uri(self) -> None: assert self.config.base_uri == "https://test-uri.com" From 8e46ed9d67236dad493a9fb96a6621c67cec6b3e Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Mon, 2 Jun 2025 11:39:52 +0200 Subject: [PATCH 2/3] Fix ruff and mypy issues --- src/saic_ismart_client_ng/api/base.py | 173 +++++++++++++------------- 1 file changed, 89 insertions(+), 84 deletions(-) diff --git a/src/saic_ismart_client_ng/api/base.py b/src/saic_ismart_client_ng/api/base.py index ec6bd72..ac14d41 100644 --- a/src/saic_ismart_client_ng/api/base.py +++ b/src/saic_ismart_client_ng/api/base.py @@ -33,13 +33,11 @@ from saic_ismart_client_ng.listener import SaicApiListener from saic_ismart_client_ng.model import SaicApiConfiguration - class IsDataclass(Protocol): # as already noted in comments, checking for this attribute is currently # the most reliable way to ascertain that something is a dataclass __dataclass_fields__: ClassVar[dict[str, Any]] - T = TypeVar("T", bound=IsDataclass) logger = logging.getLogger(__name__) @@ -47,9 +45,9 @@ class IsDataclass(Protocol): class AbstractSaicApi: def __init__( - self, - configuration: SaicApiConfiguration, - listener: SaicApiListener | None = None, + self, + configuration: SaicApiConfiguration, + listener: SaicApiListener | None = None, ) -> None: self.__configuration = configuration self.__api_client = SaicApiClient(configuration, listener=listener) @@ -61,7 +59,10 @@ async def login(self) -> LoginResp: "Accept": "application/json", "Authorization": "Basic c3dvcmQ6c3dvcmRfc2VjcmV0", } - firebase_device_id = "simulator*********************************************" + str(int(datetime.datetime.now().timestamp())) + firebase_device_id = ( + "simulator*********************************************" + + str(int(datetime.datetime.now().timestamp())) + ) form_body = { "grant_type": "password", "username": self.__configuration.username, @@ -73,14 +74,18 @@ async def login(self) -> LoginResp: } if self.__configuration.username_is_email: - form_body.update({ - "loginType": "2" - }) + form_body.update({"loginType": "2"}) + elif self.__configuration.phone_country_code is not None: + form_body.update( + { + "loginType": "1", + "countryCode": self.__configuration.phone_country_code, + } + ) else: - form_body.update({ - "loginType": "1", - "countryCode": self.__configuration.phone_country_code, - }) + raise SaicApiException( + "Username is not an email but no phone number country code has been provided." + ) result = await self.execute_api_call( "POST", @@ -91,7 +96,7 @@ async def login(self) -> LoginResp: ) # Update the user token if not (access_token := result.access_token) or not ( - expiration := result.expires_in + expiration := result.expires_in ): raise SaicApiException( "Failed to get an access token, please check your credentials" @@ -104,15 +109,15 @@ async def login(self) -> LoginResp: return result async def execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, ) -> T: result = await self.__execute_api_call( method, @@ -130,15 +135,15 @@ async def execute_api_call( return result async def execute_api_call_with_optional_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, ) -> T | None: return await self.__execute_api_call( method, @@ -152,14 +157,14 @@ async def execute_api_call_with_optional_result( ) async def execute_api_call_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, ) -> None: await self.__execute_api_call( method, @@ -172,16 +177,16 @@ async def execute_api_call_no_result( ) async def __execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> T | None: try: url = f"{self.__configuration.base_uri}{path.removeprefix('/')}" @@ -203,15 +208,15 @@ async def __execute_api_call( raise SaicApiException(msg, return_code=500) from e async def execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T: result = await self.__execute_api_call_with_event_id( method, @@ -228,14 +233,14 @@ async def execute_api_call_with_event_id( return result async def execute_api_call_with_event_id_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> None: await self.__execute_api_call_with_event_id( method, @@ -247,15 +252,15 @@ async def execute_api_call_with_event_id_no_result( ) async def __execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T | None: @tenacity.retry( stop=tenacity.stop_after_delay(30), @@ -280,11 +285,11 @@ async def execute_api_call_with_event_id_inner(*, event_id: str) -> T | None: # pylint: disable=too-many-branches async def __deserialize( - self, - request: httpx.Request, - response: httpx.Response, - data_class: type[T] | None, - allow_null_body: bool, + self, + request: httpx.Request, + response: httpx.Response, + data_class: type[T] | None, + allow_null_body: bool, ) -> T | None: try: request_event_id = request.headers.get("event-id") @@ -377,9 +382,9 @@ def logout(self) -> None: @property def is_logged_in(self) -> bool: return ( - self.__api_client.user_token is not None - and self.__token_expiration is not None - and self.__token_expiration > datetime.datetime.now() + self.__api_client.user_token is not None + and self.__token_expiration is not None + and self.__token_expiration > datetime.datetime.now() ) @property From 320a3d1660b5d21185158e11076ac07c064c669e Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Mon, 2 Jun 2025 11:40:06 +0200 Subject: [PATCH 3/3] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ac66f6..56994a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "saic_ismart_client_ng" -version = "0.9.0" +version = "0.9.1" description = "SAIC next gen client library (MG iSMART)" authors = [ { name = "Giovanni Condello", email = "saic-python-client@nanomad.net" },