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
@ -8,20 +13,34 @@ from mealie.core.dependencies import get_admin_user, get_current_user
class AdminAPIRouter(APIRouter):
"""Router for functions to be protected behind admin authentication"""
def __init__(
self,
tags: Optional[list[str]] = None,
prefix: str = "",
):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)], **kwargs)
class UserAPIRouter(APIRouter):
"""Router for functions to be protected behind user authentication"""
def __init__(
self,
tags: Optional[list[str]] = None,
prefix: str = "",
):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)], **kwargs)
class MealieCrudRoute(APIRoute):
"""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.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper
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.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)

View file

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.group.group_events import (
GroupEventNotifierCreate,
GroupEventNotifierOut,
@ -17,7 +18,9 @@ from mealie.schema.mapper import cast
from mealie.schema.query import GetAll
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)

View file

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.labels import (
MultiPurposeLabelCreate,
MultiPurposeLabelOut,
@ -16,7 +17,7 @@ from mealie.schema.labels import (
from mealie.schema.mapper import cast
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)

View file

@ -6,6 +6,7 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema.group.group_shopping_list import (
ShoppingListCreate,
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.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)
@ -95,6 +98,7 @@ class ShoppingListItemController(BaseUserController):
return shopping_list_item
@item_router.head("/{item_id}", response_model=ShoppingListItemOut)
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@ -144,7 +148,7 @@ class ShoppingListItemController(BaseUserController):
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)
@ -189,6 +193,7 @@ class ShoppingListController(BaseUserController):
return val
@router.head("/{item_id}", response_model=ShoppingListOut)
@router.get("/{item_id}", response_model=ShoppingListOut)
def get_one(self, item_id: UUID4):
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.routes._base import BaseUserController, controller
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.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
@ -112,7 +112,7 @@ class RecipeExportController(BaseRecipeController):
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)

View file

@ -6,12 +6,13 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper
from mealie.schema.query import GetAll
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
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)

View file

@ -6,12 +6,13 @@ from pydantic import UUID4
from mealie.routes._base.base_controllers import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute
from mealie.schema import mapper
from mealie.schema.query import GetAll
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
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)

View file

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

View file

@ -46,6 +46,8 @@ def serialize_list_items(list_items: list[ShoppingListItemOut]) -> list:
item_dict["id"] = str(item.id)
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
@ -151,6 +153,8 @@ def test_shopping_list_items_update_many_reorder(
as_dict.append(item_dict)
# 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)
assert response.status_code == 200