Skip to content

Conversation

@jschlyter
Copy link
Member

@jschlyter jschlyter commented Sep 11, 2025

Summary by CodeRabbit

  • New Features

    • Optional injection of a custom async HTTP client, automatic token expiry detection, and automatic Authorization header management with safer shutdown.
  • Refactor

    • Networking migrated to a modern async HTTP client with centralized token refresh, retry-on-auth-failure, unified request/response handling, and improved error propagation.
  • Chores

    • Added the httpx dependency to support the updated networking stack.

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Migrates the external API client from aiohttp to httpx, adds optional dependency injection for an httpx.AsyncClient, centralizes token refresh under a lock with JWT expiry decoding and skew, adds a retry-on-401 flow, updates request helpers and error handling, ensures proper client shutdown with aclose(), and adds httpx to dependencies.

Changes

Cohort / File(s) Summary of changes
HTTP client migration & token handling
chargeamps/external.py
Replaces aiohttp usage with httpx.AsyncClient; constructor gains httpx_client and _owns_client; adds token_lock and token_skew, decodes JWT exp for expiry, and sets Authorization header after login/refresh; centralizes token logic in _exclusive_ensure_token; adds _httpx_retry to refresh token and retry once on 401; _post/_get/_put now return httpx.Response, call response.raise_for_status(), parse via response.json(), handle httpx.HTTPStatusError/httpx.RequestError, and remove SSL flag handling; shutdown() calls aclose() when owning the client.
Dependency update
pyproject.toml
Adds dependency httpx>=0.28.1.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Client as ChargeAmpsExternalClient
  participant HTTP as httpx.AsyncClient
  participant API as ChargeAmps API
  note over Client: Construct with optional injected httpx.AsyncClient

  App->>Client: request resource
  Client->>Client: check token expiry under token_lock
  alt token valid
    Client->>HTTP: GET /resource (Authorization)
  else token expired
    Client->>HTTP: POST /refresh (refresh_token)
    HTTP->>API: network
    API-->>HTTP: 200 {access_token, ...}
    HTTP-->>Client: Response
    Client->>Client: update token, set Authorization
    Client->>HTTP: retry original GET /resource
  end
  HTTP->>API: network
  API-->>HTTP: 200 JSON
  HTTP-->>Client: Response
  Client-->>App: parsed data

  App->>Client: shutdown()
  Client->>HTTP: aclose() (if _owns_client)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Replace aiohttp with httpx #39 — Implements the same migration from aiohttp to httpx, adds httpx_client parameter, _httpx_retry, and updates _post/_get/_put signatures; likely the same or closely related change.

Poem

I hop through threads where tokens gleam,
New streams now speak in httpx dream.
I guard the lock, refresh with glee,
Retry once more, then set it free.
Soft paws close clients — all is clean. 🥕

Pre-merge checks and finishing touches and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Replace aiohttp with httpx" is concise, a single sentence, and directly describes the primary change in the PR — migrating from aiohttp to httpx (notably in chargeamps/external.py and the added httpx dependency in pyproject.toml), so it accurately summarizes the main change for reviewers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch httpx

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d243fc7 and cf2c1b1.

📒 Files selected for processing (1)
  • chargeamps/external.py (8 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
chargeamps/external.py (4)

63-74: Also handle network errors on refresh.

Currently only HTTPStatusError is handled; transient network errors (DNS, timeouts) raise RequestError.

-            except httpx.HTTPStatusError:
+            except (httpx.HTTPStatusError, httpx.RequestError):
                 self._logger.warning("Token refresh failed")
                 self._token = None
                 self._refresh_token = None

80-93: Also handle network errors on login.

Mirror refresh handling for resilience.

-            except httpx.HTTPStatusError as exc:
+            except (httpx.HTTPStatusError, httpx.RequestError) as exc:
                 self._logger.error("Login failed")
                 self._token = None
                 self._refresh_token = None
                 self._token_expire = 0
                 raise exc

227-233: Bug: sending JSON string instead of empty object.

json="{}" serializes to a JSON string, not {}. Many APIs expect an empty object.

-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})
@@
-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})

1-233: Update CLI exception handling to catch httpx.HTTPStatusError

chargeamps/cli.py currently imports and catches aiohttp.ClientResponseError (around line 12 and the except at ~349) while the external client uses httpx; httpx errors will bypass the handler — change the except and ensure httpx is imported.

Suggested patch:

- from aiohttp.client_exceptions import ClientResponseError
+ import httpx
+ from aiohttp.client_exceptions import ClientResponseError
-    except ClientResponseError as exc:
+    except httpx.HTTPStatusError as exc:
         sys.stderr.write(str(exc))
