Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/cronometer_api_mcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,8 +958,10 @@ def enrich_diary_servings(self, diary: dict) -> dict:
- measure: {measure_id, name, grams_per_unit} for the entry's
measureId (falls back to the food's defaultMeasureId)
- servings: grams / grams_per_unit, when derivable
- nutrients: the food's per-100g profile scaled to the entry's grams,
labeled with name/unit/category via the nutrient definitions catalog
- nutrients: the food's nutrient profile scaled to the entry's amount
(per-100g for Weight/Atomic measures, per-serving for Recipe
measures), labeled with name/unit/category via the nutrient
definitions catalog

Enrichment is best-effort: if the get_foods call fails or a food is not
returned, the corresponding entries are left unchanged. The diary dict
Expand Down Expand Up @@ -1029,9 +1031,17 @@ def enrich_diary_servings(self, diary: dict) -> dict:
):
entry["servings"] = round(grams / grams_per_unit, 4)

# Food nutrients are stored per-100g; scale to the entry's grams.
# Nutrient scaling depends on the measure type:
# - Recipe measures: nutrients are stored per one reference
# serving and the diary "grams" field is a serving count, so
# scale by grams directly.
# - Weight/Atomic measures: nutrients are stored per-100g and
# "grams" is real grams, so scale by grams / 100.
if isinstance(grams, (int, float)):
scale = grams / 100.0
if measure and measure.get("type") == "Recipe":
scale = grams
else:
scale = grams / 100.0
scaled: list[dict] = []
for n in food.get("nutrients", []):
if not isinstance(n, dict):
Expand Down
32 changes: 31 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ def _enrich_client(tmp_path: Path):
"grams": 50,
"order": 65538,
},
{
"type": "Serving",
"foodId": 300,
"measureId": 30,
"grams": 1.1, # Recipe: "grams" is a serving count, not grams
"order": 65539,
},
{"type": "Exercise", "name": "Running", "order": 1},
]
}
Expand All @@ -227,6 +234,18 @@ def _enrich_client(tmp_path: Path):
"measures": [{"id": 20, "name": "glass", "value": 244.0}],
"nutrients": [{"id": 208, "amount": 42.0}],
},
{
"id": 300,
"name": "Recipe Food",
"source": "Custom",
"defaultMeasureId": 30,
"measures": [
{"id": 30, "name": "serving", "value": 1, "amount": 1, "type": "Recipe"},
{"id": 31, "name": "g", "value": 233.4, "amount": 1, "type": "Recipe"},
],
# stored per one reference serving (not per-100g)
"nutrients": [{"id": 208, "amount": 708.538}, {"id": 203, "amount": 56.03}],
},
]

NUTRIENT_DEFS = {
Expand Down Expand Up @@ -268,8 +287,19 @@ def test_enrich_diary_merges_names_measures_and_scaled_nutrients(tmp_path):
# 42 kcal/100g scaled to 50g -> 21
assert next(n for n in milk["nutrients"] if n["id"] == 208)["amount"] == 21.0

recipe = entries[2]
assert recipe["name"] == "Recipe Food"
assert recipe["measure"]["measure_id"] == 30
assert recipe["measure"]["name"] == "serving"
# Recipe measure: nutrients are per-serving and "grams" is a serving count,
# so 708.538 kcal/serving * 1.1 servings -> 779.39 (not grams/100)
energy = next(n for n in recipe["nutrients"] if n["id"] == 208)
assert energy["amount"] == 779.3918
protein = next(n for n in recipe["nutrients"] if n["id"] == 203)
assert protein["amount"] == 61.633

# Non-Serving entry untouched
assert entries[2] == {"type": "Exercise", "name": "Running", "order": 1}
assert entries[3] == {"type": "Exercise", "name": "Running", "order": 1}


def test_enrich_diary_is_best_effort_when_get_foods_fails(tmp_path):
Expand Down
Loading