Add custom recipe creation and update API - #238
Dragons0458 wants to merge 14 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #238 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 7 7
Lines 965 1358 +393
Branches 70 128 +58
==========================================
+ Hits 965 1358 +393 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
miaucl
left a comment
There was a problem hiding this comment.
Thanks for the PR. There are some comments to address, especially the bug (regression) regarding the 'or-chaining'.
| else: | ||
| raw_recipes = [] | ||
| recipes_data: list[object] = list(raw_recipes) | ||
| raw_recipes = data.get("data") or data.get("recipes") or [] |
There was a problem hiding this comment.
Bug: reintroduces the "or-chaining" fix that was previously reverted.
raw_recipes = data.get("data") or data.get("recipes") or []This is the exact pattern that was fixed in a prior commit ("fix or-chaining"), which replaced it with an explicit if "data" in data: ... elif "recipes" in data: ... check specifically because [] is falsy in Python. Reproduced locally:
data = {"data": [], "recipes": [{"id": "r1", "title": "stale-fallback"}]}
raw_recipes = data.get("data") or data.get("recipes") or []
# => [{"id": "r1", "title": "stale-fallback"}] (wrong: an explicit empty "data"
# result incorrectly falls back to "recipes")Please restore the explicit key-check pattern instead of the or-chain.
There was a problem hiding this comment.
Fixed: restored the explicit if "data" in data / elif "recipes" in data key check and added a regression test for {"data": [], "recipes": [...]}.
| "get", | ||
| url, | ||
| "loading custom recipe", | ||
| headers={"ACCEPT": CUSTOM_RECIPES_PATH_ACCEPT}, |
There was a problem hiding this comment.
This adds a new ACCEPT header to get_custom_recipe(), which is a real behavior change to an already-shipped public method. The PR description states "existing custom recipe read/remove methods unchanged" — worth clarifying in the description/changelog why this was needed (likely to get structured instructions/annotations back from the API).
There was a problem hiding this comment.
Documented: the ACCEPT header is intentional so Cookidoo returns the full customer-recipe representation (structured instructions/annotations). Updated the method docstring and PR description accordingly.
| async def search_recipes( | ||
| self, | ||
| query: str | None = None, | ||
| *, |
There was a problem hiding this comment.
Adding * here makes locale and everything after it keyword-only. Any existing caller using search_recipes("chicken", "en") positionally would break. This is a breaking API change and probably deserves a changelog/major-version note.
There was a problem hiding this comment.
Fixed: removed the accidental * and restored the positional signature from master. No breaking change.
| r"((prod|nonprod)/img/customer-recipe/)?[A-Za-z0-9_-]+\." | ||
| r"(bmp|jpe|jpeg|jpg|png)", | ||
| image, | ||
| ): |
There was a problem hiding this comment.
Invalid image references (e.g. a full CDN URL) are silently converted to None here rather than raising a validation error. Combined with image_owned_by_user = ... if normalized_image is not None else False below, a caller passing image= + image_owned_by_user=True can have both silently overridden with no warning. Consider raising a ValueError instead of silently dropping invalid input.
There was a problem hiding this comment.
Fixed: caller-provided images are now validated with ValueError before any request. Silent normalization remains only for inherited API/display URLs when echoing an update payload.
| if not isinstance(recipe_id, str) or not recipe_id: | ||
| raise CookidooParseException("No recipe ID returned from creation.") | ||
|
|
||
| await self._patch_custom_recipe(recipe_id, payload, "update custom recipe") |
There was a problem hiding this comment.
create_custom_recipe is POST (create stub) → PATCH (populate) → GET (reload). If this PATCH raises (auth/network/parse error), the stub recipe already exists server-side but its id is never surfaced to the caller — there's no cleanup and no way for the caller to know/remove the orphaned stub. Worth documenting this risk or including the id in the raised exception context.
There was a problem hiding this comment.
Fixed: on populate/reload failure after the stub POST, the orphaned recipe id is attached via Exception.add_note, logged as a warning, and documented in the create_custom_recipe docstring.
| ) -> dict[str, object]: | ||
| """Serialize an annotation temperature without mutating the model.""" | ||
| raw_value = temperature.value | ||
| value = raw_value.value if hasattr(raw_value, "value") else raw_value |
There was a problem hiding this comment.
Minor style: this hasattr(x, "value") enum-unwrapping duck-typing pattern is repeated ~8 times across this file. Since all the Thermomix enums are StrEnum, a small helper like _enum_value(x: str | StrEnum) -> str: return x.value if isinstance(x, StrEnum) else x would be more explicit/mypy-friendly and remove the duplication.
There was a problem hiding this comment.
Fixed: extracted _enum_value and replaced the repeated hasattr(..., "value") duck-typing.
| image_owned_by_user=( | ||
| recipe.image_owned_by_user | ||
| if recipe.image_owned_by_user is not None | ||
| else True |
There was a problem hiding this comment.
Minor style: this nested ternary (X if cond1 else True if cond2 else Z) is correct but hard to parse at a glance. Consider extracting to a small helper or an explicit if/elif chain for readability.
There was a problem hiding this comment.
Fixed: replaced the nested ternary with an explicit if/elif/else for image_owned_by_user.
Thank you for your review and your comments! I could check them and I could improve my PR, can you please review again? The library has more methods and functionalities with the time, I'm happy to use it with my Thermomix, thank you for your work! :D |
Define Thermomix mode, speed, temperature and accessory enums and export them. Co-authored-by: Cursor <cursoragent@cursor.com>
Add the created-recipes update endpoint and implement create_custom_recipe. Co-authored-by: Cursor <cursoragent@cursor.com>
Cover create_custom_recipe, recipe step annotations, and enum behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Make search_recipes filter parameters keyword-only for Ruff PLR0917 and expand tests to restore full package coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
Add annotated create/update and copy-from-recipe smoke tests, request the full custom recipe media type on get, and drop invalid CDN image URLs before PATCH. Co-authored-by: Cursor <cursoragent@cursor.com>
Restore explicit search-result key checks, reject invalid caller image paths, surface orphaned stub ids on create failures, and clean up keyword-only/search and enum serialization style issues. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the public positional signature compatible with master while satisfying CI's positional-argument limit.
25114c9 to
84d9920
Compare
Pins cookidoo-connector's cookidoo-api dependency to a commit on Dragons0458's fork (miaucl/cookidoo-api#238, open/unreviewed, not on PyPI) to get create_custom_recipe()/update_custom_recipe() ahead of that PR merging upstream. Exposes both as MCP tools (cookidoo_create_custom_recipe/cookidoo_update_custom_recipe), including structured Thermomix guided-cooking instructions (time/temperature/speed + slot-anchored annotations). Re-pin to a released cookidoo-api version once the upstream PR ships - the recipe-creation API may still change before then. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Context
miaucl/cookidoo-api#222 added
list_custom_recipes()so the client can read the user's custom recipe collection, but Cookidoo also supports creating recipes from scratch and updating their content through thecreated-recipesworkflow.The create/update flow uses a stub
POSTfollowed by aPATCHwith the full recipe payload, and guided-cooking steps can include Thermomix-specific annotations that need to be serialized into the API format.Objective
The goal of this PR is to add focused
create_custom_recipe()andupdate_custom_recipe()APIs while keeping the existing custom recipe remove methods unchanged and routing HTTP work through the shared_request_jsonhelper.What changed
Create and update custom recipes
create_custom_recipe()to create a stub recipe, patch it with the full payload, and return the reloadedCookidooCustomRecipe.update_custom_recipe()to merge caller-provided fields with the existing recipe, patch the result, and return the refreshed recipe.created-recipes/{language}/{id}update path and shared_patch_custom_recipe()helper.CookidooCreateCustomRecipeandCookidooUpdateCustomRecipeinput models and exported them through the public package API.POST, the orphaned recipe id is attached to the raised exception viaException.add_note.Guided-cooking steps and annotations
CookidooInstruction, ingredient/TTS/mode/custom annotations, and step settings)._process_recipe_steps()and annotation serialization helpers so string steps and structuredCookidooInstructionvalues can both be sent to Cookidoo.Existing read path
get_custom_recipe()now sendsACCEPT: application/vnd.vorwerk.customer-recipe.full+jsonso Cookidoo returns the full customer-recipe representation with structured instructions and annotations required by the new parser. Listing and remove behavior is unchanged.Tests
Behavioral notes
create_custom_recipe()accepts aCookidooCreateCustomRecipemodel and returnsCookidooCustomRecipe.update_custom_recipe()accepts a recipe id plusCookidooUpdateCustomRecipe; omitted fields are taken from the existing recipe.ThermomixMachineType.TM7when none is provided.imagevalues must be Cookidoo customer-recipe paths/filenames; CDN/display URLs raiseValueError. Inherited API image URLs are still normalized when echoing an update payload.list_custom_recipes(),add_custom_recipe_from(), andremove_custom_recipe()behavior is unchanged.Validation
.venv/bin/python -m pytest -q341 passed, 100% coverage.venv/bin/python -m pytest -q smoke_test/test_2_methods.py::TestMethods::test_cookidoo_create_custom_recipe.venv/bin/python -m ruff check ..venv/bin/python -m mypy cookidoo_apiMade with Cursor