🧹 Nitpick comments (5)
chargeamps/external.py (5)

3-11: Drop unused asyncio import.

asyncio isn’t referenced; the # noqa can go too.

-import asyncio  # noqa
 import logging
 import time

49-57: Refresh slightly early to avoid edge-expiry.

A small skew reduces 401s due to clock drift.

-        if self._token_expire > time.time():
+        SKEW = 30  # seconds
+        if self._token_expire - SKEW > time.time():
             return

98-107: Be defensive when decoding JWT exp and update headers atomically.

Ensure exp is int and always refresh auth headers in one shot (keeps apiKey if used).

-        token_payload = jwt.decode(self._token, options={"verify_signature": False})
-        self._token_expire = token_payload.get("exp", 0)
-
-        self._headers["Authorization"] = f"Bearer {self._token}"
+        token_payload = jwt.decode(self._token, options={"verify_signature": False})
+        exp = token_payload.get("exp", 0)
+        self._token_expire = int(exp) if isinstance(exp, (int, float)) else 0
+
+        self._headers = {**self._headers, "Authorization": f"Bearer {self._token}"}

108-134: Optional: serialize default headers once into the client.

If you consistently need apiKey and Authorization, consider creating the client with headers=self._headers and updating via self._httpx_client.headers.update(...) after login. This reduces per-call header merging.


80-93: Add a lightweight token-refresh lock to avoid concurrent refresh storms.

Multiple concurrent requests can race into refresh/login. A single asyncio.Lock around the critical section prevents duplicate work.

# at __init__
self._token_lock = asyncio.Lock()

# wrap core of _ensure_token
async with self._token_lock:
    # existing logic...
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db87e71 and e742269.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • chargeamps/external.py (9 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-12-17T22:36:20.497Z
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: chargeamps/external.py:218-223
Timestamp: 2024-12-17T22:36:20.497Z
Learning: In `chargeamps/external.py`, ensure that docstrings and error handling remain consistent with the rest of the module, avoiding the addition of detailed documentation or extensive error handling to individual functions unless it's applied uniformly.

Applied to files:

  • chargeamps/external.py
🧬 Code graph analysis (1)
chargeamps/external.py (4)
chargeamps/base.py (5)
  • shutdown (28-29)
  • get_chargepoints (32-34)
  • get_chargepoint_status (37-39)
  • get_chargepoint_settings (42-44)
  • ChargeAmpsClient (26-63)
tests/test_client.py (1)
  • test_external_client (7-10)
examples/test.py (1)
  • test (7-29)
chargeamps/cli.py (1)
  • main_loop (175-358)
🔇 Additional comments (7)
pyproject.toml (1)

24-30: Do not remove aiohttp — it's still imported/used; migrate usages before dropping the dependency.

Evidence:

  • chargeamps/cli.py:12 — from aiohttp.client_exceptions import ClientResponseError
  • pyproject.toml:24 — "aiohttp>=3.12.15"
  • uv.lock contains aiohttp package entries

Either migrate the CLI usage to httpx (update exception handling) and then remove aiohttp + regenerate the lockfile, or keep aiohttp.

Likely an incorrect or invalid review comment.

chargeamps/external.py (6)

140-142: LGTM — response parsing to models is correct.


159-161: LGTM — session list parsing looks good.


169-171: LGTM — single session payload handling is fine.


176-178: LGTM — status payload handling is fine.


183-185: LGTM — settings payload handling is fine.


201-203: LGTM — connector settings payload handling is fine.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
chargeamps/external.py (1)

239-243: json="{}" sends a JSON string, not an object — use {}

Passing a string results in ""{}"" on the wire. Use an empty object.

-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})
-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})

Also applies to: 244-248

🧹 Nitpick comments (5)
chargeamps/external.py (5)

63-74: Auth flow solid; consider small expiry skew to reduce edge 401s

Subtract a small skew from exp to avoid just-expired tokens triggering retries.

-        self._token_expire = token_payload.get("exp", 0)
+        # Add slight skew to avoid edge-of-expiry 401s
+        self._token_expire = max(0, int(token_payload.get("exp", 0)) - 30)

Also applies to: 80-96, 101-110


126-133: Drop duplicate raise_for_status in callers

_httpx_retry already raises; callers can just return its result.

