Skip to content
Open
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
2 changes: 2 additions & 0 deletions cookidoo_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
CookidooCollection,
CookidooConfig,
CookidooCookingActivity,
CookidooCookingHistoryEntry,
CookidooCookState,
CookidooDevice,
CookidooIngredient,
Expand Down Expand Up @@ -57,6 +58,7 @@
"CookidooAuthData",
"CookidooCookingActivity",
"CookidooCookState",
"CookidooCookingHistoryEntry",
"cooking_activity_from_push",
"CookidooAdditionalItem",
"CookidooIngredientItem",
Expand Down
6 changes: 6 additions & 0 deletions cookidoo_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
REMOVE_RECIPE_FROM_CUSTOM_COLLECTION_PATH: Final = (
"organize/{language}/api/custom-list/{id}/recipes/{recipe}"
)
# "Last cooked": the recipes the account has cooked, newest first. The service
# returns the whole history in one response (it takes no page/limit params).
COOKING_HISTORY_PATH: Final = "organize/{language}/api/cooking-history"
COOKING_HISTORY_PATH_ACCEPT: Final = (
"application/vnd.vorwerk.organize.cooking-history.mobile+json"
)
MANAGED_COLLECTIONS_PATH: Final = "organize/{language}/api/managed-list"
MANAGED_COLLECTIONS_PATH_ACCEPT: Final = (
"application/vnd.vorwerk.organize.managed-list.mobile+json"
Expand Down
49 changes: 49 additions & 0 deletions cookidoo_api/cookidoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from cookidoo_api.const import (
CIAM_BASE_URL,
CIAM_LOGIN_SRV_URL,
COOKING_HISTORY_PATH_ACCEPT,
CUSTOM_COLLECTIONS_PATH_ACCEPT,
CUSTOM_RECIPES_PATH_ACCEPT,
DEFAULT_API_HEADERS,
Expand Down Expand Up @@ -52,6 +53,7 @@
cookidoo_additional_item_from_json,
cookidoo_calendar_day_from_json,
cookidoo_collection_from_json,
cookidoo_cooking_history_entry_from_json,
cookidoo_custom_recipe_from_json,
cookidoo_device_from_json,
cookidoo_ingredient_item_from_json,
Expand All @@ -67,6 +69,7 @@
AdditionalItemJSON,
CalendarDayJSON,
CommunityProfileJSON,
CookingHistoryEntryJSON,
CustomCollectionJSON,
CustomRecipeJSON,
CustomRecipesJSON,
Expand All @@ -84,6 +87,7 @@
CookidooCalendarDay,
CookidooCollection,
CookidooConfig,
CookidooCookingHistoryEntry,
CookidooCustomRecipe,
CookidooDevice,
CookidooIngredientItem,
Expand Down Expand Up @@ -2017,6 +2021,51 @@ async def get_managed_collections(self, page: int = 0) -> list[CookidooCollectio
],
)

async def get_cooking_history(self) -> list[CookidooCookingHistoryEntry]:
"""Get the cooking history ("last cooked") of the account.

The service returns the whole history in a single response, newest
entry first; it accepts no pagination parameters.

Returns
-------
list[CookidooCookingHistoryEntry]
The cooked recipes, most recently cooked first

Raises
------
CookidooAuthException
When the access token is not valid anymore
CookidooRequestException
If the request fails.
CookidooParseException
If the parsing of the request response fails.

"""

await self._ensure_endpoints()
url = self.api_endpoint / self._path("organize:api-cooking-history").format(
**self._cfg.localization.__dict__
)
result = self._ensure_mapping(
await self._request_json(
"get",
url,
"loading cooking history",
headers={"ACCEPT": COOKING_HISTORY_PATH_ACCEPT},
),
"loading cooking history",
)
return self._parse_result(
"loading cooking history",
lambda: [
cookidoo_cooking_history_entry_from_json(
cast(CookingHistoryEntryJSON, entry), self._cfg.localization
)
for entry in cast(Sequence[object], result["entries"])
],
)

async def add_managed_collection(
self,
managed_collection_id: str,
Expand Down
40 changes: 40 additions & 0 deletions cookidoo_api/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
CalendarDayJSON,
CalenderDayRecipeJSON,
CommunityProfileJSON,
CookingHistoryEntryJSON,
CustomCollectionJSON,
CustomRecipeContentJSON,
CustomRecipeJSON,
Expand All @@ -40,6 +41,7 @@
CookidooChapterRecipe,
CookidooCollection,
CookidooCookingActivity,
CookidooCookingHistoryEntry,
CookidooCookState,
CookidooCustomRecipe,
CookidooDevice,
Expand Down Expand Up @@ -677,6 +679,44 @@ def _to_day_recipe(recipe: CalenderDayRecipeJSON) -> CookidooCalendarDayRecipe:
)


def cookidoo_cooking_history_entry_from_json(
entry: CookingHistoryEntryJSON,
localization: CookidooLocalizationConfig | None = None,
) -> CookidooCookingHistoryEntry:
"""Convert a cooking history entry received from the API to a cookidoo item."""
recipe = entry["recipe"]

assets = recipe.get("assets")
thumbnail, image = None, None
descriptive_assets = [assets["images"]] if assets and assets["images"] else None
if descriptive_assets is not None:
thumbnail, image = _extract_images_from_descriptive_assets(descriptive_assets)

# The service reports the duration as a stringified float of seconds
# (e.g. ``"5100.0"``), unlike the planning endpoints' plain int.
try:
total_time = int(float(recipe["totalTime"]))
except (TypeError, ValueError):
total_time = 0

cooked_at = _push_timestamp(entry["details"]["timestamp"])
if cooked_at is None:
raise ValueError(
f"Cooking history entry for recipe {recipe['id']} has an "
f"unparsable timestamp: {entry['details']['timestamp']!r}"
)

return CookidooCookingHistoryEntry(
id=recipe["id"],
name=recipe["title"],
cooked_at=cooked_at,
total_time=total_time,
thumbnail=thumbnail,
image=image,
url=_construct_recipe_url(localization, recipe["id"]),
)


async def __get_localization_options(
country: str | None = None,
language: str | None = None,
Expand Down
31 changes: 31 additions & 0 deletions cookidoo_api/raw_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,37 @@ class CalenderDayRecipeJSON(TypedDict):
assets: CalenderDayRecipeAssetsJSON | None


class CookingHistoryRecipeJSON(TypedDict):
"""The json for a recipe in the cooking history in the API."""

id: str
title: str
totalTime: str
type: str
locale: str
assets: CalenderDayRecipeAssetsJSON | None


class CookingHistoryDetailsJSON(TypedDict):
"""The json for the details of a cooking history entry in the API."""

timestamp: str


class CookingHistoryEntryJSON(TypedDict):
"""The json for a cooking history entry in the API."""

details: CookingHistoryDetailsJSON
recipe: CookingHistoryRecipeJSON


class CookingHistoryJSON(TypedDict):
"""The json for the cooking history in the API."""

userId: str
entries: list[CookingHistoryEntryJSON]


class CalendarDayJSON(TypedDict):
"""The json for a calendar day in the API."""

Expand Down
32 changes: 32 additions & 0 deletions cookidoo_api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,38 @@ class CookidooCalendarDayRecipe:
url: str


@dataclass
class CookidooCookingHistoryEntry:
"""Cookidoo cooking history ("last cooked") entry.

Attributes
----------
id
The id of the recipe
name
The label of the recipe
cooked_at
When the recipe was cooked (timezone-aware, UTC)
total_time
The time for the recipe, in seconds
thumbnail
The thumbnail image URL (small preview)
image
The full-size image URL
url
The URL of the recipe

"""

id: str
name: str
cooked_at: datetime
total_time: int
thumbnail: str | None
image: str | None
url: str


@dataclass
class CookidooCalendarDay:
"""Cookidoo calendar day type.
Expand Down
2 changes: 2 additions & 0 deletions cookidoo_api/well_known.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ADD_INGREDIENT_ITEMS_FOR_RECIPES_PATH,
ADD_RECIPES_TO_CALENDER_PATH,
COMMUNITY_PROFILE_PATH,
COOKING_HISTORY_PATH,
CUSTOM_COLLECTIONS_PATH,
CUSTOM_RECIPE_PATH,
CUSTOM_RECIPES_PATH,
Expand Down Expand Up @@ -110,6 +111,7 @@
"organize",
REMOVE_RECIPE_FROM_CUSTOM_COLLECTION_PATH,
),
"organize:api-cooking-history": ("organize", COOKING_HISTORY_PATH),
"organize:api-managed-list": ("organize", MANAGED_COLLECTIONS_PATH),
"organize:api-managed-list-single": ("organize", REMOVE_MANAGED_COLLECTION_PATH),
"planning:api-my-week-from-date": ("planning", RECIPES_IN_CALENDAR_WEEK_PATH),
Expand Down
29 changes: 29 additions & 0 deletions docs/raw-api-requests/get-cooking-history.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
GET https://cookidoo.de/organize/de-DE/api/cooking-history HTTP/2.0
accept: application/vnd.vorwerk.organize.cooking-history.mobile+json
authorization: Bearer <redacted>
accept-encoding: gzip
content-length: 0

# Notes
# - The service takes no pagination parameters: `?page=` / `?limit=` are
# accepted but ignored, and the full history comes back in one response.
# - Entries are ordered newest-cooked first.
# - `totalTime` is seconds, serialised as a stringified float ("5100.0").
# - Requesting any other representation yields 406 with:
# Acceptable representations: [
# application/vnd.vorwerk.organize.cooking-history.mobile+json,
# application/json
# ]
# `application/json` returns the same entries with extra legacy image
# fields (asciiTitle, squareImage, responsiveImageSrcset, ...).
# - OPTIONS reports `Allow: POST,GET,HEAD,OPTIONS` here, and
# `Allow: POST,OPTIONS` on .../cooking-history/multiple. Only the GET is
# implemented by this library; the POSTs (recording a cook) are untested.


HTTP/2.0 200
content-type: application/vnd.vorwerk.organize.cooking-history.mobile+json
x-content-type-options: nosniff
vary: Accept-Encoding

{"userId":"00000000-0000-0000-0000-000000000000","entries":[{"details":{"timestamp":"2026-09-05T05:31:47.529Z"},"recipe":{"assets":{"images":{"square":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/27C5A685-9083-4864-A0BE-F751B99CA7D3/Derivates/2AEAE2E5-BB52-4A3A-A6C1-33D08A874419","portrait":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/27C5A685-9083-4864-A0BE-F751B99CA7D3/Derivates/2AEAE2E5-BB52-4A3A-A6C1-33D08A874419","landscape":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/27C5A685-9083-4864-A0BE-F751B99CA7D3/Derivates/2AEAE2E5-BB52-4A3A-A6C1-33D08A874419"}},"id":"r59322","locale":"","title":"Vollkorn-Toastbrötchen","totalTime":"5100.0","type":"VORWERK"}},{"details":{"timestamp":"2026-08-28T13:56:30.168Z"},"recipe":{"assets":{"images":{"square":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/A1B2C3D4-0000-0000-0000-000000000000/Derivates/B2C3D4E5-0000-0000-0000-000000000000","portrait":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/A1B2C3D4-0000-0000-0000-000000000000/Derivates/B2C3D4E5-0000-0000-0000-000000000000","landscape":"https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/A1B2C3D4-0000-0000-0000-000000000000/Derivates/B2C3D4E5-0000-0000-0000-000000000000"}},"id":"r54743","locale":"","title":"Pizzateig","totalTime":"900.0","type":"VORWERK"}}]}
3 changes: 3 additions & 0 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ async def main():
_managed_collections = await cookidoo.get_managed_collections()
await cookidoo.remove_managed_collection("col500401")

# Cooking history ("last cooked")
_cooking_history = await cookidoo.get_cooking_history()

# Recipe details
_recipe_details = await cookidoo.get_recipe_details("r59322")

Expand Down
15 changes: 15 additions & 0 deletions smoke_test/test_2_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ async def test_cookidoo_managed_collections(self, cookidoo: Cookidoo) -> None:
assert isinstance(managed_collections, list)
assert len(managed_collections) == 0

async def test_cookidoo_cooking_history(self, cookidoo: Cookidoo) -> None:
"""Test cookidoo cooking history."""
cooking_history = await cookidoo.get_cooking_history()
assert isinstance(cooking_history, list)

# The account may legitimately have never cooked anything; only assert
# the shape and ordering when there is something to look at.
for entry in cooking_history:
assert entry.id
assert entry.name
assert entry.cooked_at.tzinfo is not None

timestamps = [entry.cooked_at for entry in cooking_history]
assert timestamps == sorted(timestamps, reverse=True)

async def test_cookidoo_custom_collections(self, cookidoo: Cookidoo) -> None:
"""Test cookidoo custom collections."""
added_custom_collection = await cookidoo.add_custom_collection(
Expand Down
35 changes: 35 additions & 0 deletions tests/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,41 @@
]
}

COOKIDOO_TEST_RESPONSE_GET_COOKING_HISTORY = {
"userId": "00000000-0000-0000-0000-000000000000",
"entries": [
{
"details": {"timestamp": "2026-09-05T05:31:47.529Z"},
"recipe": {
"id": "r59322",
"title": "Vollkorn-Toastbrötchen",
# The service reports seconds as a stringified float.
"totalTime": "5100.0",
"type": "VORWERK",
"locale": "",
"assets": {
"images": {
"square": "https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/square",
"portrait": "https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/portrait",
"landscape": "https://assets.tmecosys.com/image/upload/{transformation}/img/recipe/ras/Assets/landscape",
}
},
},
},
{
"details": {"timestamp": "2026-08-28T13:56:30.168Z"},
"recipe": {
"id": "r54743",
"title": "Pizzateig",
"totalTime": "900.0",
"type": "VORWERK",
"locale": "",
"assets": {"images": None},
},
},
],
}

COOKIDOO_TEST_RESPONSE_GET_MANAGED_COLLECTIONS = {
"managedlists": [
{
Expand Down
Loading