1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-19 05:09:40 +02:00

feat: Recipe Finder (aka Cocktail Builder) (#4542)

This commit is contained in:
Michael Genson 2024-12-03 07:27:41 -06:00 committed by GitHub
parent d26e29d1c5
commit 4e0cf985bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1959 additions and 151 deletions

View file

@ -1,5 +1,6 @@
import random
from typing import Any
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
@ -8,6 +9,7 @@ from pydantic import UUID4
from mealie.schema.cookbook.cookbook import SaveCookBook
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_category import TagSave
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient, SaveIngredientFood
from tests.utils import api_routes
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
@ -335,3 +337,71 @@ def test_public_recipe_cookbook_filter_with_recipes(
assert str(other_household_recipe.id) not in recipe_ids
else:
assert str(other_household_recipe.id) in recipe_ids
@pytest.mark.parametrize("is_private_group", [True, False])
@pytest.mark.parametrize("is_private_household", [True, False])
@pytest.mark.parametrize("is_private_recipe", [True, False])
def test_get_suggested_recipes(
api_client: TestClient,
unique_user: TestUser,
random_recipe: Recipe,
is_private_group: bool,
is_private_household: bool,
is_private_recipe: bool,
):
database = unique_user.repos
## Set Up Group
group = database.groups.get_one(unique_user.group_id)
assert group and group.preferences
group.preferences.private_group = is_private_group
database.group_preferences.update(group.id, group.preferences)
## Set Up Household
household = database.households.get_one(unique_user.household_id)
assert household and household.preferences
household.preferences.private_household = is_private_household
household.preferences.recipe_public = not is_private_household
database.household_preferences.update(household.id, household.preferences)
## Set Recipe `settings.public` attribute
assert random_recipe.settings
random_recipe.settings.public = not is_private_recipe
database.recipes.update(random_recipe.slug, random_recipe)
## Add a known food to the recipe
known_food = database.ingredient_foods.create(
SaveIngredientFood(id=uuid4(), name=random_string(), group_id=unique_user.group_id)
)
random_recipe.recipe_ingredient = [RecipeIngredient(food_id=known_food.id, food=known_food)]
random_recipe.settings.disable_amount = False
database.recipes.update(random_recipe.slug, random_recipe)
## Try to find suggested recipes
recipe_group = database.groups.get_by_slug_or_id(random_recipe.group_id)
recipe_household = database.households.get_by_slug_or_id(random_recipe.household_id)
assert recipe_group
assert recipe_household
response = api_client.get(
api_routes.explore_groups_group_slug_recipes_suggestions(recipe_group.slug),
params={"maxMissingFoods": 0, "foods": [str(known_food.id)], "includeFoodsOnHand": False},
)
if is_private_group:
assert response.status_code == 404
assert response.json()["detail"] == "group not found"
return
if is_private_household or is_private_recipe:
if is_private_group:
assert response.json()["detail"] == "group not found"
else:
assert response.json()["items"] == []
return
as_json = response.json()
assert len(as_json["items"]) == 1
assert as_json["items"][0]["recipe"]["name"] == random_recipe.name
assert as_json["items"][0]["recipe"]["slug"] == random_recipe.slug

View file

@ -0,0 +1,581 @@
import random
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_ingredient import IngredientFood, RecipeIngredient, SaveIngredientFood
from mealie.schema.recipe.recipe_settings import RecipeSettings
from mealie.schema.recipe.recipe_tool import RecipeToolOut, RecipeToolSave
from tests.utils import api_routes
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def create_food(user: TestUser, on_hand: bool = False):
return user.repos.ingredient_foods.create(
SaveIngredientFood(id=uuid4(), name=random_string(), group_id=user.group_id, on_hand=on_hand)
)
def create_tool(user: TestUser, on_hand: bool = False):
return user.repos.tools.create(
RecipeToolSave(id=uuid4(), name=random_string(), group_id=user.group_id, on_hand=on_hand)
)
def create_recipe(
user: TestUser,
*,
foods: list[IngredientFood] | None = None,
tools: list[RecipeToolOut] | None = None,
disable_amount: bool = False,
**kwargs,
):
if foods:
ingredients = [RecipeIngredient(food_id=food.id, food=food) for food in foods]
else:
ingredients = []
recipe = user.repos.recipes.create(
Recipe(
user_id=user.user_id,
group_id=user.group_id,
name=kwargs.pop("name", random_string()),
recipe_ingredient=ingredients,
tools=tools or [],
settings=RecipeSettings(disable_amount=disable_amount),
**kwargs,
)
)
return recipe
@pytest.fixture(autouse=True)
def base_recipes(unique_user: TestUser, h2_user: TestUser):
for user in [unique_user, h2_user]:
for _ in range(10):
create_recipe(
user,
foods=[create_food(user) for _ in range(random_int(5, 10))],
tools=[create_tool(user) for _ in range(random_int(5, 10))],
)
@pytest.mark.parametrize("filter_foods", [True, False])
@pytest.mark.parametrize("filter_tools", [True, False])
def test_suggestion_filter(api_client: TestClient, unique_user: TestUser, filter_foods: bool, filter_tools: bool):
create_params: dict = {}
api_params: dict = {"maxMissingFoods": 0, "maxMissingTools": 0, "limit": 10}
if filter_foods:
known_food = create_food(unique_user)
create_params["foods"] = [known_food]
api_params["foods"] = [str(known_food.id)]
if filter_tools:
known_tool = create_tool(unique_user)
create_params["tools"] = [known_tool]
api_params["tools"] = [str(known_tool.id)]
recipes = [create_recipe(unique_user, **create_params) for _ in range(3)]
try:
expected_recipe_ids = {str(recipe.id) for recipe in recipes if recipe.id}
response = api_client.get(api_routes.recipes_suggestions, params=api_params, headers=unique_user.token)
response.raise_for_status()
data = response.json()
if not filter_foods and not filter_tools:
assert len(data["items"]) == 10
else:
assert len(data["items"]) == 3
for item in data["items"]:
assert item["recipe"]["id"] in expected_recipe_ids
assert item["missingFoods"] == []
assert item["missingTools"] == []
finally:
for recipe in recipes:
unique_user.repos.recipes.delete(recipe.slug)
def test_food_suggestion_filter_with_max(api_client: TestClient, unique_user: TestUser):
food_1, food_2, food_3, food_4 = (create_food(unique_user) for _ in range(4))
recipe_exact = create_recipe(unique_user, foods=[food_1])
recipe_missing_one = create_recipe(unique_user, foods=[food_1, food_2])
recipe_missing_two = create_recipe(unique_user, foods=[food_1, food_2, food_3])
recipe_missing_three = create_recipe(unique_user, foods=[food_1, food_2, food_3, food_4])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 1, "includeFoodsOnHand": False, "foods": [str(food_1.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
fetched_recipe_ids = {item["recipe"]["id"] for item in data["items"]}
assert set(fetched_recipe_ids) == {str(recipe_exact.id), str(recipe_missing_one.id)}
for item in data["items"]:
missing_food_ids = [food["id"] for food in item["missingFoods"]]
if item["recipe"]["id"] == str(recipe_exact.id):
assert missing_food_ids == []
else:
assert missing_food_ids == [str(food_2.id)]
finally:
for recipe in [recipe_exact, recipe_missing_one, recipe_missing_two, recipe_missing_three]:
unique_user.repos.recipes.delete(recipe.slug)
def test_tool_suggestion_filter_with_max(api_client: TestClient, unique_user: TestUser):
tool_1, tool_2, tool_3, tool_4 = (create_tool(unique_user) for _ in range(4))
recipe_exact = create_recipe(unique_user, tools=[tool_1])
recipe_missing_one = create_recipe(unique_user, tools=[tool_1, tool_2])
recipe_missing_two = create_recipe(unique_user, tools=[tool_1, tool_2, tool_3])
recipe_missing_three = create_recipe(unique_user, tools=[tool_1, tool_2, tool_3, tool_4])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingTools": 1, "includeToolsOnHand": False, "tools": [str(tool_1.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
fetched_recipe_ids = {item["recipe"]["id"] for item in data["items"]}
assert set(fetched_recipe_ids) == {str(recipe_exact.id), str(recipe_missing_one.id)}
for item in data["items"]:
missing_tool_ids = [tool["id"] for tool in item["missingTools"]]
if item["recipe"]["id"] == str(recipe_exact.id):
assert missing_tool_ids == []
else:
assert missing_tool_ids == [str(tool_2.id)]
finally:
for recipe in [recipe_exact, recipe_missing_one, recipe_missing_two, recipe_missing_three]:
unique_user.repos.recipes.delete(recipe.slug)
def test_ignore_empty_food_filter(api_client: TestClient, unique_user: TestUser):
known_tool = create_tool(unique_user)
recipe = create_recipe(
unique_user, foods=[create_food(unique_user) for _ in range(random_int(3, 5))], tools=[known_tool]
)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "tools": [str(known_tool.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert len(data["items"]) == 1
item = data["items"][0]
assert item["recipe"]["id"] == str(recipe.id)
assert item["missingFoods"] == []
assert item["missingTools"] == []
finally:
unique_user.repos.recipes.delete(recipe.slug)
def test_ignore_empty_tool_filter(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
recipe = create_recipe(
unique_user, foods=[known_food], tools=[create_tool(unique_user) for _ in range(random_int(3, 5))]
)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "foods": [str(known_food.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert len(data["items"]) == 1
item = data["items"][0]
assert item["recipe"]["id"] == str(recipe.id)
assert item["missingFoods"] == []
assert item["missingTools"] == []
finally:
unique_user.repos.recipes.delete(recipe.slug)
@pytest.mark.parametrize("include_on_hand", [True, False])
def test_include_foods_on_hand(api_client: TestClient, unique_user: TestUser, include_on_hand: bool):
on_hand_food = create_food(unique_user, on_hand=True)
off_hand_food = create_food(unique_user, on_hand=False)
recipe = create_recipe(unique_user, foods=[on_hand_food, off_hand_food])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={
"maxMissingFoods": 0,
"maxMissingTools": 0,
"includeFoodsOnHand": include_on_hand,
"foods": [str(off_hand_food.id)],
},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
if not include_on_hand:
assert len(data["items"]) == 0
else:
assert len(data["items"]) == 1
item = data["items"][0]
assert item["recipe"]["id"] == str(recipe.id)
assert item["missingFoods"] == []
finally:
unique_user.repos.recipes.delete(recipe.slug)
@pytest.mark.parametrize("include_on_hand", [True, False])
def test_include_tools_on_hand(api_client: TestClient, unique_user: TestUser, include_on_hand: bool):
on_hand_tool = create_tool(unique_user, on_hand=True)
off_hand_tool = create_tool(unique_user, on_hand=False)
recipe = create_recipe(unique_user, tools=[on_hand_tool, off_hand_tool])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={
"maxMissingFoods": 0,
"maxMissingTools": 0,
"includeToolsOnHand": include_on_hand,
"tools": [str(off_hand_tool.id)],
},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
if not include_on_hand:
assert len(data["items"]) == 0
else:
assert len(data["items"]) == 1
item = data["items"][0]
assert item["recipe"]["id"] == str(recipe.id)
assert item["missingTools"] == []
finally:
unique_user.repos.recipes.delete(recipe.slug)
def test_exclude_recipes_with_no_foods(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
recipe_with_foods = create_recipe(unique_user, foods=[known_food])
recipe_without_foods = create_recipe(unique_user, foods=[])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "foods": [str(known_food.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {str(recipe_with_foods.id)}
for item in data["items"]:
assert item["missingFoods"] == []
finally:
for recipe in [recipe_with_foods, recipe_without_foods]:
unique_user.repos.recipes.delete(recipe.slug)
def test_include_recipes_with_no_tools(api_client: TestClient, unique_user: TestUser):
known_tool = create_tool(unique_user)
recipe_with_tools = create_recipe(unique_user, tools=[known_tool])
recipe_without_tools = create_recipe(unique_user, tools=[])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "tools": [str(known_tool.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {
str(recipe_with_tools.id),
str(recipe_without_tools.id),
}
for item in data["items"]:
assert item["missingTools"] == []
finally:
for recipe in [recipe_with_tools, recipe_without_tools]:
unique_user.repos.recipes.delete(recipe.slug)
def test_ignore_recipes_with_ingredient_amounts_disabled_with_foods(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
recipe_with_amounts = create_recipe(unique_user, foods=[known_food])
recipe_without_amounts = create_recipe(unique_user, foods=[known_food], disable_amount=True)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "foods": [str(known_food.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {str(recipe_with_amounts.id)}
for item in data["items"]:
assert item["missingFoods"] == []
finally:
for recipe in [recipe_with_amounts, recipe_without_amounts]:
unique_user.repos.recipes.delete(recipe.slug)
def test_include_recipes_with_ingredient_amounts_disabled_without_foods(api_client: TestClient, unique_user: TestUser):
known_tool = create_tool(unique_user)
recipe_with_amounts = create_recipe(unique_user, tools=[known_tool])
recipe_without_amounts = create_recipe(unique_user, tools=[known_tool], disable_amount=True)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={
"maxMissingFoods": 0,
"maxMissingTools": 0,
"includeFoodsOnHand": False,
"tools": [str(known_tool.id)],
},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {
str(recipe_with_amounts.id),
str(recipe_without_amounts.id),
}
for item in data["items"]:
assert item["missingFoods"] == []
finally:
for recipe in [recipe_with_amounts, recipe_without_amounts]:
unique_user.repos.recipes.delete(recipe.slug)
def test_exclude_recipes_with_no_user_foods(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
food_on_hand = create_food(unique_user, on_hand=True)
recipe_with_user_food = create_recipe(unique_user, foods=[known_food])
recipe_with_on_hand_food = create_recipe(unique_user, foods=[food_on_hand])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 10, "includeFoodsOnHand": True, "foods": [str(known_food.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {str(recipe_with_user_food.id)}
assert data["items"][0]["missingFoods"] == []
finally:
for recipe in [recipe_with_user_food, recipe_with_on_hand_food]:
unique_user.repos.recipes.delete(recipe.slug)
def test_recipe_order(api_client: TestClient, unique_user: TestUser):
user_food_1, user_food_2, other_food_1, other_food_2, other_food_3 = (create_food(unique_user) for _ in range(5))
user_tool_1, other_tool_1, other_tool_2 = (create_tool(unique_user) for _ in range(3))
food_on_hand = create_food(unique_user, on_hand=True)
recipe_lambdas = [
# No missing tools or foods
(0, lambda: create_recipe(unique_user, tools=[user_tool_1], foods=[user_food_1])),
# No missing tools, one missing food
(1, lambda: create_recipe(unique_user, tools=[user_tool_1], foods=[user_food_1, other_food_1])),
# One missing tool, no missing foods
(2, lambda: create_recipe(unique_user, tools=[user_tool_1, other_tool_1], foods=[user_food_1])),
# One missing tool, one missing food
(3, lambda: create_recipe(unique_user, tools=[user_tool_1, other_tool_1], foods=[user_food_1, other_food_1])),
# Two missing tools, two missing foods, two user foods
(
4,
lambda: create_recipe(
unique_user,
tools=[user_tool_1, other_tool_1, other_tool_2],
foods=[user_food_1, user_food_2, other_food_1, other_food_2],
),
),
# Two missing tools, two missing foods, one user food
(
5,
lambda: create_recipe(
unique_user,
tools=[user_tool_1, other_tool_1, other_tool_2],
foods=[user_food_1, other_food_1, other_food_2],
),
),
# Two missing tools, three missing foods, two user foods, don't include food on hand
(
6,
lambda: create_recipe(
unique_user,
tools=[user_tool_1, other_tool_1, other_tool_2],
foods=[user_food_1, user_food_2, other_food_1, other_food_2, other_food_3],
),
),
# Two missing tools, three missing foods, one user food, include food on hand
(
7,
lambda: create_recipe(
unique_user,
tools=[user_tool_1, other_tool_1, other_tool_2],
foods=[food_on_hand, user_food_1, other_food_1, other_food_2, other_food_3],
),
),
]
# create recipes in a random order
random.shuffle(recipe_lambdas)
recipe_tuples: list[tuple[int, Recipe]] = []
for i, recipe_lambda in recipe_lambdas:
recipe_tuples.append((i, recipe_lambda()))
recipe_tuples.sort(key=lambda x: x[0])
recipes = [recipe_tuple[1] for recipe_tuple in recipe_tuples]
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={
"maxMissingFoods": 3,
"maxMissingTools": 3,
"includeFoodsOnHand": True,
"includeToolsOnHand": True,
"limit": 10,
"foods": [str(user_food_1.id), str(user_food_2.id)],
"tools": [str(user_tool_1.id)],
},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert len(data["items"]) == len(recipes)
for i, (item, recipe) in enumerate(zip(data["items"], recipes, strict=True)):
try:
assert item["recipe"]["id"] == str(recipe.id)
except AssertionError as e:
raise AssertionError(f"Recipe in position {i} was incorrect") from e
finally:
for recipe in recipes:
unique_user.repos.recipes.delete(recipe.slug)
def test_respect_user_sort(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
# Create recipes with names A, B, C, D out of order
recipe_b = create_recipe(unique_user, foods=[known_food], name="B")
recipe_c = create_recipe(unique_user, foods=[known_food, create_food(unique_user)], name="C")
recipe_a = create_recipe(unique_user, foods=[known_food, create_food(unique_user)], name="A")
recipe_d = create_recipe(unique_user, foods=[known_food, create_food(unique_user)], name="D")
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 1, "foods": [str(known_food.id)], "orderBy": "name", "orderDirection": "desc"},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert len(data["items"]) == 4
# "B" should come first because it matches all foods, even though the user sort would put it last
assert [item["recipe"]["name"] for item in data["items"]] == ["B", "D", "C", "A"]
finally:
for recipe in [recipe_a, recipe_b, recipe_c, recipe_d]:
unique_user.repos.recipes.delete(recipe.slug)
def test_limit_param(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
limit = random_int(12, 20)
recipes = [create_recipe(unique_user, foods=[known_food]) for _ in range(limit)]
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "foods": [str(known_food.id)], "limit": limit},
headers=unique_user.token,
)
response.raise_for_status()
assert len(response.json()["items"]) == limit
finally:
for recipe in recipes:
unique_user.repos.recipes.delete(recipe.slug)
def test_query_filter(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
recipes_with_prefix = [
create_recipe(unique_user, foods=[known_food], name=f"MY_PREFIX{random_string()}") for _ in range(10)
]
recipes_without_prefix = [
create_recipe(unique_user, foods=[known_food], name=f"MY_OTHER_PREFIX{random_string()}") for _ in range(10)
]
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "foods": [str(known_food.id)], "queryFilter": 'name LIKE "MY_PREFIX%"'},
headers=unique_user.token,
)
response.raise_for_status()
assert len(response.json()["items"]) == len(recipes_with_prefix)
assert {item["recipe"]["id"] for item in response.json()["items"]} == {
str(recipe.id) for recipe in recipes_with_prefix
}
finally:
for recipe in recipes_with_prefix + recipes_without_prefix:
unique_user.repos.recipes.delete(recipe.slug)
def test_include_cross_household_recipes(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
known_food = create_food(unique_user)
recipe = create_recipe(unique_user, foods=[known_food])
other_recipe = create_recipe(h2_user, foods=[known_food])
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "foods": [str(known_food.id)], "includeCrossHousehold": True},
headers=h2_user.token,
)
response.raise_for_status()
data = response.json()
assert len(data["items"]) == 2
assert {item["recipe"]["id"] for item in data["items"]} == {str(recipe.id), str(other_recipe.id)}
finally:
unique_user.repos.recipes.delete(recipe.slug)
h2_user.repos.recipes.delete(other_recipe.slug)

View file

@ -634,7 +634,8 @@ def test_pagination_order_by_multiple(unique_user: TestUser, order_direction: Or
random.shuffle(abbreviations)
random.shuffle(descriptions)
assert abbreviations != descriptions
while abbreviations == descriptions:
random.shuffle(descriptions)
units_to_create: list[SaveIngredientUnit] = []
for abbreviation in abbreviations:
@ -694,7 +695,8 @@ def test_pagination_order_by_multiple_directions(
random.shuffle(abbreviations)
random.shuffle(descriptions)
assert abbreviations != descriptions
while abbreviations == descriptions:
random.shuffle(descriptions)
units_to_create: list[SaveIngredientUnit] = []
for abbreviation in abbreviations:

View file

@ -161,6 +161,8 @@ recipes_create_zip = "/api/recipes/create/zip"
"""`/api/recipes/create/zip`"""
recipes_exports = "/api/recipes/exports"
"""`/api/recipes/exports`"""
recipes_suggestions = "/api/recipes/suggestions"
"""`/api/recipes/suggestions`"""
recipes_test_scrape_url = "/api/recipes/test-scrape-url"
"""`/api/recipes/test-scrape-url`"""
recipes_timeline_events = "/api/recipes/timeline/events"
@ -303,6 +305,11 @@ def explore_groups_group_slug_recipes_recipe_slug(group_slug, recipe_slug):
return f"{prefix}/explore/groups/{group_slug}/recipes/{recipe_slug}"
def explore_groups_group_slug_recipes_suggestions(group_slug):
"""`/api/explore/groups/{group_slug}/recipes/suggestions`"""
return f"{prefix}/explore/groups/{group_slug}/recipes/suggestions"
def foods_item_id(item_id):
"""`/api/foods/{item_id}`"""
return f"{prefix}/foods/{item_id}"