-        response = await self._httpx_retry(self._httpx_client.post, url, headers, **kwargs)
-        response.raise_for_status()
-        return response
+        return await self._httpx_retry(self._httpx_client.post, url, headers, **kwargs)
-        response = await self._httpx_retry(self._httpx_client.get, url, headers, **kwargs)
-        response.raise_for_status()
-        return response
+        return await self._httpx_retry(self._httpx_client.get, url, headers, **kwargs)
-        response = await self._httpx_retry(self._httpx_client.put, url, headers, **kwargs)
-        response.raise_for_status()
-        return response
+        return await self._httpx_retry(self._httpx_client.put, url, headers, **kwargs)

Also applies to: 134-141, 142-149


153-157: Use comprehensions to simplify list building

Equivalent and more concise.

-        res = []
-        for chargepoint in response.json():
-            res.append(ChargePoint.model_validate(chargepoint))
-        return res
+        return [ChargePoint.model_validate(cp) for cp in response.json()]
-        res = []
-        for session in response.json():
-            res.append(ChargingSession.model_validate(session))
-        return res
+        return [ChargingSession.model_validate(s) for s in response.json()]

Also applies to: 173-176


211-217: Fix misleading docstrings

These methods operate on connector settings, not “owned chargepoints.”

-        """Get all owned chargepoints"""
+        """Get connector settings"""
-        """Get all owned chargepoints"""
+        """Set connector settings"""

Also applies to: 219-229


244-248: Add type hint for reboot parameter

Minor consistency polish.

-    async def reboot(self, charge_point_id) -> None:
+    async def reboot(self, charge_point_id: str) -> None:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba3aaee and 283f261.

📒 Files selected for processing (1)
  • chargeamps/external.py (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
chargeamps/external.py (2)
chargeamps/base.py (5)
  • shutdown (28-29)
  • get_chargepoints (32-34)
  • get_chargepoint_status (37-39)
  • get_chargepoint_settings (42-44)
  • ChargeAmpsClient (26-63)
chargeamps/models.py (4)
  • ChargePoint (30-38)
  • ChargingSession (79-86)
  • ChargePointStatus (58-61)
  • ChargePointSettings (64-67)
🔇 Additional comments (2)
chargeamps/external.py (2)

3-9: httpx migration imports — OK

Imports look correct for the switch to httpx.


32-41: Client DI, ownership tracking, and sane timeouts — LGTM

Nice DI, ownership flag, and conditional aclose().

Also applies to: 48-51

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
chargeamps/external.py (2)

218-225: Docstrings are incorrect for connector settings APIs

Both functions say “Get all owned chargepoints”; should describe connector settings.

Apply:

 async def get_chargepoint_connector_settings(
     self, charge_point_id: str, connector_id: int
 ) -> ChargePointConnectorSettings:
-    """Get all owned chargepoints"""
+    """Get connector settings for a charge point"""

 async def set_chargepoint_connector_settings(
     self, settings: ChargePointConnectorSettings
 ) -> None:
-    """Get all owned chargepoints"""
+    """Update connector settings for a charge point"""

Also applies to: 226-236


249-255: Sending JSON string instead of empty JSON object

Using json="{}" serializes to the string literal "{}". These endpoints likely expect an empty object. Also add typing to reboot().

Apply:

 async def remote_stop(self, charge_point_id: str, connector_id: int) -> None:
     """Remote stop chargepoint"""
     request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/connectors/{connector_id}/remotestop"
-    await self._put(request_uri, json="{}")
+    await self._put(request_uri, json={})

-async def reboot(self, charge_point_id) -> None:
+async def reboot(self, charge_point_id: str) -> None:
     """Reboot chargepoint"""
     request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/reboot"
-    await self._put(request_uri, json="{}")
+    await self._put(request_uri, json={})
♻️ Duplicate comments (3)
chargeamps/external.py (3)

39-43: Client ownership + sane timeouts — LGTM

Good call tracking ownership and setting conservative httpx timeouts.


51-54: Close only when owned — LGTM

Prevents surprising callers that inject their own AsyncClient.


118-132: 401 retry path: missing await and stale Authorization on second attempt

Second call isn’t awaited and headers aren’t rebuilt to include the refreshed Bearer token.

Apply:

 async def _httpx_retry(self, method, url, headers, **kwargs) -> httpx.Response:
     try:
         response = await method(url, headers=headers, **kwargs)
         response.raise_for_status()
         return response
     except httpx.HTTPStatusError as exc:
         if exc.response.status_code == 401:
             self._token = None
             self._token_expire = 0
             await self._ensure_token()
-            response = method(url, headers=headers, **kwargs)
+            # Ensure refreshed Authorization is used; keep caller headers.
+            # Prefer our Authorization over any stale one.
+            cleaned = {k: v for k, v in headers.items() if k.lower() != "authorization"}
+            headers = {**cleaned, **self._headers}
+            response = await method(url, headers=headers, **kwargs)
             response.raise_for_status()
             return response
         raise
🧹 Nitpick comments (2)
chargeamps/external.py (2)

56-66: Avoid redundant refresh/login with double-checked locking

Re-check validity inside the lock to prevent queued callers from repeating refresh/login work.

Apply:

 async def _ensure_token(self) -> None:
-    if self._token_expire - self._token_skew > time.time():
-        return
-    if self._token is None:
-        self._logger.info("Token not found")
-    elif self._token_expire > 0:
-        self._logger.info("Token expired")
-
-    async with self._token_lock:
-        await self._exclusive_ensure_token()
+    if self._token_expire - self._token_skew > time.time():
+        return
+    if self._token is None:
+        self._logger.info("Token not found")
+    elif self._token_expire > 0:
+        self._logger.info("Token expired")
+    async with self._token_lock:
+        # Another task may have refreshed while we awaited the lock.
+        if self._token_expire - self._token_skew > time.time():
+            return
+        await self._exclusive_ensure_token()

133-139: Drop duplicate raise_for_status() in callers

_httpx_retry already raises on non-2xx. These extra calls are redundant.

Apply:

 async def _post(self, path, **kwargs) -> httpx.Response:
     await self._ensure_token()
     headers = {**self._headers, **kwargs.pop("headers", {})}
     url = urljoin(self._base_url, path)
-    response = await self._httpx_retry(self._httpx_client.post, url, headers, **kwargs)
-    response.raise_for_status()
-    return response
+    return await self._httpx_retry(self._httpx_client.post, url, headers, **kwargs)

 async def _get(self, path, **kwargs) -> httpx.Response:
     await self._ensure_token()
     headers = {**self._headers, **kwargs.pop("headers", {})}
     url = urljoin(self._base_url, path)
-    response = await self._httpx_retry(self._httpx_client.get, url, headers, **kwargs)
-    response.raise_for_status()
-    return response
+    return await self._httpx_retry(self._httpx_client.get, url, headers, **kwargs)

 async def _put(self, path, **kwargs) -> httpx.Response:
     await self._ensure_token()
     headers = {**self._headers, **kwargs.pop("headers", {})}
     url = urljoin(self._base_url, path)
-    response = await self._httpx_retry(self._httpx_client.put, url, headers, **kwargs)
-    response.raise_for_status()
-    return response
+    return await self._httpx_retry(self._httpx_client.put, url, headers, **kwargs)

Also applies to: 141-147, 149-155

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 283f261 and 17a62d7.

📒 Files selected for processing (1)
  • chargeamps/external.py (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-12-17T22:36:20.497Z
Learnt from: yarcod
PR: kirei/python-chargeamps#30
File: chargeamps/external.py:218-223
Timestamp: 2024-12-17T22:36:20.497Z
Learning: In `chargeamps/external.py`, ensure that docstrings and error handling remain consistent with the rest of the module, avoiding the addition of detailed documentation or extensive error handling to individual functions unless it's applied uniformly.

Applied to files:

  • chargeamps/external.py
🧬 Code graph analysis (1)
chargeamps/external.py (5)
chargeamps/base.py (5)
  • shutdown (28-29)
  • get_chargepoints (32-34)
  • get_chargepoint_status (37-39)
  • get_chargepoint_settings (42-44)
  • ChargeAmpsClient (26-63)
chargeamps/models.py (4)
  • ChargePoint (30-38)
  • ChargingSession (79-86)
  • ChargePointStatus (58-61)
  • ChargePointSettings (64-67)
tests/test_client.py (1)
  • test_external_client (7-10)
examples/test.py (1)
  • test (7-29)
chargeamps/local.py (1)
  • ChargeAmpsLocalClient (6-7)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
chargeamps/external.py (1)

245-251: Fix JSON body type for empty objects (send {} not "{}")

json="{}" sends a JSON string; change to json={} to send an empty object.

File: chargeamps/external.py — replace at lines 245 and 250:

@@
     async def remote_stop(self, charge_point_id: str, connector_id: int) -> None:
         """Remote stop chargepoint"""
         request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/connectors/{connector_id}/remotestop"
-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})
@@
     async def reboot(self, charge_point_id) -> None:
         """Reboot chargepoint"""
         request_uri = f"/api/{API_VERSION}/chargepoints/{charge_point_id}/reboot"
-        await self._put(request_uri, json="{}")
+        await self._put(request_uri, json={})
🧹 Nitpick comments (6)
chargeamps/external.py (6)

