Skip to content

Commit 07a73d7

Browse files
CopilotiMicknl
andcommitted
Address PR feedback: Fix datetime.now() to use UTC, handle 204 responses, add OAuth error handling, remove enum conversion duplication, fix SSL context mutation
Co-authored-by: iMicknl <1424596+iMicknl@users.noreply.github.com>
1 parent c632270 commit 07a73d7

File tree

4 files changed

+35
-14
lines changed

4 files changed

+35
-14
lines changed

pyoverkiz/auth/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ def is_expired(self, *, skew_seconds: int = 5) -> bool:
2121
if not self.expires_at:
2222
return False
2323

24-
return datetime.datetime.now() >= self.expires_at - datetime.timedelta(
25-
seconds=skew_seconds
26-
)
24+
return datetime.datetime.now(
25+
datetime.UTC
26+
) >= self.expires_at - datetime.timedelta(seconds=skew_seconds)
2727

2828

2929
class AuthStrategy(Protocol):

pyoverkiz/auth/strategies.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ async def _post_login(self, data: Mapping[str, Any]) -> None:
121121
f"Login failed for {self.server.name}: {response.status}"
122122
)
123123

124+
# A 204 No Content response cannot have a body, so skip JSON parsing.
125+
if response.status == 204:
126+
return
127+
124128
result = await response.json()
125129
if not result.get("success"):
126130
raise BadCredentialsException("Login failed: bad credentials")
@@ -200,9 +204,9 @@ async def _request_access_token(
200204
self.context.refresh_token = token.get("refresh_token")
201205
expires_in = token.get("expires_in")
202206
if expires_in:
203-
self.context.expires_at = datetime.datetime.now() + datetime.timedelta(
204-
seconds=cast(int, expires_in) - 5
205-
)
207+
self.context.expires_at = datetime.datetime.now(
208+
datetime.UTC
209+
) + datetime.timedelta(seconds=cast(int, expires_in) - 5)
206210

207211

208212
class CozytouchAuthStrategy(SessionLoginStrategy):
@@ -394,6 +398,18 @@ async def _exchange_token(self, payload: Mapping[str, str]) -> None:
394398
) as response:
395399
token = await response.json()
396400

401+
# Handle OAuth error responses explicitly before accessing the access token.
402+
error = token.get("error")
403+
if error:
404+
description = token.get("error_description") or token.get("message")
405+
if description:
406+
raise InvalidTokenException(
407+
f"Error retrieving Rexel access token: {description}"
408+
)
409+
raise InvalidTokenException(
410+
f"Error retrieving Rexel access token: {error}"
411+
)
412+
397413
access_token = token.get("access_token")
398414
if not access_token:
399415
raise InvalidTokenException("No Rexel access token provided.")
@@ -403,9 +419,9 @@ async def _exchange_token(self, payload: Mapping[str, str]) -> None:
403419
self.context.refresh_token = token.get("refresh_token")
404420
expires_in = token.get("expires_in")
405421
if expires_in:
406-
self.context.expires_at = datetime.datetime.now() + datetime.timedelta(
407-
seconds=cast(int, expires_in) - 5
408-
)
422+
self.context.expires_at = datetime.datetime.now(
423+
datetime.UTC
424+
) + datetime.timedelta(seconds=cast(int, expires_in) - 5)
409425

410426
@staticmethod
411427
def _ensure_consent(access_token: str) -> None:

pyoverkiz/client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ def __init__(
166166
if self.server_config.type == APIType.LOCAL and verify_ssl:
167167
# To avoid security issues while authentication to local API, we add the following authority to
168168
# our HTTPS client trust store: https://ca.overkiz.com/overkiz-root-ca-2048.crt
169-
self._ssl = SSL_CONTEXT_LOCAL_API
169+
# Create a copy of the SSL context to avoid mutating the shared global context
170+
self._ssl = ssl.SSLContext(SSL_CONTEXT_LOCAL_API.protocol)
171+
self._ssl.load_verify_locations(
172+
cafile=os.path.dirname(os.path.realpath(__file__))
173+
+ "/overkiz-root-ca-2048.crt"
174+
)
170175

171176
# Disable strict validation introduced in Python 3.13, which doesn't
172177
# work with Overkiz self-signed gateway certificates

pyoverkiz/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ def create_server_config(
3737
configuration_url: str | None = None,
3838
) -> ServerConfig:
3939
"""Generate server configuration with the provided endpoint and metadata."""
40+
# ServerConfig.__init__ handles the enum conversion, but mypy doesn't see it
41+
# due to attrs @define decorator, so we need type: ignore comments.
4042
return ServerConfig(
41-
server=server
42-
if isinstance(server, Server) or server is None
43-
else Server(server),
43+
server=server, # type: ignore[arg-type]
4444
name=name,
4545
endpoint=endpoint,
4646
manufacturer=manufacturer,
4747
configuration_url=configuration_url,
48-
type=type if isinstance(type, APIType) else APIType(type),
48+
type=type, # type: ignore[arg-type]
4949
)
5050

5151

0 commit comments

Comments
 (0)