chore: retro-review PR #86 — feat(providers): add OpenElectricity wholesale-price client (Phase 7 PR-2)#118
Conversation
…PR-2) Standalone async client for OpenElectricity v4 — not yet wired into the coordinator or config flow (that's PR-2 part 2 / Plan 07-02b). Foundation for Phase 8 Silver-compliance handlers and Phase 9 external-statistics integration. * ``custom_components/pricehawk/providers/openelectricity.py`` — new module exposing ``OpenElectricityPriceSource`` async client + ``WholesalePrice`` frozen dataclass. CC BY-NC 4.0 attribution baked into every result per research §1.3. * ``manifest.json:requirements`` pinned to ``openelectricity>=0.10.1,<0.11`` — minor-bounded per the SDK README's "currently under active development" notice. Audit invariants applied (07-02-AUDIT.md): * M1 — API key never appears in ``__repr__`` output or log messages. ``_scrub()`` filters the full key plus its 8-char prefix from any text passed to ``_LOGGER.warning``. ``test_log_scrubs_api_key_from_sdk_error`` uses ``caplog`` to verify against a leaky SDK exception. * M2 — ``asyncio.timeout(_FETCH_TIMEOUT_SECONDS=30.0)`` bounds every SDK call. Hung connection → ``None`` + WARNING, never an HA-wide stall. * M3 — Missing ``openelectricity`` install raises ``ConfigEntryNotReady`` (HA retries setup), not ``ImportError`` (permanent crash). Lazy import inside the async method. * S1 — Auth-error detection is belt-and-braces: message-string match plus exception-class-name probe. Defensive against the SDK's active-development error-message format drift. * S2 — HTTP 429 rate-limit has a distinct branch: ``None`` + WARNING saying "rate-limited", last-good cache preserved, no spurious ``ConfigEntryAuthFailed``. * S5 — Module is intentionally NOT exported from ``providers/__init__.py``. ``OpenElectricityPriceSource`` is a wholesale-price source, not a ``Provider`` Protocol implementation. Consumers import directly. Implementation diverged from plan pseudo-code: real ``openelectricity==0.10.1`` ``TimeSeriesResponse`` is nested three levels deep (``data`` → ``results`` → ``data``), not flat. ``_extract_latest_for_region`` walks the nested structure and returns tz-aware UTC. We skip ``response.to_records()`` because it emits naive-local timestamps. 12 contract tests (``tests/test_openelectricity_provider.py``) cover AC-1 through AC-7e: happy-path NEM, happy-path WEM, attribution-verbatim, 401 mapping, generic-error cache preservation, empty-data, invalid region, empty API key, timeout, missing SDK, 429 rate limit, repr redaction, log scrub. Test suite: 815 → 827 passed. ``tests/conftest.py`` gained one stub: ``ConfigEntryAuthFailed`` (was only ``ConfigEntryNotReady`` before). Necessary because the new module is the first user of the auth-failed exception in this project. Decisions D-P7-5 through D-P7-8 logged to DECISIONS.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @Artic0din's task in 16m 14s —— View job Code Review — PR #86: feat(providers): add OpenElectricity wholesale-price client (Phase 7 PR-2)Tasks:
Overall AssessmentThis is a well-engineered standalone module. The audit invariants are explicit in the docstring, the security properties (key redaction, timeout bounding, lazy import) are correct, and the test suite covers every named AC. A few actionable findings below. Findings🔴 Bug —
|
|
Claude finished @Artic0din's task in 13m 19s —— View job Code Review — PR #86: feat(providers): add OpenElectricity wholesale-price client
Overall this is a well-structured, defensively-written provider module. The audit-invariant approach (M1/M2/M3 tagged inline), the frozen dataclass cross-PR contract, and the scrubbing layer are all solid patterns. The test file maps cleanly to acceptance criteria and the SDK-stub approach avoids a real network dependency. Three correctness concerns worth addressing before the wiring PR (07-02b). Correctness issues1.
# Current — 403 falls through to ConfigEntryAuthFailed
if (
"401" in msg
or "unauthor" in msg
or "invalid api key" in msg
or "forbidden" in msg # <-- 403 ≠ 401
):
return TrueSuggested fix: strip 2. if len(self._api_key) >= 8:
scrubbed = scrubbed.replace(self._api_key[:8], _REDACTED_PREFIX)For a key like 3. candidates.append((float(val), ts))The OpenElectricity API can return # Suggested guard
val_f = float(val)
if not math.isfinite(val_f):
continue
candidates.append((val_f, ts))Test gaps4. The
5. Timezone-naive timestamp fallback ( There's a guard for naive timestamps from the SDK but no test that exercises the branch. Worth one test with 6. No test for the 403-vs-401 boundary Given the concern in finding #1 above, a test with Minor observations
Summary
|
|
Retro-review complete. Findings (if any) tracked in fix/retro-review-batch (#143) or noted in .planning/retro-review-findings.md. Closing synthetic PR + cleaning branches. |
Synthetic retro-review of merged PR #86. Diff = original
bdf6f74..40fff3e(same as original PR). Do NOT merge; close after @claude review posts.