Skip to content

Add custom recipe creation and update API - #238

Open
Dragons0458 wants to merge 14 commits into
miaucl:masterfrom
Dragons0458:feature/custom-recipes
Open

Dragons0458 wants to merge 14 commits into
miaucl:masterfrom
Dragons0458:feature/custom-recipes

Conversation

@Dragons0458

@Dragons0458 Dragons0458 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 the created-recipes workflow.

The create/update flow uses a stub POST followed by a PATCH with 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() and update_custom_recipe() APIs while keeping the existing custom recipe remove methods unchanged and routing HTTP work through the shared _request_json helper.

What changed

Create and update custom recipes

  • Added create_custom_recipe() to create a stub recipe, patch it with the full payload, and return the reloaded CookidooCustomRecipe.
  • Added update_custom_recipe() to merge caller-provided fields with the existing recipe, patch the result, and return the refreshed recipe.
  • Added the created-recipes/{language}/{id} update path and shared _patch_custom_recipe() helper.
  • Added CookidooCreateCustomRecipe and CookidooUpdateCustomRecipe input models and exported them through the public package API.
  • Validated recipe input locally (times, yield text, annotation slots, ingredient references, caller-provided image paths) before sending requests.
  • If populate/reload fails after the stub POST, the orphaned recipe id is attached to the raised exception via Exception.add_note.

Guided-cooking steps and annotations

  • Added Thermomix enums for machine type, mode, speed, temperature, direction, browning power, and steaming accessories.
  • Added typed instruction/annotation models (CookidooInstruction, ingredient/TTS/mode/custom annotations, and step settings).
  • Added _process_recipe_steps() and annotation serialization helpers so string steps and structured CookidooInstruction values can both be sent to Cookidoo.
  • Normalized Varoma temperature values in annotation payloads to match the API shape.

Existing read path

  • get_custom_recipe() now sends ACCEPT: application/vnd.vorwerk.customer-recipe.full+json so Cookidoo returns the full customer-recipe representation with structured instructions and annotations required by the new parser. Listing and remove behavior is unchanged.

Tests

  • Added unit tests for create/update flows, payload building, annotation handling, validation errors, auth/request/parse failures, and enum-backed machine types.
  • Extended helper/parser coverage for the new custom recipe payload types.
  • Added a read/write smoke test that creates a recipe, fetches it, performs a partial update, lists it, and removes it.

Behavioral notes

  • create_custom_recipe() accepts a CookidooCreateCustomRecipe model and returns CookidooCustomRecipe.
  • update_custom_recipe() accepts a recipe id plus CookidooUpdateCustomRecipe; omitted fields are taken from the existing recipe.
  • Default machine type is ThermomixMachineType.TM7 when none is provided.
  • Caller-provided image values must be Cookidoo customer-recipe paths/filenames; CDN/display URLs raise ValueError. Inherited API image URLs are still normalized when echoing an update payload.
  • Existing list_custom_recipes(), add_custom_recipe_from(), and remove_custom_recipe() behavior is unchanged.

Validation

  • .venv/bin/python -m pytest -q
    • 341 passed, 100% coverage
  • .venv/bin/python -m pytest -q smoke_test/test_2_methods.py::TestMethods::test_cookidoo_create_custom_recipe
    • exercises create, get, update, list, and remove against the real Cookidoo endpoint
  • .venv/bin/python -m ruff check .
  • .venv/bin/python -m mypy cookidoo_api

Made with Cursor

@github-actions github-actions Bot added the 🧪 testing Pull request that adds tests label Aug 5, 2026
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2a92337) to head (4878489).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Dragons0458
Dragons0458 marked this pull request as draft August 5, 2026 20:04
@Dragons0458
Dragons0458 marked this pull request as ready for review August 5, 2026 20:05
@miaucl miaucl added the run smoke test Smoke test requires secrets, only run after review label Aug 6, 2026
@miaucl
miaucl self-requested a review August 6, 2026 20:57
@miaucl miaucl self-assigned this Aug 6, 2026
@miaucl miaucl removed the run smoke test Smoke test requires secrets, only run after review label Aug 6, 2026

@miaucl miaucl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. There are some comments to address, especially the bug (regression) regarding the 'or-chaining'.

Comment thread cookidoo_api/helpers.py Outdated
else:
raw_recipes = []
recipes_data: list[object] = list(raw_recipes)
raw_recipes = data.get("data") or data.get("recipes") or []

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: restored the explicit if "data" in data / elif "recipes" in data key check and added a regression test for {"data": [], "recipes": [...]}.

Comment thread cookidoo_api/cookidoo.py
"get",
url,
"loading custom recipe",
headers={"ACCEPT": CUSTOM_RECIPES_PATH_ACCEPT},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cookidoo_api/cookidoo.py
async def search_recipes(
self,
query: str | None = None,
*,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: removed the accidental * and restored the positional signature from master. No breaking change.

Comment thread cookidoo_api/cookidoo.py Outdated
r"((prod|nonprod)/img/customer-recipe/)?[A-Za-z0-9_-]+\."
r"(bmp|jpe|jpeg|jpg|png)",
image,
):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cookidoo_api/cookidoo.py Outdated
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")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cookidoo_api/cookidoo.py Outdated
) -> 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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: extracted _enum_value and replaced the repeated hasattr(..., "value") duck-typing.

Comment thread cookidoo_api/cookidoo.py Outdated
image_owned_by_user=(
recipe.image_owned_by_user
if recipe.image_owned_by_user is not None
else True

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: replaced the nested ternary with an explicit if/elif/else for image_owned_by_user.

@Dragons0458

Copy link
Copy Markdown
Contributor Author

Thanks for the PR. There are some comments to address, especially the bug (regression) regarding the 'or-chaining'.

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

Dragons0458 and others added 13 commits August 6, 2026 22:55
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.
@Dragons0458
Dragons0458 force-pushed the feature/custom-recipes branch from 25114c9 to 84d9920 Compare August 7, 2026 03:55
@Dragons0458
Dragons0458 requested a review from miaucl August 7, 2026 03:56
thomassloboda added a commit to SlobodaFR/home-remote-mcps that referenced this pull request Aug 26, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧪 testing Pull request that adds tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants