mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 15:43:25 +02:00
Lays the Python-side groundwork for the phone companion app's eventual log sync (Milestone 0 of the diet-app-as-wise-balloon plan): - log_meal() stamps every entry with a UUID id for union-by-id merging across devices. - undo_last_today() now tombstones (deleted: true, re-signed) instead of popping the entry, so a sync merge can't resurrect a stale copy of something already undone on another device. - log_meal() accepts components, the full per-item macros for a composite meal, so a food bank rebuilt purely by replaying the log can recover every component's standalone nutrition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
"""Composite "meal" support for diet_guard.
|
|
|
|
A meal is a named group of individually-macroed items -- e.g. a dinner of
|
|
salad + chicken + rice, each entered with its own calories and macros. The
|
|
meal's nutrition is the sum of its items. Both the individual items and the
|
|
composite meal are saved to the food bank (see
|
|
:func:`diet_guard._foodbank.remember_meal`), so next time each item
|
|
autocompletes on its own and the whole meal can be picked as one summed entry.
|
|
|
|
This module is deliberately pure (no I/O): the sum is a total function of its
|
|
items, which keeps the arithmetic exhaustively unit-testable apart from the
|
|
bank persistence and the gate UI that compose it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from diet_guard._estimator import Nutrition
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
# Provenance stamped on a summed meal so the log/UI can tell a composite apart
|
|
# from a single looked-up food.
|
|
MEAL_SOURCE = "meal"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MealItem:
|
|
"""One named component of a composite meal, with its own nutrition.
|
|
|
|
Attributes:
|
|
name: The component's food name (e.g. ``"chicken"``).
|
|
nutrition: The component's resolved macros for the amount eaten.
|
|
"""
|
|
|
|
name: str
|
|
nutrition: Nutrition
|
|
|
|
|
|
def meal_total(items: Sequence[MealItem]) -> Nutrition:
|
|
"""Return the summed nutrition of a meal's items.
|
|
|
|
Every macro and the portion weight are added across the items and rounded to
|
|
0.1, and the result is stamped ``source=MEAL_SOURCE`` so it is
|
|
distinguishable from a single food. An empty sequence sums to an all-zero
|
|
meal rather than raising, so callers need not special-case "no items yet".
|
|
|
|
Args:
|
|
items: The meal's components.
|
|
|
|
Returns:
|
|
A :class:`~diet_guard._estimator.Nutrition` whose fields are
|
|
the per-item sums.
|
|
"""
|
|
return Nutrition(
|
|
kcal=round(sum((item.nutrition.kcal for item in items), 0.0), 1),
|
|
protein_g=round(sum((item.nutrition.protein_g for item in items), 0.0), 1),
|
|
carbs_g=round(sum((item.nutrition.carbs_g for item in items), 0.0), 1),
|
|
fat_g=round(sum((item.nutrition.fat_g for item in items), 0.0), 1),
|
|
grams=round(sum((item.nutrition.grams for item in items), 0.0), 1),
|
|
source=MEAL_SOURCE,
|
|
)
|
|
|
|
|
|
def item_to_component(item: MealItem) -> dict[str, object]:
|
|
"""Return a composite meal's per-component log record for ``item``.
|
|
|
|
The food bank's own ``components`` field (see
|
|
:func:`diet_guard._foodbank.remember_meal`) stores only component
|
|
*names* -- it is rebuilt from the log, not the other way round. This
|
|
record carries the component's full macros so a bank rebuilt purely by
|
|
replaying the log (the companion phone app's sync model) can recover
|
|
each component's standalone nutrition, not just the composite's summed
|
|
total.
|
|
|
|
Args:
|
|
item: One component of a composite meal.
|
|
|
|
Returns:
|
|
A plain dict of the component's name and macros, suitable for the
|
|
``components`` list passed to :func:`diet_guard._state.log_meal`.
|
|
"""
|
|
return {
|
|
"name": item.name,
|
|
"kcal": item.nutrition.kcal,
|
|
"protein_g": item.nutrition.protein_g,
|
|
"carbs_g": item.nutrition.carbs_g,
|
|
"fat_g": item.nutrition.fat_g,
|
|
"grams": item.nutrition.grams,
|
|
}
|