feat: added "last-modified" header to supported record types (#1379)

* fixed type error

* exposed created/updated timestamps to shopping list schema

* added custom route to mix in "last-modified" header when available in CRUD routes

* mixed in MealieCrudRoute to APIRouters

* added HEAD route for shopping lists/list-items

* replaced default serializer with FastAPI's
This commit is contained in:
Michael Genson 2022-06-21 12:41:14 -05:00 committed by GitHub
parent 5db4dedc3f
commit 292bf7068a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 67 additions and 25 deletions

View File

@ -1,6 +1,11 @@
from typing import Optional import json
from collections.abc import Callable
from enum import Enum
from json.decoder import JSONDecodeError
from typing import Optional, Union
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request, Response
from fastapi.routing import APIRoute
from mealie.core.dependencies import get_admin_user, get_current_user from mealie.core.dependencies import get_admin_user, get_current_user
@ -8,20 +13,34 @@ from mealie.core.dependencies import get_admin_user, get_current_user
class AdminAPIRouter(APIRouter): class AdminAPIRouter(APIRouter):
"""Router for functions to be protected behind admin authentication""" """Router for functions to be protected behind admin authentication"""
def __init__( def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
self, super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)], **kwargs)
tags: Optional[list[str]] = None,
prefix: str = "",
):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
class UserAPIRouter(APIRouter): class UserAPIRouter(APIRouter):
"""Router for functions to be protected behind user authentication""" """Router for functions to be protected behind user authentication"""
def __init__( def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
self, super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)], **kwargs)
tags: Optional[list[str]] = None,
prefix: str = "",
): class MealieCrudRoute(APIRoute):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)]) """Route class to include the last-modified header when returning a MealieModel, when available"""
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
try:
response = await original_route_handler(request)
response_body = json.loads(response.body)
if type(response_body) == dict:
if last_modified := response_body.get("updateAt"):
response.headers["last-modified"] = last_modified
except JSONDecodeError:
pass
return response
return custom_route_handler

View File

@ -6,12 +6,13 @@ from pydantic import UUID4
from mealie.core.exceptions import mealie_registered_exceptions from mealie.core.exceptions import mealie_registered_exceptions
from mealie.routes._base import BaseUserController, controller from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
from mealie.services.event_bus_service.message_types import EventTypes from mealie.services.event_bus_service.message_types import EventTypes
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"]) router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"], route_class=MealieCrudRoute)
@controller(router) @controller(router)

View File

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.group.group_events import ( from mealie.schema.group.group_events import (
GroupEventNotifierCreate, GroupEventNotifierCreate,
GroupEventNotifierOut, GroupEventNotifierOut,
@ -17,7 +18,9 @@ from mealie.schema.mapper import cast
from mealie.schema.query import GetAll from mealie.schema.query import GetAll
from mealie.services.event_bus_service.event_bus_service import EventBusService from mealie.services.event_bus_service.event_bus_service import EventBusService
router = APIRouter(prefix="/groups/events/notifications", tags=["Group: Event Notifications"]) router = APIRouter(
prefix="/groups/events/notifications", tags=["Group: Event Notifications"], route_class=MealieCrudRoute
)
@controller(router) @controller(router)

View File

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.labels import ( from mealie.schema.labels import (
MultiPurposeLabelCreate, MultiPurposeLabelCreate,
MultiPurposeLabelOut, MultiPurposeLabelOut,
@ -16,7 +17,7 @@ from mealie.schema.labels import (
from mealie.schema.mapper import cast from mealie.schema.mapper import cast
from mealie.schema.query import GetAll from mealie.schema.query import GetAll
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"]) router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"], route_class=MealieCrudRoute)
@controller(router) @controller(router)

View File

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.group.group_shopping_list import ( from mealie.schema.group.group_shopping_list import (
ShoppingListCreate, ShoppingListCreate,
ShoppingListItemCreate, ShoppingListItemCreate,
@ -23,7 +24,9 @@ from mealie.services.event_bus_service.event_bus_service import EventBusService,
from mealie.services.event_bus_service.message_types import EventTypes from mealie.services.event_bus_service.message_types import EventTypes
from mealie.services.group_services.shopping_lists import ShoppingListService from mealie.services.group_services.shopping_lists import ShoppingListService
item_router = APIRouter(prefix="/groups/shopping/items", tags=["Group: Shopping List Items"]) item_router = APIRouter(
prefix="/groups/shopping/items", tags=["Group: Shopping List Items"], route_class=MealieCrudRoute
)
@controller(item_router) @controller(item_router)
@ -95,6 +98,7 @@ class ShoppingListItemController(BaseUserController):
return shopping_list_item return shopping_list_item
@item_router.head("/{item_id}", response_model=ShoppingListItemOut)
@item_router.get("/{item_id}", response_model=ShoppingListItemOut) @item_router.get("/{item_id}", response_model=ShoppingListItemOut)
def get_one(self, item_id: UUID4): def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id) return self.mixins.get_one(item_id)
@ -144,7 +148,7 @@ class ShoppingListItemController(BaseUserController):
return shopping_list_item return shopping_list_item
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"]) router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"], route_class=MealieCrudRoute)
@controller(router) @controller(router)
@ -189,6 +193,7 @@ class ShoppingListController(BaseUserController):
return val return val
@router.head("/{item_id}", response_model=ShoppingListOut)
@router.get("/{item_id}", response_model=ShoppingListOut) @router.get("/{item_id}", response_model=ShoppingListOut)
def get_one(self, item_id: UUID4): def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id) return self.mixins.get_one(item_id)

View File

@ -19,7 +19,7 @@ from mealie.pkgs import cache
from mealie.repos.repository_recipes import RepositoryRecipes from mealie.repos.repository_recipes import RepositoryRecipes
from mealie.routes._base import BaseUserController, controller from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import UserAPIRouter from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
from mealie.schema.query import GetAll from mealie.schema.query import GetAll
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
@ -112,7 +112,7 @@ class RecipeExportController(BaseRecipeController):
return FileResponse(temp_path, filename=f"{slug}.zip") return FileResponse(temp_path, filename=f"{slug}.zip")
router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"]) router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"], route_class=MealieCrudRoute)
@controller(router) @controller(router)

View File

@ -6,12 +6,13 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.query import GetAll from mealie.schema.query import GetAll
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
from mealie.schema.response.responses import SuccessResponse from mealie.schema.response.responses import SuccessResponse
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"]) router = APIRouter(prefix="/foods", tags=["Recipes: Foods"], route_class=MealieCrudRoute)
@controller(router) @controller(router)

View File

@ -6,12 +6,13 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.query import GetAll from mealie.schema.query import GetAll
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
from mealie.schema.response.responses import SuccessResponse from mealie.schema.response.responses import SuccessResponse
router = APIRouter(prefix="/units", tags=["Recipes: Units"]) router = APIRouter(prefix="/units", tags=["Recipes: Units"], route_class=MealieCrudRoute)
@controller(router) @controller(router)

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from datetime import datetime
from typing import Optional, Union
from pydantic import UUID4 from pydantic import UUID4
@ -38,6 +39,9 @@ class ShoppingListItemCreate(MealieModel):
label_id: Optional[UUID4] = None label_id: Optional[UUID4] = None
recipe_references: list[ShoppingListItemRecipeRef] = [] recipe_references: list[ShoppingListItemRecipeRef] = []
created_at: Optional[datetime]
update_at: Optional[datetime]
class ShoppingListItemUpdate(ShoppingListItemCreate): class ShoppingListItemUpdate(ShoppingListItemCreate):
id: UUID4 id: UUID4
@ -45,7 +49,7 @@ class ShoppingListItemUpdate(ShoppingListItemCreate):
class ShoppingListItemOut(ShoppingListItemUpdate): class ShoppingListItemOut(ShoppingListItemUpdate):
label: Optional[MultiPurposeLabelSummary] label: Optional[MultiPurposeLabelSummary]
recipe_references: list[ShoppingListItemRecipeRefOut] = [] recipe_references: list[Union[ShoppingListItemRecipeRef, ShoppingListItemRecipeRefOut]] = []
class Config: class Config:
orm_mode = True orm_mode = True
@ -54,6 +58,9 @@ class ShoppingListItemOut(ShoppingListItemUpdate):
class ShoppingListCreate(MealieModel): class ShoppingListCreate(MealieModel):
name: str = None name: str = None
created_at: Optional[datetime]
update_at: Optional[datetime]
class ShoppingListRecipeRefOut(MealieModel): class ShoppingListRecipeRefOut(MealieModel):
id: UUID4 id: UUID4

View File

@ -46,6 +46,8 @@ def serialize_list_items(list_items: list[ShoppingListItemOut]) -> list:
item_dict["id"] = str(item.id) item_dict["id"] = str(item.id)
as_dict.append(item_dict) as_dict.append(item_dict)
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
as_dict = utils.jsonify(as_dict)
return as_dict return as_dict
@ -151,6 +153,8 @@ def test_shopping_list_items_update_many_reorder(
as_dict.append(item_dict) as_dict.append(item_dict)
# update list # update list
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
as_dict = utils.jsonify(as_dict)
response = api_client.put(Routes.items, json=as_dict, headers=unique_user.token) response = api_client.put(Routes.items, json=as_dict, headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200