mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 18:07:20 +01:00
Ninth batch of the 250-line sweep. Every Python file in the repo is now under cap (over-cap count 25 -> 18; the 18 remaining are all Dart). _foodbank.py 365 -> 210 (+ _foodbank_search, _foodbank_rebuild) test_averages.py 351 -> 222 (+ test_averages_report.py) _estimator.py 325 -> 137 (+ _estimator_off.py) _budget.py 323 -> 227 (+ _budget_biometrics, _budget_derived) test_foodbank.py 311 -> 117 (+ test_foodbank_rebuild.py) _averages.py 298 -> 174 (+ _averages_periods, _averages_report) test_budget_history.py 288 -> 162 (+ test_budget_history_io.py) The split boundaries are constrained by which module owns each conftest-patched state path, not by line count. BUDGET_FILE stays in _budget, FOOD_BANK_FILE in _foodbank: the derived/search/rebuild halves reach the file only through their parent's private helpers. Two of the new modules had copied FOOD_BANK_FILE into their headers as an incidental import, which is exactly the leak the guard is for -- both cleaned. test_state_redirect.py now covers all five redirected paths (FOOD_LOG_FILE, BUDGET_FILE, BUDGET_HISTORY_FILE, FOOD_BANK_FILE, MANUAL_BANK_FILE) rather than just the food log, in both directions: only the owning module may name each constant, and each redirect demonstrably moves a real write off the live path. Two rewires fail-closed rather than silently: `_budget.budget_weight` and `_estimator.requests` were module-attribute references no import rewrite touches, and both raised AttributeError immediately. The latter appeared only after ruff removed the now-unused `requests` import from _estimator -- the import was load-bearing solely as a patch target. Verified: 707 Python tests / 100% branch coverage, 5 layout-fits tests under xvfb, check_patch_targets 0, pre-commit clean, ~/.local/share/diet_guard byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
86 lines
3.2 KiB
Python
86 lines
3.2 KiB
Python
"""Rebuilding the derived food bank from the meal log.
|
|
|
|
Split out of :mod:`._foodbank` to keep both files under the repo's 250-line
|
|
limit. The sync tick calls :func:`rebuild_food_bank` after a merge. It *does*
|
|
persist, but through :mod:`._foodbank`'s private ``_write_bank`` rather than
|
|
opening the file here, so ``FOOD_BANK_FILE`` stays named by :mod:`._foodbank`
|
|
alone -- which is what ``tests/conftest.py`` redirects, and what
|
|
``test_state_redirect.py`` enforces.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from diet_guard._coerce import as_float
|
|
from diet_guard._estimator import Nutrition
|
|
from diet_guard._foodbank import BankRecord, _apply_upsert, _write_bank
|
|
|
|
__all__ = ["rebuild_food_bank"]
|
|
|
|
|
|
def _entry_nutrition(entry: dict[str, object], *, source: str) -> Nutrition:
|
|
"""Build a :class:`Nutrition` from a raw log entry's macro fields."""
|
|
return Nutrition(
|
|
kcal=as_float(entry.get("kcal")),
|
|
protein_g=as_float(entry.get("protein_g")),
|
|
carbs_g=as_float(entry.get("carbs_g")),
|
|
fat_g=as_float(entry.get("fat_g")),
|
|
grams=as_float(entry.get("grams")),
|
|
source=source,
|
|
)
|
|
|
|
|
|
def rebuild_food_bank(log: dict[str, list[dict[str, object]]]) -> dict[str, BankRecord]:
|
|
"""Rebuild the bank from scratch by replaying ``log``'s entries, then persist it.
|
|
|
|
Replays in a fixed, device-independent order (by ``time`` then ``id``),
|
|
so two devices that converge on the same merged log also converge on the
|
|
same bank -- this is what lets the food bank stay *derived*, never
|
|
synced, with no counter-merge (CRDT) logic needed for ``count``. Mirrors
|
|
the Dart port's ``FoodBankService.rebuild`` exactly, including the
|
|
composite-meal branch (banks each component, then the composite itself).
|
|
|
|
Deleted (tombstoned) entries are skipped entirely, same as
|
|
:func:`diet_guard._state.load_log`.
|
|
|
|
Args:
|
|
log: A full log keyed by date, e.g. from
|
|
:func:`diet_guard._state.read_raw_log` after a sync merge.
|
|
|
|
Returns:
|
|
The freshly rebuilt bank (also written to disk).
|
|
"""
|
|
entries = sorted(
|
|
(
|
|
entry
|
|
for day_entries in log.values()
|
|
for entry in day_entries
|
|
if not entry.get("deleted")
|
|
),
|
|
key=lambda entry: (str(entry.get("time", "")), str(entry.get("id", ""))),
|
|
)
|
|
bank: dict[str, BankRecord] = {}
|
|
for entry in entries:
|
|
components = entry.get("components")
|
|
component_names: list[str] | None = None
|
|
if isinstance(components, list):
|
|
component_names = []
|
|
for component in components:
|
|
if not isinstance(component, dict):
|
|
continue
|
|
name = str(component.get("name", ""))
|
|
component_names.append(name)
|
|
_apply_upsert(
|
|
bank,
|
|
name,
|
|
_entry_nutrition(component, source="food bank"),
|
|
components=None,
|
|
)
|
|
_apply_upsert(
|
|
bank,
|
|
str(entry.get("desc", "")),
|
|
_entry_nutrition(entry, source=str(entry.get("source", "manual"))),
|
|
components=component_names,
|
|
)
|
|
_write_bank(bank)
|
|
return bank
|