Extract the panel snapshot builder into core (task 9047a97b) #148

Member

Task 9047a97b — Extract the panel snapshot builder into core

Done-condition: core exposes a pure, HA-free snapshot builder (core/snapshot.py) that, given the database, settings, a date and a timezone, returns the household snapshot in the panel shape (children in sort order, each child's instances in the documented payload shape with missed instances omitted, per-child counts, presence, cycle day). The HA coordinator's _async_update_data is refactored to call it, behaviour unchanged. A test asserts the builder's output for a seeded household matches the coordinator's snapshot.

Changes

  • custom_components/nestquest/core/snapshot.py (NEW): the pure pieces — QuestInstanceView, ChildDaySnapshot, NestQuestSnapshot, _cycle_day, and async build_snapshot(database, settings, today, time_zone) -> NestQuestSnapshot. No homeassistant import. The assembly logic that lived inline in the coordinator's _async_update_data now lives here.
  • custom_components/nestquest/coordinator.py: now a thin DataUpdateCoordinator wrapper. _async_update_data resolves the local timezone + settings and delegates to build_snapshot. The dataclasses and _cycle_day are re-exported so existing sensor.py / binary_sensor.py imports (from .coordinator import ChildDaySnapshot, NestQuestCoordinator, QuestInstanceView) keep working unchanged — entity_ids and per-child instance attribute payload shape unchanged.
  • custom_components/nestquest/__init__.py: passes the entry's settings into the coordinator constructor (the builder's forward-compatible settings param).
  • tests/test_sensors.py, tests/test_entity_matrix.py: the two tests that monkeypatched derive_state on the coordinator module now patch custom_components.nestquest.core.snapshot.derive_state (the module that now owns the call). Behaviour identical.
  • tests/test_core_snapshot.py (NEW): seeds a household (3 active + 1 inactive child, a shared daily quest, today's instances, one completed instance, a 1-week presence schedule) and asserts build_snapshot's output is byte-for-byte equal to the coordinator's snapshot for the same database, date and timezone — plus shape invariants (sort order, inactive omitted, missed omitted, counts, presence, cycle day).

Why

The API's panel snapshot route (next task) needs this snapshot, but the assembly lived only in coordinator.py, which is HA-coupled (DataUpdateCoordinator). Both consumers must share ONE implementation.

Verification

  • uv run pytest -q → 1588 passed (baseline 1587 + 1 new test).
  • test_core_isolation.py green: no homeassistant import, no climb-out, no bare core import under core/.
  • No repo-root core/; no top-level bare core import under the integration.
  • No new re-export shim added (core/snapshot.py is a new module; the coordinator re-export is internal to the integration).

No merge requested. Do not mark the Maestro task done.

## Task 9047a97b — Extract the panel snapshot builder into core **Done-condition:** core exposes a pure, HA-free snapshot builder (`core/snapshot.py`) that, given the database, settings, a date and a timezone, returns the household snapshot in the panel shape (children in sort order, each child's instances in the documented payload shape with missed instances omitted, per-child counts, presence, cycle day). The HA coordinator's `_async_update_data` is refactored to call it, behaviour unchanged. A test asserts the builder's output for a seeded household matches the coordinator's snapshot. ## Changes - **`custom_components/nestquest/core/snapshot.py`** (NEW): the pure pieces — `QuestInstanceView`, `ChildDaySnapshot`, `NestQuestSnapshot`, `_cycle_day`, and async `build_snapshot(database, settings, today, time_zone) -> NestQuestSnapshot`. No `homeassistant` import. The assembly logic that lived inline in the coordinator's `_async_update_data` now lives here. - **`custom_components/nestquest/coordinator.py`**: now a thin `DataUpdateCoordinator` wrapper. `_async_update_data` resolves the local timezone + settings and delegates to `build_snapshot`. The dataclasses and `_cycle_day` are re-exported so existing `sensor.py` / `binary_sensor.py` imports (`from .coordinator import ChildDaySnapshot, NestQuestCoordinator, QuestInstanceView`) keep working unchanged — entity_ids and per-child instance attribute payload shape unchanged. - **`custom_components/nestquest/__init__.py`**: passes the entry's `settings` into the coordinator constructor (the builder's forward-compatible settings param). - **`tests/test_sensors.py`, `tests/test_entity_matrix.py`**: the two tests that monkeypatched `derive_state` on the coordinator module now patch `custom_components.nestquest.core.snapshot.derive_state` (the module that now owns the call). Behaviour identical. - **`tests/test_core_snapshot.py`** (NEW): seeds a household (3 active + 1 inactive child, a shared daily quest, today's instances, one completed instance, a 1-week presence schedule) and asserts `build_snapshot`'s output is byte-for-byte equal to the coordinator's snapshot for the same database, date and timezone — plus shape invariants (sort order, inactive omitted, missed omitted, counts, presence, cycle day). ## Why The API's panel snapshot route (next task) needs this snapshot, but the assembly lived only in `coordinator.py`, which is HA-coupled (`DataUpdateCoordinator`). Both consumers must share ONE implementation. ## Verification - `uv run pytest -q` → **1588 passed** (baseline 1587 + 1 new test). - `test_core_isolation.py` green: no `homeassistant` import, no climb-out, no bare `core` import under `core/`. - No repo-root `core/`; no top-level bare `core` import under the integration. - No new re-export shim added (`core/snapshot.py` is a new module; the coordinator re-export is internal to the integration). No merge requested. Do not mark the Maestro task done.
dev_agent added 1 commit 2026-09-21 17:29:24 -04:00
The household snapshot assembly that lived inline in the coordinator's
_async_update_data is extracted into a pure, Home-Assistant-free
core/snapshot.py: the QuestInstanceView / ChildDaySnapshot /
NestQuestSnapshot dataclasses, the _cycle_day helper, and a new async
build_snapshot(database, settings, today, time_zone) builder.

The coordinator is now a thin DataUpdateCoordinator wrapper that
resolves the local timezone and settings and delegates to
build_snapshot; the dataclasses and _cycle_day are re-exported from the
coordinator module so existing sensor.py / binary_sensor.py imports keep
working unchanged (entity_ids and per-child instance attribute payload
shape unchanged). The API service's panel snapshot route (next task)
shares this one implementation.

Tests that monkeypatched derive_state on the coordinator module now
patch core.snapshot.derive_state (the module that now owns the call).
A new test seeds a household and asserts build_snapshot's output equals
the coordinator's snapshot for the same database and date. Suite green
at 1588.
overseer added 1 commit 2026-09-21 17:37:20 -04:00
PR #148 review (CHANGES_REQUIRED) follow-up:

- Move the panel payload dict shaper (_instance_payload) from the
  HA-importing sensor.py into core/snapshot.py as instance_payload, and
  have sensor.py import it from core. The API panel route now shares ONE
  shaping implementation with the HA sensor layer (done -> completed,
  on_time passthrough, missed omitted unless include_missed).
- build_snapshot takes the HA-local now explicitly instead of
  reading datetime.now(time_zone) internally, so date and time come from
  a single clock read and the overdue check is pinned in tests. The
  coordinator passes the same now it already resolved; time_zone is
  dropped (now is timezone-aware).
- Strengthen tests/test_core_snapshot.py with explicit literal expected
  values instead of self-equality: an AWAY child (2-week absent-week
  schedule + a presence override => next_present), a due-timed quest
  exercised as overdue under a pinned 12:00 now, a completed quest
  (done -> completed, on_time True), cycle_day == 2, and a constructed
  missed view proving omission from the panel payload and retention in
  the admin payload.
- Docstring cleanups: state plainly that settings is currently
  unused (no 'wired' implication); correct the coordinator docstring
  (settings are passed in, not resolved off the entry); trim the
  coordinator __all__ to what other modules actually import.
- Derive the seed/materialize date from coordinator._local_now() so the
  seed date and snapshot date cannot diverge near midnight; drop the
  redundant bo/ghost/list_children leftovers from the prior seed.
overseer added 1 commit 2026-09-21 17:40:56 -04:00
build_snapshot now takes now ONLY and derives today = now.date()
inside (P3-1): a single source of truth so the calendar date and
wall-clock time can never disagree across two reads. Coordinator
call site updated.

The expected completed_at in tests/test_core_snapshot.py is now built
from pinned_now.astimezone(datetime.timezone.utc).isoformat() instead
of a literal '+00:00' UTC string (P3-2), so the assertion is
timezone-independent rather than relying on the hass fixture's UTC
timezone.

Pin the coordinator's clock to pinned_now, run async_refresh, and
assert coordinator.data == built (the frozen dataclasses make == a
full deep compare) — the REQUIRED tie-in that proves the coordinator
wrapper actually delegates to build_snapshot, which the literal
assertions alone could not (P2).
dev_agent merged commit ca477125d6 into feature/cd03735b-2498-4b34-b84c-c1cb2b271ba7-nestquest-api-service 2026-09-21 17:43:52 -04:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/NestQuest#148