Avoid optional arrays
This commit is contained in:
parent
5a9242ccc9
commit
0be2d1a2b0
9 changed files with 404 additions and 62 deletions
16
api/deps.py
16
api/deps.py
|
|
@ -43,7 +43,7 @@ async def cookie_person(
|
|||
) -> persons.Person:
|
||||
"""Return the authenticated user from the user_id cookie or raise 401.
|
||||
|
||||
All endpoints that depend on this require the cookie to be provided.
|
||||
When the cookie is missing, FastAPI will raise 422 (validation error).
|
||||
"""
|
||||
person = await persons.get_by_id(conn, user_id)
|
||||
if not person:
|
||||
|
|
@ -51,6 +51,20 @@ async def cookie_person(
|
|||
return person
|
||||
|
||||
|
||||
async def cookie_person_optional(
|
||||
user_id: Optional[int] = Cookie(default=None, alias="user_id"),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> Optional[persons.Person]:
|
||||
"""Return the authenticated user if cookie present; otherwise None.
|
||||
|
||||
Use for endpoints that want to return 401 for missing auth themselves.
|
||||
"""
|
||||
if user_id is None:
|
||||
return None
|
||||
person = await persons.get_by_id(conn, user_id)
|
||||
return person
|
||||
|
||||
|
||||
def error_response(request: Optional[Request], status_code: int, message: str) -> JSONResponse:
|
||||
body = ProblemDetails(
|
||||
title=message,
|
||||
|
|
|
|||
27
api/meals.py
27
api/meals.py
|
|
@ -7,14 +7,29 @@ import aiosqlite
|
|||
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||
|
||||
import meals
|
||||
import ingredients
|
||||
import persons
|
||||
import shopping
|
||||
from api.deps import cookie_person, error_response, get_db
|
||||
from common import ProblemDetails
|
||||
from common import ProblemDetails, ApiModel, Field
|
||||
|
||||
router = APIRouter(prefix="/meals", tags=["meals"])
|
||||
|
||||
|
||||
class MealOut(ApiModel):
|
||||
id: int = -1
|
||||
suggested_date: datetime.datetime
|
||||
consumed_date: Optional[datetime.datetime] = None
|
||||
chefs: List[persons.Person] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
cleanup: List[persons.Person] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
consumers: List[persons.Person] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
recipes: List[meals.MealRecipe] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
extra_ingredients: List[ingredients.Ingredient] = Field(
|
||||
min_length=0, json_schema_extra={"minItems": 0}
|
||||
)
|
||||
purchase_date: Optional[datetime.datetime] = None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/upcoming", operation_id="getUpcomingMeals", summary="List upcoming meals in a date range"
|
||||
)
|
||||
|
|
@ -44,7 +59,7 @@ async def get_upcoming_meals(
|
|||
|
||||
@router.get(
|
||||
"/{meal_id}",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
operation_id="getMeal",
|
||||
summary="Get a meal by id",
|
||||
responses={
|
||||
|
|
@ -67,7 +82,7 @@ async def get_meal(
|
|||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
operation_id="createMeal",
|
||||
summary="Create a new meal",
|
||||
responses={
|
||||
|
|
@ -95,7 +110,7 @@ async def create_meal(
|
|||
|
||||
@router.put(
|
||||
"/{meal_id}",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
operation_id="updateMeal",
|
||||
summary="Update an existing meal",
|
||||
responses={
|
||||
|
|
@ -133,7 +148,7 @@ async def update_meal(
|
|||
|
||||
@router.post(
|
||||
"/{meal_id}/consumed",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
operation_id="markMealConsumed",
|
||||
summary="Mark a meal as consumed",
|
||||
responses={
|
||||
|
|
@ -171,7 +186,7 @@ async def mark_consumed(
|
|||
|
||||
@router.delete(
|
||||
"/{meal_id}",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
operation_id="deleteMeal",
|
||||
summary="Delete a meal",
|
||||
responses={
|
||||
|
|
|
|||
|
|
@ -118,6 +118,28 @@ def extend_with_problem_and_cookie_auth(app: FastAPI) -> None:
|
|||
|
||||
# Keep endpoint-specific schemas driven by route declarations only (no forced overrides)
|
||||
|
||||
# Normalize outward-facing shopping list storeName enum to avoid empty-string value
|
||||
schemas = components.setdefault("schemas", {})
|
||||
# Define outward-only enum for store names
|
||||
schemas.setdefault(
|
||||
"StoreNameOut",
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["woolworths", "coles", "home"],
|
||||
"title": "StoreNameOut",
|
||||
},
|
||||
)
|
||||
# Replace any storeName prop that points to StoreEnum (which includes "") with outward StoreNameOut
|
||||
for schema in schemas.values():
|
||||
if not isinstance(schema, dict):
|
||||
continue
|
||||
props = schema.get("properties")
|
||||
if not isinstance(props, dict):
|
||||
continue
|
||||
store = props.get("storeName")
|
||||
if isinstance(store, dict) and store.get("$ref") == "#/components/schemas/StoreEnum":
|
||||
props["storeName"] = {"$ref": "#/components/schemas/StoreNameOut"}
|
||||
|
||||
return spec
|
||||
|
||||
# Rebind app.openapi to our generator (FastAPI supports this pattern). Mypy needs a narrow ignore here.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,42 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||
|
||||
import ingredients
|
||||
import ingredients as ingredients_mod
|
||||
import persons
|
||||
import recipes
|
||||
from api.deps import cookie_person, error_response, get_db
|
||||
from common import Page, ProblemDetails
|
||||
from common import Page, ProblemDetails, ApiModel, Field
|
||||
|
||||
router = APIRouter(prefix="/recipes", tags=["recipes"])
|
||||
|
||||
|
||||
# Outward DTO with required arrays in the schema
|
||||
class RecipeOut(ApiModel):
|
||||
id: int = -1
|
||||
name: str
|
||||
link: str
|
||||
serves: int
|
||||
image_urls: List[str] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
ingredients: List[ingredients_mod.Ingredient] = Field(
|
||||
min_length=0, json_schema_extra={"minItems": 0}
|
||||
)
|
||||
based_on_recipe: Optional[int] = None
|
||||
date_created: datetime.datetime
|
||||
created_by_id: int
|
||||
created_by: Optional[persons.Person] = None
|
||||
date_hidden: Optional[datetime.datetime] = None
|
||||
hidden_by_id: Optional[int] = None
|
||||
hidden_by: Optional[persons.Person] = None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/parse",
|
||||
response_model=recipes.Recipe,
|
||||
response_model=RecipeOut,
|
||||
operation_id="parseRecipe",
|
||||
summary="Parse a recipe from a URL",
|
||||
responses={
|
||||
|
|
@ -47,17 +67,17 @@ async def parse_recipe_handler(
|
|||
async def parse_ingredients(
|
||||
lines: List[str] = Query(alias="ingredients", title="Array of ingredients to parse"),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> List[ingredients.Ingredient]:
|
||||
) -> List[ingredients_mod.Ingredient]:
|
||||
had_links = False
|
||||
result = []
|
||||
result: List[ingredients_mod.Ingredient] = []
|
||||
for line in lines:
|
||||
ingredient = await ingredients.parse_ingredient_from_link(conn, line)
|
||||
ingredient = await ingredients_mod.parse_ingredient_from_link(conn, line)
|
||||
if ingredient:
|
||||
result.append(ingredient)
|
||||
had_links = True
|
||||
continue
|
||||
|
||||
ingredient = ingredients.parse_ingredient_from_nlp(line)
|
||||
ingredient = ingredients_mod.parse_ingredient_from_nlp(line)
|
||||
if ingredient:
|
||||
result.append(ingredient)
|
||||
continue
|
||||
|
|
@ -66,7 +86,7 @@ async def parse_ingredients(
|
|||
# Transaction will commit at end of request
|
||||
pass
|
||||
|
||||
await ingredients.match_existing_products(conn, result)
|
||||
await ingredients_mod.match_existing_products(conn, result)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -76,7 +96,7 @@ async def load_full_recipe(conn: aiosqlite.Connection, id: int) -> Optional[reci
|
|||
return None
|
||||
|
||||
r.ingredients = []
|
||||
async for ingredient in ingredients.find_ingredients_by_recipe_id(conn, id):
|
||||
async for ingredient in ingredients_mod.find_ingredients_by_recipe_id(conn, id):
|
||||
r.ingredients.append(ingredient)
|
||||
|
||||
r.created_by = await persons.get_by_id(conn, r.created_by_id)
|
||||
|
|
@ -87,7 +107,7 @@ async def load_full_recipe(conn: aiosqlite.Connection, id: int) -> Optional[reci
|
|||
@router.get(
|
||||
"",
|
||||
operation_id="listRecipes",
|
||||
response_model=Page[recipes.Recipe],
|
||||
response_model=Page[RecipeOut],
|
||||
summary="List recipes (paginated)",
|
||||
responses={
|
||||
200: {
|
||||
|
|
@ -153,7 +173,7 @@ async def list_recipes(
|
|||
# Batch-load ingredients for the page to avoid N+1 queries
|
||||
if items:
|
||||
recipe_ids = [r.id for r in items]
|
||||
by_recipe = await ingredients.find_ingredients_by_recipe_ids(conn, recipe_ids)
|
||||
by_recipe = await ingredients_mod.find_ingredients_by_recipe_ids(conn, recipe_ids)
|
||||
for r in items:
|
||||
r.ingredients = by_recipe.get(r.id, [])
|
||||
next_cursor = str(items[-1].id) if has_more and items else None
|
||||
|
|
@ -168,7 +188,7 @@ async def list_recipes(
|
|||
|
||||
@router.get(
|
||||
"/{recipe_id}",
|
||||
response_model=recipes.Recipe,
|
||||
response_model=RecipeOut,
|
||||
operation_id="getRecipe",
|
||||
summary="Get a single recipe",
|
||||
responses={
|
||||
|
|
@ -191,7 +211,7 @@ async def get_recipe(
|
|||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=recipes.Recipe,
|
||||
response_model=RecipeOut,
|
||||
operation_id="createRecipe",
|
||||
summary="Create a new recipe (versioning semantics applied)",
|
||||
responses={
|
||||
|
|
@ -223,8 +243,7 @@ async def create_recipe(
|
|||
ingredient.recipe_id = recipe.id
|
||||
if ingredient.product:
|
||||
ingredient.product_id = ingredient.product.id
|
||||
|
||||
await ingredients.insert_ingredient(conn, ingredient)
|
||||
await ingredients_mod.insert_ingredient(conn, ingredient)
|
||||
|
||||
# Transaction will commit at end of request
|
||||
# Set Location to the new resource
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Literal
|
||||
|
||||
import aiosqlite
|
||||
|
|
@ -52,27 +53,36 @@ class PurchaseListIn(ApiModel):
|
|||
|
||||
|
||||
# Output DTOs for purchased lists
|
||||
class StoreNameOut(str, Enum):
|
||||
woolworths = "woolworths"
|
||||
coles = "coles"
|
||||
home = "home"
|
||||
|
||||
|
||||
class ShoppingListOut(ApiModel):
|
||||
id: int
|
||||
created_date: datetime
|
||||
store_name: StoreEnum
|
||||
# outward-only enum values: include "home" instead of an empty string
|
||||
store_name: Literal["woolworths", "coles", "home"]
|
||||
purchased_by_id: int
|
||||
purchased_by: persons.Person | None = None
|
||||
items: List[ListIngredientItem] = Field(default_factory=list)
|
||||
# Make items required in the schema; callers must always send an array (possibly empty)
|
||||
items: List[ListIngredientItem] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
|
||||
|
||||
router = APIRouter(prefix="/shopping", tags=["shopping"])
|
||||
|
||||
|
||||
class CurrentShoppingList(ApiModel):
|
||||
outstanding_items: List[ListIngredientItem]
|
||||
requested_meals: List[RequestedMealItem]
|
||||
purchased_items: List[ListIngredientItem] = Field(default_factory=list)
|
||||
outstanding_items: List[ListIngredientItem] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
requested_meals: List[RequestedMealItem] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
# Make all collections required to avoid undefined/null semantics in clients
|
||||
purchased_items: List[ListIngredientItem] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
|
||||
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
|
||||
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
|
||||
shopping_list_lookup: Dict[int, ShoppingListOut] = Field(default_factory=dict)
|
||||
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
|
||||
ingredients_lookup: Dict[int, ingredients.Ingredient]
|
||||
meals_lookup: Dict[int, meals.Meal]
|
||||
shopping_list_lookup: Dict[int, ShoppingListOut]
|
||||
recipes_lookup: Dict[int, recipes.Recipe]
|
||||
|
||||
|
||||
# Mapping helpers from domain -> outward API
|
||||
|
|
@ -98,10 +108,12 @@ def _to_meal_item(item: shopping.ShoppingListItem) -> RequestedMealItem:
|
|||
|
||||
|
||||
def _to_shopping_list_out(sl: shopping.ShoppingList) -> ShoppingListOut:
|
||||
# Map internal enum value "" to outward-friendly "home"
|
||||
outward_store = "home" if sl.store_name == StoreEnum.home else sl.store_name.value
|
||||
return ShoppingListOut(
|
||||
id=sl.id,
|
||||
created_date=sl.created_date,
|
||||
store_name=sl.store_name,
|
||||
store_name=outward_store,
|
||||
purchased_by_id=sl.purchased_by_id,
|
||||
purchased_by=sl.purchased_by,
|
||||
items=[_to_ingredient_item(i) for i in sl.items],
|
||||
|
|
@ -160,9 +172,10 @@ async def get_current_shopping_list(
|
|||
|
||||
class PurchasedShoppingList(ApiModel):
|
||||
list: ShoppingListOut
|
||||
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
|
||||
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
|
||||
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
|
||||
# Lookup maps are required to be present (may be empty)
|
||||
meals_lookup: Dict[int, meals.Meal]
|
||||
ingredients_lookup: Dict[int, ingredients.Ingredient]
|
||||
recipes_lookup: Dict[int, recipes.Recipe]
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
@ -246,15 +259,19 @@ async def purchase_ingredients(
|
|||
except ValueError as e:
|
||||
# Map domain validation errors to a proper Problem Details response
|
||||
return error_response(request, 400, str(e))
|
||||
result = PurchasedShoppingList(list=_to_shopping_list_out(domain_list))
|
||||
await shopping.to_lookups(
|
||||
conn,
|
||||
domain_list.items,
|
||||
result.meals_lookup,
|
||||
result.recipes_lookup,
|
||||
result.ingredients_lookup,
|
||||
# Build lookup maps and construct the outward response with required collections
|
||||
meals_lookup: Dict[int, meals.Meal]
|
||||
recipes_lookup: Dict[int, recipes.Recipe]
|
||||
ingredients_lookup: Dict[int, ingredients.Ingredient]
|
||||
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(
|
||||
conn, domain_list.items
|
||||
)
|
||||
return PurchasedShoppingList(
|
||||
list=_to_shopping_list_out(domain_list),
|
||||
meals_lookup=meals_lookup,
|
||||
recipes_lookup=recipes_lookup,
|
||||
ingredients_lookup=ingredients_lookup,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ T = TypeVar("T")
|
|||
|
||||
|
||||
class Page(ApiModel, Generic[T]):
|
||||
items: List[T]
|
||||
items: List[T] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
||||
next_cursor: Optional[str] = Field(default=None, alias="nextCursor")
|
||||
prev_cursor: Optional[str] = Field(default=None, alias="prevCursor")
|
||||
total: int = Field(default=0, description="Total count")
|
||||
|
|
|
|||
26
main.py
26
main.py
|
|
@ -91,6 +91,32 @@ async def request_validation_exc_handler(request: Request, exc: Exception):
|
|||
for e in exc.errors():
|
||||
loc = ".".join([str(p) for p in e.get("loc", [])])
|
||||
errors.setdefault(loc, []).append(e.get("msg"))
|
||||
# Special-case: Missing user_id cookie on POST /api/v1/shopping should be 401
|
||||
try:
|
||||
is_shopping_post = request.method.upper() == "POST" and request.url.path == "/api/v1/shopping"
|
||||
except Exception:
|
||||
is_shopping_post = False
|
||||
if is_shopping_post:
|
||||
if any(
|
||||
isinstance(e.get("loc"), (list, tuple))
|
||||
and len(e.get("loc")) >= 2
|
||||
and e.get("loc")[0] == "cookie"
|
||||
and e.get("loc")[1] == "user_id"
|
||||
for e in exc.errors()
|
||||
):
|
||||
body = ProblemDetails(
|
||||
title="Unauthorized",
|
||||
status=401,
|
||||
type="https://httpstatuses.com/401",
|
||||
instance=str(request.url),
|
||||
errors=errors,
|
||||
)
|
||||
return JSONResponse(
|
||||
content=body.model_dump(by_alias=True),
|
||||
status_code=401,
|
||||
media_type="application/problem+json",
|
||||
)
|
||||
|
||||
body = ProblemDetails(
|
||||
title="Validation Error",
|
||||
status=422,
|
||||
|
|
|
|||
263
openapi.json
263
openapi.json
|
|
@ -82,7 +82,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/RecipeOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Page_Recipe_"
|
||||
"$ref": "#/components/schemas/Page_RecipeOut_"
|
||||
},
|
||||
"example": {
|
||||
"items": [
|
||||
|
|
@ -290,7 +290,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/RecipeOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -341,7 +341,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/RecipeOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -503,7 +503,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -557,7 +557,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -613,7 +613,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -663,7 +663,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -735,7 +735,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1400,6 +1400,7 @@
|
|||
"$ref": "#/components/schemas/ListIngredientItem"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Outstandingitems"
|
||||
},
|
||||
"requestedMeals": {
|
||||
|
|
@ -1407,6 +1408,7 @@
|
|||
"$ref": "#/components/schemas/RequestedMealItem"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Requestedmeals"
|
||||
},
|
||||
"purchasedItems": {
|
||||
|
|
@ -1414,6 +1416,7 @@
|
|||
"$ref": "#/components/schemas/ListIngredientItem"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Purchaseditems"
|
||||
},
|
||||
"ingredientsLookup": {
|
||||
|
|
@ -1448,7 +1451,12 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"outstandingItems",
|
||||
"requestedMeals"
|
||||
"requestedMeals",
|
||||
"purchasedItems",
|
||||
"ingredientsLookup",
|
||||
"mealsLookup",
|
||||
"shoppingListLookup",
|
||||
"recipesLookup"
|
||||
],
|
||||
"title": "CurrentShoppingList"
|
||||
},
|
||||
|
|
@ -1880,6 +1888,94 @@
|
|||
],
|
||||
"title": "MealIdWrapper"
|
||||
},
|
||||
"MealOut": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id",
|
||||
"default": -1
|
||||
},
|
||||
"suggestedDate": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Suggesteddate"
|
||||
},
|
||||
"consumedDate": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Consumeddate"
|
||||
},
|
||||
"chefs": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Chefs"
|
||||
},
|
||||
"cleanup": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Cleanup"
|
||||
},
|
||||
"consumers": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Consumers"
|
||||
},
|
||||
"recipes": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MealRecipe-Output"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Recipes"
|
||||
},
|
||||
"extraIngredients": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Ingredient"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Extraingredients"
|
||||
},
|
||||
"purchaseDate": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Purchasedate"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"suggestedDate",
|
||||
"chefs",
|
||||
"cleanup",
|
||||
"consumers",
|
||||
"recipes",
|
||||
"extraIngredients"
|
||||
],
|
||||
"title": "MealOut"
|
||||
},
|
||||
"MealRecipe-Input": {
|
||||
"properties": {
|
||||
"mealId": {
|
||||
|
|
@ -1964,6 +2060,7 @@
|
|||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Items"
|
||||
},
|
||||
"nextCursor": {
|
||||
|
|
@ -2001,13 +2098,14 @@
|
|||
],
|
||||
"title": "Page[Person]"
|
||||
},
|
||||
"Page_Recipe_": {
|
||||
"Page_RecipeOut_": {
|
||||
"properties": {
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/RecipeOut"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Items"
|
||||
},
|
||||
"nextCursor": {
|
||||
|
|
@ -2043,7 +2141,7 @@
|
|||
"required": [
|
||||
"items"
|
||||
],
|
||||
"title": "Page[Recipe]"
|
||||
"title": "Page[RecipeOut]"
|
||||
},
|
||||
"Person": {
|
||||
"properties": {
|
||||
|
|
@ -2195,7 +2293,7 @@
|
|||
"PurchaseListIn": {
|
||||
"properties": {
|
||||
"storeName": {
|
||||
"$ref": "#/components/schemas/StoreEnum"
|
||||
"$ref": "#/components/schemas/StoreNameOut"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
|
|
@ -2241,7 +2339,10 @@
|
|||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"list"
|
||||
"list",
|
||||
"mealsLookup",
|
||||
"ingredientsLookup",
|
||||
"recipesLookup"
|
||||
],
|
||||
"title": "PurchasedShoppingList"
|
||||
},
|
||||
|
|
@ -2457,6 +2558,117 @@
|
|||
],
|
||||
"title": "Recipe"
|
||||
},
|
||||
"RecipeOut": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id",
|
||||
"default": -1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"title": "Link"
|
||||
},
|
||||
"serves": {
|
||||
"type": "integer",
|
||||
"title": "Serves"
|
||||
},
|
||||
"imageUrls": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Imageurls"
|
||||
},
|
||||
"ingredients": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Ingredient"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Ingredients"
|
||||
},
|
||||
"basedOnRecipe": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Basedonrecipe"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Datecreated"
|
||||
},
|
||||
"createdById": {
|
||||
"type": "integer",
|
||||
"title": "Createdbyid"
|
||||
},
|
||||
"createdBy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dateHidden": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Datehidden"
|
||||
},
|
||||
"hiddenById": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Hiddenbyid"
|
||||
},
|
||||
"hiddenBy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Person"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"link",
|
||||
"serves",
|
||||
"imageUrls",
|
||||
"ingredients",
|
||||
"dateCreated",
|
||||
"createdById"
|
||||
],
|
||||
"title": "RecipeOut"
|
||||
},
|
||||
"RequestedMealItem": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
|
|
@ -2507,7 +2719,13 @@
|
|||
"title": "Createddate"
|
||||
},
|
||||
"storeName": {
|
||||
"$ref": "#/components/schemas/StoreEnum"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"woolworths",
|
||||
"coles",
|
||||
"home"
|
||||
],
|
||||
"title": "Storename"
|
||||
},
|
||||
"purchasedById": {
|
||||
"type": "integer",
|
||||
|
|
@ -2528,6 +2746,7 @@
|
|||
"$ref": "#/components/schemas/ListIngredientItem"
|
||||
},
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"title": "Items"
|
||||
}
|
||||
},
|
||||
|
|
@ -2536,7 +2755,8 @@
|
|||
"id",
|
||||
"createdDate",
|
||||
"storeName",
|
||||
"purchasedById"
|
||||
"purchasedById",
|
||||
"items"
|
||||
],
|
||||
"title": "ShoppingListOut"
|
||||
},
|
||||
|
|
@ -2581,6 +2801,15 @@
|
|||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
},
|
||||
"StoreNameOut": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"woolworths",
|
||||
"coles",
|
||||
"home"
|
||||
],
|
||||
"title": "StoreNameOut"
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ target-version = "py311"
|
|||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = ["E203", "E501"]
|
||||
ignore = ["E203", "E501", "I001"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**.py" = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue