Skip to content

Migrate to external rohlik-api package, remove internal client - #76

Merged
dvejsada merged 6 commits into
masterfrom
claude/external-api-package
Jun 28, 2026
Merged

Migrate to external rohlik-api package, remove internal client#76
dvejsada merged 6 commits into
masterfrom
claude/external-api-package

Conversation

@dvejsada

Copy link
Copy Markdown
Owner

Summary

Replaces the in-tree RohlikCZAPI client with the published rohlik-api PyPI package and reworks the integration to consume the package's service-based API and typed models directly (no compatibility wrapper). Net ~920 lines deleted.

What changed

  • manifest — depend on rohlik-api==0.1.0.
  • hub — drive RohlikAPI directly:
    • one HA-managed aiohttp session per account (its own cookie jar → keeps multiple accounts isolated), lazy login, transparent 401 re-auth handled by the package;
    • map InvalidCredentialsError / RohlikAPIErrorConfigEntryAuthFailed / UpdateFailed;
    • reimplement the order-history enrichment orchestration on the package primitives (orders.get_all_delivered, orders.get_detail, products.get_categories);
    • refresh_slots via delivery.get_next_slots; close the session on unload.
  • config_flow — validate credentials via RohlikAPI.login().
  • sensor / todo — consume the typed Cart model (attribute access) instead of a dict.
  • services — convert returned models (Cart, SearchResults, ShoppingList) to dicts only at the HA service-response boundary, so user-facing service response shapes are unchanged.
  • removedrohlik_api.py (706 lines) and errors.py.

Compatibility

rohlik-api requires Python ≥ 3.13, which Home Assistant mandates from 2025.2. This PR bumps the HACS minimum (hacs.json) and the README badge to 2025.2 so installs can't land on a 3.12 runtime where the dependency would fail to install.

Testing

  • Full suite green: 21 passed (the internal-client unit tests were removed — that surface is now covered by the package's own repository).
  • All integration modules import cleanly against the installed package.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 2 commits June 27, 2026 20:59
Replace the in-tree 706-line RohlikCZAPI client (and its errors module)
with the published rohlik-api PyPI package, and rework the integration to
consume the package's service-based API and typed models directly:

- manifest: depend on rohlik-api==0.1.0.
- hub: drive RohlikAPI (per-account HA-managed session with its own cookie
  jar, lazy login + transparent 401 re-auth); map InvalidCredentialsError /
  RohlikAPIError to ConfigEntryAuthFailed / UpdateFailed; reimplement the
  order-history enrichment orchestration on top of the package primitives
  (orders.get_all_delivered / orders.get_detail / products.get_categories);
  refresh_slots via delivery.get_next_slots; close the session on unload.
- config_flow: validate credentials via RohlikAPI.login().
- sensor/todo: consume the Cart model (attributes) instead of a dict.
- services: convert returned models to dicts at the HA response boundary.
- tests: patch the new client surface; cart fixture is now a Cart model;
  drop the internal-client unit tests (now covered by the package's repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe
The rohlik-api dependency declares requires-python >=3.13. HA mandates
Python 3.13 from 2025.2 onward, so bump the HACS minimum and README badge
to match and avoid installs failing on a 3.12 runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe

Copy link
Copy Markdown
Owner Author

@claude review


Generated by Claude Code

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Claude finished @dvejsada's task in 3m 10s —— View job


PR Review

  • Gather context
  • Read changed files
  • Review hub.py
  • Review config_flow.py
  • Review sensor.py, todo.py, services.py
  • Review tests
  • Post review findings

Overall this is a clean, well-reasoned migration. The session-per-account isolation, lazy login, and transparent 401 re-auth are all correct HA patterns. The lock split (store lock vs. enrich lock) is well thought out. A few issues worth addressing below.

Medium Issues

1. Session leak in async_close if client.close() throws (hub.py:754-759)

async def async_close(self) -> None:
    await self._client.close()   # ← if this raises, session is never closed
    await self._session.close()

If the package's close() call fails (e.g. logout request times out), _session.close() is skipped, leaking the HA-managed aiohttp session. Fix with try/finally:

async def async_close(self) -> None:
    try:
        await self._client.close()
    finally:
        await self._session.close()

Fix this →

2. add_to_cart may return a non-serializable service response (hub.py:762-768, services.py:40-46)

# hub.py
added = await self._client.cart.add_items([...])
return {"added_products": added}   # ← added is returned raw, no asdict()

The PR correctly uses asdict() for Cart, SearchResults, and ShoppingList at the service boundary — but the add_to_cart result wraps added directly. If cart.add_items() returns a dataclass or other non-serializable type, HA's service response serialization will fail. This should be verified against the rohlik-api package's return type; if it's not a plain Python primitive/list/dict, apply asdict() here as well.

Minor Issues

3. Progress callback fires at i=0 in _fetch_product_categories (hub.py:735)

if progress_callback and i % 50 == 0:   # ← triggers immediately at i=0
    await progress_callback(i, total)

The first persistent notification immediately reports 0/N (0%) before any work is done. Compare with _fetch_order_items line 709 which correctly guards with if i and i % 50 == 0:. Same fix applies here.

Fix this →

4. Legacy Optional / Dict typing imports (hub.py:8)

from typing import Any, Optional, Dict

Python 3.13 is now the minimum. Optional[X]X | None, Dictdict. These are unused as class-level annotations (the existing code already uses X | None style), but they're still used in the new service method return hints on lines 762, 782, etc. A simple cleanup would be to drop the import and use lowercase built-ins.

5. config_flow.py doesn't surface network errors distinctly

validate_input only catches InvalidCredentialsError; any RohlikAPIError (server down, timeout, etc.) bubbles up to except Exception → errors["base"] = "unknown". Consider catching RohlikAPIError specifically to show a cannot_connect error string instead of unknown.

6. Test fixture bags shape doesn't match sensor (tests/fixtures_data.py:33, sensor.py:775)

The fixture sets "bags": {"data": {"reusableBagsCount": 0}}, but BagsAmountSensor reads data["bags"].get('current', 0) — the key 'current' doesn't exist in the fixture. The sensor will silently return 0. This is pre-existing and the tests pass because no test asserts the bags sensor value, but it means the fixture doesn't faithfully represent the real API shape for this key. Worth fixing the fixture to match the real shape (or vice versa).


Positive Highlights

  • Session-per-account via async_create_clientsession with its own cookie jar is the right HA idiom.
  • Lock split (_store_lock for in-memory mutations, _enrich_lock across network I/O) is clean and well-documented.
  • finally: await client.close() in validate_input is correct one-shot pattern.
  • ConfigEntryAuthFailed / UpdateFailed exception mapping is idiomatic.
  • asdict() used correctly at service boundaries for SearchResults, ShoppingList, Cart.
  • 21 tests still green, net ~920 lines deleted — excellent signal-to-noise ratio.

claude added 2 commits June 27, 2026 21:21
The integration now depends on rohlik-api at runtime; the test workflow
installs requirements_test.txt only, so add the package there to fix the
ModuleNotFoundError during test collection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe
- async_close: close the HA-managed session in a finally so a failing
  client.close() (e.g. logout timeout) can't leak it.
- config_flow: map RohlikAPIError to the existing cannot_connect error
  string instead of falling through to 'unknown' (both user + reauth steps);
  add a test for it.
- _fetch_product_categories: don't fire the progress callback at i=0.
- hub: drop unused Optional/Dict typing imports, use lowercase dict.
- tests: make the bags fixture match the shape the sensor reads (current/max).

cart.add_items returns list[int] (plain primitives), so the add_to_cart
service response needs no asdict() - left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe

Copy link
Copy Markdown
Owner Author

Thanks for the thorough review — addressed in 0c61720:

# Finding Action
1 Session leak if client.close() throws async_close now closes the HA session in a finally
2 add_to_cart response serialization ⏭️ Verified cart.add_items() returns list[int] (plain primitives) — already serializable, no asdict() needed
3 Progress callback fires at i=0 ✅ Guarded with and i, matching _fetch_order_items
4 Legacy Optional/Dict imports ✅ Dropped (Optional was unused); return hints use dict
5 Network errors → unknown RohlikAPIError now maps to the existing cannot_connect string (user + reauth steps) + a test
6 bags fixture shape mismatch ✅ Fixture now matches what BagsAmountSensor reads (current/max)

Suite green at 22 passed (added the cannot_connect flow test). Also note: the earlier CI red was a missing test-env dependency, fixed in c367e3a by adding rohlik-api to requirements_test.txt.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

@claude review


Generated by Claude Code

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Claude finished @dvejsada's task in 2m 31s —— View job


PR Review

  • Gather context
  • Read hub.py (current state after 0c61720)
  • Read config_flow.py
  • Read sensor.py, todo.py, services.py
  • Read tests and fixtures
  • Post review findings

All six findings from the prior round are correctly resolved. The code is in excellent shape. Two minor nits remain from this pass:

Minor Issues

1. services.py still has legacy List / Dict typing (services.py:4, 33, 48, 72, 94, 107)

hub.py was cleaned up but services.py still imports and uses the capitalised forms:

from typing import List, Dict, Any          # line 4

async def async_add_to_cart_service(call: ServiceCall) -> List[int]:   # line 33
async def async_search_product_service(call: ServiceCall) -> Dict[str, Any]:  # line 48
# ...

Python 3.13 is now the minimum, so these should be list[int], dict[str, Any] etc. — same cleanup that was applied to hub.py.

Fix this →

2. async_add_to_cart_service return type annotation is wrong (services.py:33)

async def async_add_to_cart_service(call: ServiceCall) -> List[int]:
    ...
    result = await account.add_to_cart(product_id, quantity)
    return result   # hub.add_to_cart returns {"added_products": list[int]}

The service returns the dict from hub.add_to_cart ({"added_products": [...]}), not a list[int]. The annotation should be dict[str, Any] (or dict at minimum).

Fix this →


Verified Correct

Prior finding Status
Session leak in async_close try/finally in place (hub.py:756–762)
add_to_cart serialization cart.add_items() returns list[int]; confirmed primitive
Progress callback fires at i=0 ✅ Guarded with and i (hub.py:735)
Legacy Optional/Dict in hub.py ✅ Dropped; from typing import Any only
Network errors → unknown RohlikAPIErrorcannot_connect, both user and reauth steps
bags fixture shape mismatch ✅ Fixture now has {"current": 0, "max": 0} matching sensor

Positive Highlights

  • config_flow.py three-tier error handling (invalid_auth / cannot_connect / unknown) is clean and complete, with the new test_user_flow_cannot_connect test covering it.
  • validate_input correctly creates a one-shot client with its own session and tears it down in finally, while the coordinator path uses a long-lived HA-managed session with its own cookie jar.
  • Test coverage is solid — 22 tests, all meaningful (coordinator lifecycle, reauth, slot refresh, legacy store migration, enrichment lock split).
  • Net ~920 lines deleted with no loss of functionality is a strong outcome.

The two nits above are cosmetic and won't affect runtime behaviour. This is otherwise merge-ready.

Drop legacy List/Dict typing imports in favour of lowercase built-ins, and
fix async_add_to_cart_service's return hint (it returns the dict from
hub.add_to_cart, not a list[int]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe

Copy link
Copy Markdown
Owner Author

Both cosmetic nits from the last review are fixed in bf394ff:

  1. services.py legacy List/Dict typing → lowercase list/dict built-ins (import trimmed to Any).
  2. async_add_to_cart_service return hint corrected to dict[str, Any] (it returns {"added_products": [...]}, not list[int]).

CI is green on bf394ff (pytest + validate), 22 tests passing. With both review rounds fully addressed and the reviewer's "merge-ready" assessment, this is ready to merge whenever you'd like.


Generated by Claude Code

0.2.0 fixes delivery.get_next_slots() returning None for accounts whose
login response omits a delivery address (it now falls back to the saved
address list). The integration already calls get_next_slots(), so it picks
this up with no code change - such accounts will now get their slot sensors.

get_data() and all return types are unchanged (no breaking changes). The
new weekly-deals helper (products.get_week_sales) and other 0.2.0 additions
are intentionally not surfaced in the integration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqvRozwrnqt3NgyaYeBZe
@dvejsada
dvejsada merged commit f0925eb into master Jun 28, 2026
4 checks passed
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