108-116: Harden token parsing; tolerate refresh responses without refreshToken.

Avoid KeyError if refreshToken isn’t returned; also coerce exp to int.

-        response_payload = response.json()
-
-        self._token = response_payload["token"]
-        self._refresh_token = response_payload["refreshToken"]
-
-        token_payload = jwt.decode(self._token, options={"verify_signature": False})
-        self._token_expire = token_payload.get("exp", 0)
+        response_payload = response.json()
+        self._token = response_payload["token"]
+        self._refresh_token = response_payload.get("refreshToken", self._refresh_token)
+        token_payload = jwt.decode(self._token, options={"verify_signature": False})
+        self._token_expire = int(token_payload.get("exp", 0))

104-107: 不要 silently continue when no response; raise.

If both refresh/login failed to produce a response, return leaves client unusable. Prefer a clear error.

-        if response is None:
-            self._logger.error("No response")
-            return
+        if response is None:
+            raise RuntimeError("Token acquisition failed: no response from login/refresh")

39-43: Add connection limits to AsyncClient to avoid unbounded concurrency.

httpx defaults allow many concurrent connections; set conservative limits.

-        self._httpx_client = httpx_client or httpx.AsyncClient(
-            timeout=httpx.Timeout(connect=5.0, read=15.0, write=15.0, pool=5.0)
-        )
+        self._httpx_client = httpx_client or httpx.AsyncClient(
+            timeout=httpx.Timeout(connect=5.0, read=15.0, write=15.0, pool=5.0),
+            limits=httpx.Limits(max_connections=20, max_keepalive_connections=20),
+        )

214-231: Correct method docstrings (copy-paste).

Docstrings mention “Get all owned chargepoints” but these operate on connector settings.

@@
-    ) -> ChargePointConnectorSettings:
-        """Get all owned chargepoints"""
+    ) -> ChargePointConnectorSettings:
+        """Get connector settings for a specific charge point connector"""
@@
-    ) -> None:
-        """Get all owned chargepoints"""
+    ) -> None:
+        """Set connector settings for a specific charge point connector"""

153-160: Tighten loops into comprehensions.

Minor readability win.

@@
-        res = []
-        for chargepoint in response.json():
-            res.append(ChargePoint.model_validate(chargepoint))
-        return res
+        return [ChargePoint.model_validate(cp) for cp in response.json()]
@@
-        res = []
-        for session in response.json():
-            res.append(ChargingSession.model_validate(session))
-        return res
+        return [ChargingSession.model_validate(s) for s in response.json()]

Also applies to: 177-179


247-247: Add missing type hint.

Annotate charge_point_id for consistency.

-    async def reboot(self, charge_point_id) -> None:
+    async def reboot(self, charge_point_id: str) -> None:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa97c68 and d243fc7.

📒 Files selected for processing (1)
  • chargeamps/external.py (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-13T08:41:26.874Z
Learnt from: jschlyter
PR: kirei/python-chargeamps#39
File: chargeamps/external.py:43-43
Timestamp: 2025-09-13T08:41:26.874Z
Learning: In the Charge Amps External API v5, the `apiKey` header is only required for authentication endpoints (/login and /refreshToken), not for general API endpoints like chargepoints and settings which use Bearer token authorization.

Applied to files:

  • chargeamps/external.py
🧬 Code graph analysis (1)
chargeamps/external.py (4)
chargeamps/base.py (5)
  • shutdown (28-29)
  • get_chargepoints (32-34)
  • get_chargepoint_status (37-39)
  • get_chargepoint_settings (42-44)
  • ChargeAmpsClient (26-63)
chargeamps/models.py (4)
  • ChargePoint (30-38)
  • ChargingSession (79-86)
  • ChargePointStatus (58-61)
  • ChargePointSettings (64-67)
tests/test_client.py (1)
  • test_external_client (7-10)
examples/test.py (1)
  • test (7-29)
🔇 Additional comments (2)
chargeamps/external.py (2)

73-79: apiKey only on auth endpoints — correct.

Headers for refresh/login include apiKey while regular calls use Bearer. Matches the External API behavior.

Also applies to: 90-96


51-53: Client ownership, token lock, and 401 retry look solid.

  • Only closing owned client is correct.
  • Token operations are serialized with asyncio.Lock.
  • Single 401 refresh+retry with rebuilt headers is implemented correctly.

Also applies to: 118-133, 135-152

@jschlyter jschlyter merged commit 56cad20 into main Sep 13, 2025
6 of 7 checks passed
@jschlyter jschlyter deleted the httpx branch September 13, 2025 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants