Repository navigation
update_custom_food: set the default serving's quantity - #66
Merged
rwestergren merged 4 commits intoOct 8, 2026
Merged
Conversation
A measure is `amount` units of `name` weighing `value` grams, and the app renders the serving as "<amount> <name>". update_custom_food only wrote name and value, so a food first saved as "400 g" kept amount=400 and type "Weight" and showed as "400 1 serving" with no weight. Setting serving_grams now resets the quantity to 1 and makes the measure a named serving; serving_quantity sets it explicitly. get_food_details reports each measure's quantity and type.
The server stores a new measure named "10 pieces" as amount=10, name="pieces", but keeps the name verbatim when an existing measure is re-sent, so "1 serving" on an update rendered as "1 1 serving". Do the same split on update; an explicit serving_quantity wins.
Report quantity, unit, and total gram weight per portion while keeping servings as the number of portions consumed.
A quantity parsed from serving_name is now checked like an explicit serving_quantity, so an overflowing number can't be saved as inf. The docs now state that serving_quantity without serving_grams keeps the serving's total weight.
ColinToft
added a commit
to ColinToft/cronometer-api-mcp
that referenced
this pull request
Oct 12, 2026
- _split_quantity_led_name follows the server's rule for a leading
quantity: integer, decimal or n/d before a space, so "1/2 cup" is sent
as amount 0.5, name "cup" (verified live: stored that way with no
re-send). The verbatim re-send now only fires for mid-name fractions.
- _apply_serving: a bare rename keeps the quantity ("2 cookies" ->
"2 biscuits"), except on a unit-count measure such as "400 g", which
resets to 1. update_custom_food keeps resetting to 1 when the weight is
set (upstream rwestergren#66).
- update_recipe gains serving_quantity, e.g. to repair a mangled serving.
- create_recipe checks the name, and a failure after the recipe exists
raises a CronometerError naming its food_id and pointing to
update_recipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ColinToft
added a commit
to ColinToft/cronometer-api-mcp
that referenced
this pull request
Oct 12, 2026
* Add update_recipe and delete_recipe tools
Recipes are foods with an ingredients array, saved through the same
/api/v2/add_food upsert as custom foods. Verified against the live API on
throwaway recipes: re-sending a recipe with its id edits it in place (food
id, measure ids and untouched fields survive), a re-sent ingredient list
replaces the old one wholesale, and the server re-derives nutrients and the
"full recipe" weight from the ingredients on every save.
- update_recipe(food_id, ingredients, name, serving_name, serving_grams,
comments): partial in-place edit. Refuses database foods, plain custom
foods, serving-based recipes, and serving edits on the g/oz/full recipe
measures.
- delete_recipe(food_id): retires the recipe, like delete_custom_food. No
hard-delete endpoint exists among 19 v2 names and 9 v3 paths probed.
- get_food_details now returns a recipe's ingredients (in update_recipe's
input shape), comments, and retired flag.
- create_recipe's ingredient parsing/aggregation is shared with
update_recipe; non-finite grams are now rejected. The recipe tools reject
unknown ingredient keys instead of ignoring them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Trim comments and docstrings in recipe edit change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: fold recipe delete into delete_custom_food, server-side derivation
- Drop client-side recipe nutrient aggregation and the "full recipe"
rewrite: verified live that the server derives both on create and on
update even when sent `nutrients: []`.
- Reject serving names that collide with the built-in g/oz/full recipe
measures (case-insensitive, after quantity splitting).
- Resolve a recipe's serving as its non-fixed measure rather than
defaultMeasureId, which Cronometer moves to the last-logged measure.
- One _split_quantity_led_name helper for custom foods and recipes; a zero
leading number is now an error in both.
- One _get_user_food(food_id, kind) guard. delete_custom_food now retires
recipes too; the separate delete_recipe tool and retire_recipe are gone.
- Diary-propagation claims in the update tools' docs verified live.
- Round-trip test: get_food_details ingredients back into update_recipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep serving quantity on resize; simplify user-food guard
- update_recipe(serving_grams=...) alone no longer resets the serving's
quantity ("2 bowls" stayed "1 bowls"); only serving_name sets it.
- _get_user_food(food_id) does just the source check; the edit paths
check for ingredients inline.
- Round-trip fixture documents why ingredient value == grams: an app-made
recipe stores 33 g of milk on a 100 g "cup" measure as value 33.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep recipe serving names verbatim on create; one serving rule
- create_recipe reads the recipe back and re-sends the serving verbatim
when the server re-parsed its name. Verified live: for a new measure
with amount 1 the server splits off a leading number or an n/d fraction
(d 2-5) followed by a space, e.g. "serving (1/2 batch)" -> amount 0.5,
name "batch)"; "serving (1/6 batch)" is kept. Existing measures are
stored verbatim. update_recipe verifies its read-back the same way.
- Batch weight is read back from the server's "full recipe" measure.
- Shared _apply_serving: a bare serving_name means quantity 1, weight
unchanged, for update_custom_food and update_recipe alike.
- A non-positive leading number ("0 calorie syrup") stays part of the
name again; blank names and overflowing numbers still raise.
- _get_user_food(food_id, recipe=...), shared _rename and checks.
- Tests run against a stateful FakeFoodServer that mimics the observed
upsert, re-parse and derivation behaviour.
- Drop the probe-output .gitignore entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Split leading fractions; bare renames keep the portion count
- _split_quantity_led_name follows the server's rule for a leading
quantity: integer, decimal or n/d before a space, so "1/2 cup" is sent
as amount 0.5, name "cup" (verified live: stored that way with no
re-send). The verbatim re-send now only fires for mid-name fractions.
- _apply_serving: a bare rename keeps the quantity ("2 cookies" ->
"2 biscuits"), except on a unit-count measure such as "400 g", which
resets to 1. update_custom_food keeps resetting to 1 when the weight is
set (upstream rwestergren#66).
- update_recipe gains serving_quantity, e.g. to repair a mangled serving.
- create_recipe checks the name, and a failure after the recipe exists
raises a CronometerError naming its food_id and pointing to
update_recipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stop parsing serving names client-side; revert update_custom_food
- Recipe serving names are sent exactly as given (stripped) with an
explicit serving_quantity (default 1). After every save the serving is
read back; if the server re-parsed a new measure's name it is re-sent
verbatim once, compared with == on the name and math.isclose on the
amount. Verified live for "serving (1/6 batch)", "serving (1/2 batch)",
"1 1/2 cups", "cups" x1.5 and "serving (1/3 batch)".
- A bare rename in update_recipe keeps the quantity. Removes
_split_quantity_led_name, _apply_serving and _UNIT_COUNT_MEASURES.
- update_custom_food is back to origin/main's behaviour; only the shared
_get_user_food guard and _rename touch it.
- _get_user_food refuses retired foods unless allow_retired (retire).
- add_recipe gains serving_quantity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse quantity-led recipe serving names; verify grams and nutrients
- Recipe serving names starting with a quantity ("2 bowls", "1/2 cup",
"1 1/2 cups") are refused with a pointer to serving_quantity; names with
a fraction inside ("serving (1/8 batch)") stay valid.
- The serving read-back also compares grams (math.isclose), and every
create/update checks that the server derived energy (208 > 0) for a
non-empty batch, raising with the food_id otherwise. create_recipe and
add_recipe return serving_grams.
- serving_grams heavier than the batch is refused before saving, on create
and update (including ingredient edits that shrink the batch).
- update_custom_food uses the shared _check_positive / _check_name, so a
blank rename now fails.
- FakeFoodServer derives per-100g nutrients, with a switch to simulate a
save that derived none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Accept zero-calorie recipes; refuse serving-based ones everywhere
- The derived-nutrient check now means "energy is present": a water-only
recipe reads back with energy 0 (verified live), which is valid. Uses
NUTRIENT_IDS["energy"]; drops the dead zero-grams early return.
- A derivation failure after create raises RecipeNotDerivedError and says
to check the ingredients in the app; other post-create failures still
point to update_recipe for that id.
- Serving-based recipes ("Recipe"-type measures) are refused in
_get_user_food, so retire_custom_food refuses them too.
- update_recipe returns servings_per_batch before/after, and the tool docs
say a "serving (1/N batch)" needs serving_grams recomputed when the
ingredients change.
- _full_recipe_grams shares _is_fixed_recipe_measure's normalisation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Send empty nutrients on update; servings_per_batch; simpler guards
- update_recipe sends nutrients [] like create_recipe, so the read-back
energy check proves the server derived them (verified live: an update
with [] read back 143 kcal/100 g for an egg-only batch). FakeFoodServer's
derive_nutrients=False now keeps whatever was sent.
- Serving names may not start with a digit, "." or a vulgar fraction
(catches "½ cup", ".5 cup", "2x bowls", "1,5 cups", "2", ...).
- servings_per_batch on create_recipe / update_recipe (and tools), mutually
exclusive with serving_grams: serving = ingredient total / N, so a
"serving (1/N batch)" stays 1/N when the ingredients change.
- One batch-weight model: the ingredient-gram sum. Drops the full-recipe
read-back and OddWeightServer.
- The post-create wrapper catches only CronometerError and says the recipe
exists (id N); don't re-create it, without suggesting update_recipe.
- delete_custom_food documents why serving-based recipes are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Name the recipe id on any post-create failure; refuse ambiguous serving edits
- create_recipe wraps every exception after the recipe exists in one
CronometerError ("The recipe exists (id N); don't re-create it. ...")
chained from the cause; RecipeNotDerivedError is gone.
- update_recipe refuses serving edits when the recipe has more than one
non-fixed measure, whatever the default is.
- Fix the stale per-100g docstring line and drop the total_grams alias.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve the recipe serving lazily; refuse fraction drift; round serving grams
- update_recipe only resolves the serving when a serving field is passed or
new ingredients change the batch weight, so renames and notes work on a
recipe with a drifted default or no named serving. The result only
reports the serving when it was resolved.
- Changing the batch weight without servings_per_batch or serving_grams is
refused, naming both ways to keep the serving; an ambiguous serving is
refused too.
- _recipe_serving is simply "the one non-fixed measure"; the editing flag
is gone.
- servings_per_batch rounds to 0.01 g, and read-back grams compare with a
0.01 g absolute tolerance (live: 151 g / 6 stored as 25.17).
- Pure argument checks move to _check_serving_args.
- get_food_details: note why comments uses .get (absent when unset, live).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document how diary entries follow a recipe serving resize; simplify quantity check
- Live: an entry logged by a recipe's serving measure follows a resize
(50 g became 100 g, still 1 serving). update_recipe's docstrings now say
so precisely (nutrients per gram update; serving-measure entries rescale;
g/oz entries untested), and the result carries a `note` whenever the
serving's grams change.
- Leading-quantity check is now `isnumeric() or "."`, replacing the
hand-listed _QUANTITY_START set; "⅟2 cup" and "↉" are rejected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* State verified diary behaviour for g-logged entries after a recipe resize
Live, side by side on one throwaway recipe: after a serving resize the
entry logged by the serving measure went from 50 to 100 g, while the entry
logged by "g" stayed 50 g. The update_recipe docstrings and result note now
say exactly that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let a rounded-up whole-batch serving fit; document multi-serving refusal
- _check_serving_fits allows a serving up to _GRAMS_TOL over the batch, so
servings_per_batch=1 on a batch like 523.3868 g (serving 523.39 g) is no
longer refused, on create or update.
- update_recipe docstrings: on a recipe with several named servings, any
batch-weight-changing ingredient edit is refused and must be done in the
app. Fix a stray line break in the tool docstring.
- Index food["measures"] directly in _recipe_servings and _get_user_food.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Restore the _GRAMS_TOL serving-fits check
The previous commit accidentally shipped a mutation-test variant of
_check_serving_fits (the old relative isclose), so its own N=1 tests failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Prove derivation on the settle retry; guard app-set batch weights; wrap update read-back
- _settle_recipe_serving's re-send clears nutrients again, so the second
read-back also proves the server derived them.
- update_recipe refuses, before any write, a recipe whose stored "full
recipe" weight differs from its ingredient total by more than 0.01 g
(set in the app); the server's id-0 placeholder is skipped.
- Any failure after update_recipe's save is re-raised as "Recipe N was
saved; verify with get_food_details. ..." chained from the cause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A measure is
amountunits ofnameweighingvaluegrams in total, and the app renders a serving as<amount> <name>.update_custom_foodonly writesnameandvalue, so it cannot fix a food whose default measure has a quantity other than 1.Example from a real account: a custom food whose default measure had quantity 400 and type
"Weight". Afterupdate_custom_food(food_id, serving_name="1 serving", serving_grams=250)the stored measure was{"id": 310790759, "name": "1 serving", "amount": 400, "value": 250, "type": "Weight"}get_food_detailsreads that back as1 serving = 250 g, but the iOS editor showed the serving as "400 1 serving" with no serving weight.Change
serving_gramsnow also resets the measure's quantity to 1 and its type to"Atomic", the same shapecreate_custom_foodwrites.serving_quantitysets the quantity explicitly (alone, or together withserving_grams).serving_name("1 serving", "2 cookies") is moved into the quantity, unlessserving_quantityis given. The server already does this for a new measure:add_custom_food(serving_name="10 pieces")sendsamount: 1.0and reads back asamount: 10, name: "pieces". It does not do it when an existing measure is re-sent, so an update with "1 serving" would render as "1 1 serving".serving_namealone still only renames.serving_gramsandserving_quantitymust be positive finite numbers.get_food_detailsreports each measure'squantityandtype, so the result can be checked.Verification
/api/v2/add_foodwithamount: 1, type: "Atomic"on the existing measure id returnedSUCCESS, andget_foodread back the same measure id with the new quantity, type, name and weight. The other measures and the per-100g nutrients were unchanged.serving_name="1 cup"now stores namecupwith quantity 1.tests/test_custom_food_edit.py;ruff checkand the full suite pass (162 tests).