feat(cookbook): Add full Cookbook app support with 13 tools and 2 resources
- Import recipes from URLs using schema.org metadata - Full CRUD operations for recipes - Search, categorize, and organize recipes - Manage keywords/tags and categories - Configure app settings and trigger reindexing
This commit is contained in:
@@ -14,6 +14,7 @@ from httpx import (
|
||||
from ..controllers.notes_search import NotesSearchController
|
||||
from .calendar import CalendarClient
|
||||
from .contacts import ContactsClient
|
||||
from .cookbook import CookbookClient
|
||||
from .deck import DeckClient
|
||||
from .groups import GroupsClient
|
||||
from .notes import NotesClient
|
||||
@@ -73,6 +74,7 @@ class NextcloudClient:
|
||||
self.tables = TablesClient(self._client, username)
|
||||
self.calendar = CalendarClient(self._client, username)
|
||||
self.contacts = ContactsClient(self._client, username)
|
||||
self.cookbook = CookbookClient(self._client, username)
|
||||
self.deck = DeckClient(self._client, username)
|
||||
self.users = UsersClient(self._client, username)
|
||||
self.groups = GroupsClient(self._client, username)
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Client for Nextcloud Cookbook app operations."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from .base import BaseNextcloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CookbookClient(BaseNextcloudClient):
|
||||
"""Client for Nextcloud Cookbook app operations."""
|
||||
|
||||
async def get_version(self) -> Dict[str, Any]:
|
||||
"""Get Cookbook app and API version."""
|
||||
response = await self._make_request("GET", "/apps/cookbook/api/version")
|
||||
return response.json()
|
||||
|
||||
async def get_config(self) -> Dict[str, Any]:
|
||||
"""Get current Cookbook app configuration."""
|
||||
response = await self._make_request("GET", "/apps/cookbook/api/v1/config")
|
||||
return response.json()
|
||||
|
||||
async def set_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set Cookbook app configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with fields like:
|
||||
- folder: Recipe folder path
|
||||
- update_interval: Auto-rescan interval in minutes
|
||||
- print_image: Whether to print images with recipes
|
||||
- visibleInfoBlocks: Visible info blocks configuration
|
||||
|
||||
Returns:
|
||||
Response with status message
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"POST", "/apps/cookbook/api/v1/config", json=config
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def reindex(self) -> str:
|
||||
"""Trigger a rescan of all recipes into the caching database.
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
"""
|
||||
response = await self._make_request("POST", "/apps/cookbook/api/v1/reindex")
|
||||
return response.json()
|
||||
|
||||
async def list_recipes(self) -> List[Dict[str, Any]]:
|
||||
"""Get all recipes in the database.
|
||||
|
||||
Returns:
|
||||
List of recipe stubs with basic information
|
||||
"""
|
||||
response = await self._make_request("GET", "/apps/cookbook/api/v1/recipes")
|
||||
return response.json()
|
||||
|
||||
async def get_recipe(self, recipe_id: int) -> Dict[str, Any]:
|
||||
"""Get a single recipe by ID.
|
||||
|
||||
Args:
|
||||
recipe_id: The recipe ID
|
||||
|
||||
Returns:
|
||||
Full recipe data
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"GET", f"/apps/cookbook/api/v1/recipes/{recipe_id}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def create_recipe(self, recipe_data: Dict[str, Any]) -> int:
|
||||
"""Create a new recipe.
|
||||
|
||||
Args:
|
||||
recipe_data: Recipe data following schema.org/Recipe format.
|
||||
Required: name
|
||||
Optional: description, ingredients, instructions, etc.
|
||||
|
||||
Returns:
|
||||
ID of the newly created recipe
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"POST", "/apps/cookbook/api/v1/recipes", json=recipe_data
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def update_recipe(self, recipe_id: int, recipe_data: Dict[str, Any]) -> int:
|
||||
"""Update an existing recipe.
|
||||
|
||||
Args:
|
||||
recipe_id: The recipe ID to update
|
||||
recipe_data: Updated recipe data
|
||||
|
||||
Returns:
|
||||
ID of the updated recipe
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"PUT", f"/apps/cookbook/api/v1/recipes/{recipe_id}", json=recipe_data
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def delete_recipe(self, recipe_id: int) -> str:
|
||||
"""Delete a recipe.
|
||||
|
||||
Args:
|
||||
recipe_id: The recipe ID to delete
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"DELETE", f"/apps/cookbook/api/v1/recipes/{recipe_id}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def import_recipe(self, url: str) -> Dict[str, Any]:
|
||||
"""Import a recipe from a URL using schema.org metadata.
|
||||
|
||||
Args:
|
||||
url: URL of the recipe to import
|
||||
|
||||
Returns:
|
||||
Full imported recipe data
|
||||
"""
|
||||
logger.info(f"Importing recipe from URL: {url}")
|
||||
response = await self._make_request(
|
||||
"POST", "/apps/cookbook/api/v1/import", json={"url": url}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def get_recipe_image(self, recipe_id: int, size: str = "full") -> bytes:
|
||||
"""Get the main image of a recipe.
|
||||
|
||||
Args:
|
||||
recipe_id: The recipe ID
|
||||
size: Image size - "full", "thumb" (250px), or "thumb16" (16px)
|
||||
|
||||
Returns:
|
||||
Image bytes
|
||||
"""
|
||||
response = await self._make_request(
|
||||
"GET",
|
||||
f"/apps/cookbook/api/v1/recipes/{recipe_id}/image",
|
||||
params={"size": size},
|
||||
)
|
||||
return response.content
|
||||
|
||||
async def search_recipes(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Search for recipes by keywords, tags, and categories.
|
||||
|
||||
Args:
|
||||
query: Search string (URL-encoded, space/comma separated)
|
||||
|
||||
Returns:
|
||||
List of matching recipe stubs
|
||||
"""
|
||||
# URL encode the query
|
||||
from urllib.parse import quote
|
||||
|
||||
encoded_query = quote(query)
|
||||
response = await self._make_request(
|
||||
"GET", f"/apps/cookbook/api/v1/search/{encoded_query}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def list_categories(self) -> List[Dict[str, Any]]:
|
||||
"""Get all known categories.
|
||||
|
||||
Note: A category name of '*' indicates recipes with no category.
|
||||
|
||||
Returns:
|
||||
List of categories with recipe counts
|
||||
"""
|
||||
response = await self._make_request("GET", "/apps/cookbook/api/v1/categories")
|
||||
return response.json()
|
||||
|
||||
async def get_recipes_in_category(self, category: str) -> List[Dict[str, Any]]:
|
||||
"""Get all recipes in a specific category.
|
||||
|
||||
Args:
|
||||
category: Category name (use "_" for recipes with no category)
|
||||
|
||||
Returns:
|
||||
List of recipe stubs in the category
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
encoded_category = quote(category)
|
||||
response = await self._make_request(
|
||||
"GET", f"/apps/cookbook/api/v1/category/{encoded_category}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def rename_category(self, old_name: str, new_name: str) -> str:
|
||||
"""Rename a category.
|
||||
|
||||
Args:
|
||||
old_name: Current category name
|
||||
new_name: New category name
|
||||
|
||||
Returns:
|
||||
New category name
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
encoded_old_name = quote(old_name)
|
||||
response = await self._make_request(
|
||||
"PUT",
|
||||
f"/apps/cookbook/api/v1/category/{encoded_old_name}",
|
||||
json={"name": new_name},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def list_keywords(self) -> List[Dict[str, Any]]:
|
||||
"""Get all known keywords/tags.
|
||||
|
||||
Returns:
|
||||
List of keywords with recipe counts
|
||||
"""
|
||||
response = await self._make_request("GET", "/apps/cookbook/api/v1/keywords")
|
||||
return response.json()
|
||||
|
||||
async def get_recipes_with_keywords(
|
||||
self, keywords: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get all recipes associated with certain keywords.
|
||||
|
||||
Args:
|
||||
keywords: List of keywords to filter by
|
||||
|
||||
Returns:
|
||||
List of recipe stubs matching the keywords
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
# Join keywords with commas
|
||||
keywords_str = ",".join(keywords)
|
||||
encoded_keywords = quote(keywords_str)
|
||||
response = await self._make_request(
|
||||
"GET", f"/apps/cookbook/api/v1/tags/{encoded_keywords}"
|
||||
)
|
||||
return response.json()
|
||||
Reference in New Issue
Block a user