Repository: bimmerconnected/bimmer_connected (python)

Issue:
Changes to httpx SSL cert handling
Hi,

I've just tried out bimmer_connected as a workaround for evcc.io not supporting the new captcha. I have bimmerconnected working on my MacBook, but I need to get it running on my Raspberry Pi for evcc to be able to use it.

On my RPi, when I run:

bimmerconnected status MYEMAIL MYPASS -v MYVIN rest_of_world --json --hcaptcha THE_CAPTURE_TOKEN

I get:

Traceback (most recent call last):
  File "/usr/local/bin/bimmerconnected", line 5, in <module>
    from bimmer_connected.cli import main
  File "/usr/local/lib/python3.9/dist-packages/bimmer_connected/cli.py", line 16, in <module>
    from bimmer_connected.account import MyBMWAccount
  File "/usr/local/lib/python3.9/dist-packages/bimmer_connected/account.py", line 11, in <module>
    from bimmer_connected.api.authentication import MyBMWAuthentication
  File "/usr/local/lib/python3.9/dist-packages/bimmer_connected/api/authentication.py", line 44, in <module>
    class MyBMWAuthentication(httpx.Auth):
  File "/usr/local/lib/python3.9/dist-packages/bimmer_connected/api/authentication.py", line 57, in MyBMWAuthentication
    verify: httpx._types.VerifyTypes = True,
AttributeError: module 'httpx._types' has no attribute 'VerifyTypes'

I've Googled that error and don't get any results. I have not done any Python for a very, very long time so I'm out of my depth here.

python -V reports:

Python 3.9.2

and it appears I have version 0.28.0 of httpx. I randomly tried 0.24.0 as well but it makes no difference.

_Originally posted by @pdonovan in https://github.com/bimmerconnected/bimmer_connected/discussions/687_

Pull request:
Fix for httpx>=0.28.0
## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
`httpx` have removed specific types with their update to 0.28.0, breaking the library.

## Type of change
- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (which adds functionality to this library)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #688 
- This PR is related to issue: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [ ] Tests have been added to verify that the new code works.


Patch:
diff --git a/bimmer_connected/account.py b/bimmer_connected/account.py
index 3bba627..725617b 100644
--- a/bimmer_connected/account.py
+++ b/bimmer_connected/account.py
@@ -3,10 +3,9 @@
 import datetime
 import json
 import logging
+import ssl
 from dataclasses import InitVar, dataclass, field
-from typing import List, Optional
-
-import httpx
+from typing import List, Optional, Union
 
 from bimmer_connected.api.authentication import MyBMWAuthentication
 from bimmer_connected.api.client import RESPONSE_STORE, MyBMWClient, MyBMWClientConfiguration
@@ -47,7 +46,7 @@ class MyBMWAccount:
     observer_position: InitVar[GPSPosition] = None
     """Optional. Required for getting a position on older cars."""
 
-    verify: InitVar[httpx._types.VerifyTypes] = True
+    verify: InitVar[Union[ssl.SSLContext, str, bool]] = True
     """Optional. Specify SSL context (required for Home Assistant)."""
 
     use_metric_units: InitVar[Optional[bool]] = None
diff --git a/bimmer_connected/api/authentication.py b/bimmer_connected/api/authentication.py
index 7012ed5..637fc9b 100644
--- a/bimmer_connected/api/authentication.py
+++ b/bimmer_connected/api/authentication.py
@@ -5,8 +5,9 @@ import base64
 import datetime
 import logging
 import math
+import ssl
 from collections import defaultdict
-from typing import AsyncGenerator, Generator, Optional
+from typing import AsyncGenerator, Generator, Optional, Union
 from uuid import uuid4
 
 import httpx
@@ -54,7 +55,7 @@ class MyBMWAuthentication(httpx.Auth):
         refresh_token: Optional[str] = None,
         gcid: Optional[str] = None,
         hcaptcha_token: Optional[str] = None,
-        verify: httpx._types.VerifyTypes = True,
+        verify: Union[ssl.SSLContext, str, bool] = True,
     ):
         self.username: str = username
         self.password: str = password
@@ -68,7 +69,7 @@ class MyBMWAuthentication(httpx.Auth):
         self.hcaptcha_token: Optional[str] = hcaptcha_token
         # Use external SSL context. Required in Home Assistant due to event loop blocking when httpx loads
         # SSL certificates from disk. If not given, uses httpx defaults.
-        self.verify: Optional[httpx._types.VerifyTypes] = verify
+        self.verify: Union[ssl.SSLContext, str, bool] = verify
 
     @property
     def login_lock(self) -> asyncio.Lock:
diff --git a/bimmer_connected/api/client.py b/bimmer_connected/api/client.py
index a29f60f..b11891b 100644
--- a/bimmer_connected/api/client.py
+++ b/bimmer_connected/api/client.py
@@ -1,9 +1,10 @@
 """Generic API management."""
 
 import logging
+import ssl
 from collections import defaultdict, deque
 from dataclasses import dataclass
-from typing import Deque, Dict, Optional
+from typing import Deque, Dict, Optional, Union
 
 import httpx
 
@@ -25,7 +26,7 @@ class MyBMWClientConfiguration:
     authentication: MyBMWAuthentication
     log_responses: Optional[bool] = False
     observer_position: Optional[GPSPosition] = None
-    verify: httpx._types.VerifyTypes = True
+    verify: Union[ssl.SSLContext, str, bool] = True
 
     def set_log_responses(self, log_responses: bool) -> None:
         """Set if responses are logged and clear response store."""


Test patch:
diff --git a/bimmer_connected/tests/common.py b/bimmer_connected/tests/common.py
index d4e32a6..5d4a774 100644
--- a/bimmer_connected/tests/common.py
+++ b/bimmer_connected/tests/common.py
@@ -58,6 +58,9 @@ LOCAL_CHARGING_SETTINGS: Dict[str, Dict] = {}
 class MyBMWMockRouter(respx.MockRouter):
     """Stateful MockRouter for MyBMW APIs."""
 
+    # See https://github.com/lundberg/respx/issues/277#issuecomment-2507693706
+    using = "httpx"
+
     def __init__(
         self,
         vehicles_to_load: Optional[List[str]] = None,
diff --git a/bimmer_connected/tests/test_cli.py b/bimmer_connected/tests/test_cli.py
index 62a26bf..acc63c5 100644
--- a/bimmer_connected/tests/test_cli.py
+++ b/bimmer_connected/tests/test_cli.py
@@ -261,7 +261,9 @@ def test_login_refresh_token(cli_home_dir: Path, bmw_fixture: respx.Router):
     bimmer_connected.cli.main()
 
     assert bmw_fixture.routes["token"].call_count == 1
-    assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer outdated_access_token"
+    # TODO: The following doesn't work with MyBMWMockRouter.using = "httpx"
+    # Need to wait for a respx update supporting httpx>=0.28.0 natively
+    # assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer outdated_access_token"
     assert bmw_fixture.routes["vehicles"].calls.last.request.headers["authorization"] == "Bearer some_token_string"
 
     assert (cli_home_dir / ".bimmer_connected.json").exists() is True