refactor(backend): ♻️ Split Recipe Schema Code

This commit is contained in:
hay-kot 2021-08-27 20:17:41 -08:00
parent 5ba337ab11
commit 0675c570ce
25 changed files with 494 additions and 120 deletions

View File

@ -1,4 +1,4 @@
from mealie.schema.recipe.units_and_foods import CreateIngredientUnit
from mealie.schema.recipe import CreateIngredientUnit
from sqlalchemy.orm.session import Session
from ..data_access_layer import DatabaseAccessLayer

View File

@ -5,7 +5,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CommentIn, CommentOut, CommentSaveToDB
from mealie.schema.recipe import CommentOut, CreateComment, SaveComment
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
@ -15,20 +15,20 @@ router = UserAPIRouter()
@router.post("/{slug}/comments")
async def create_comment(
slug: str,
new_comment: CommentIn,
new_comment: CreateComment,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create comment in the Database """
new_comment = CommentSaveToDB(user=current_user.id, text=new_comment.text, recipe_slug=slug)
new_comment = SaveComment(user=current_user.id, text=new_comment.text, recipe_slug=slug)
return db.comments.create(session, new_comment)
@router.put("/{slug}/comments/{id}")
async def update_comment(
id: int,
new_comment: CommentIn,
new_comment: CreateComment,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):

View File

@ -5,7 +5,7 @@ from fastapi.datastructures import UploadFile
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeAsset
from mealie.services.image.image import scrape_image, write_image
from slugify import slugify
from sqlalchemy.orm.session import Session
@ -16,7 +16,7 @@ user_router = UserAPIRouter()
@user_router.post("/{recipe_slug}/image")
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
url: CreateRecipeByURL,
):
""" Removes an existing image and replaces it with the incoming file. """

View File

@ -10,7 +10,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in, temporary_zip_path
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import Recipe, RecipeImageTypes, RecipeURLIn
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
from mealie.schema.recipe.recipe import CreateRecipe
from mealie.schema.user import UserInDB
from mealie.services.events import create_recipe_event
@ -51,14 +51,14 @@ def create_from_name(
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: RecipeURLIn):
def test_parse_recipe_url(url: CreateRecipeByURL):
return scrape_url(url.url)
@user_router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url(
background_tasks: BackgroundTasks,
url: RecipeURLIn,
url: CreateRecipeByURL,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):

View File

@ -2,7 +2,7 @@ from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe.units_and_foods import CreateIngredientFood, IngredientFood
from mealie.schema.recipe import CreateIngredientFood, IngredientFood
router = UserAPIRouter()

View File

@ -2,7 +2,7 @@ from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe.units_and_foods import CreateIngredientUnit, IngredientUnit
from mealie.schema.recipe import CreateIngredientUnit, IngredientUnit
router = UserAPIRouter()

View File

@ -4,7 +4,7 @@ from fastapi_camelcase import CamelModel
from pydantic import validator
from slugify import slugify
from ..recipe.category import CategoryBase, RecipeCategoryResponse
from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse
class SiteSettings(CamelModel):

View File

@ -1,4 +1,6 @@
from .category import *
from .comments import *
from .helpers import *
from .recipe import *
from .recipe_category import *
from .recipe_comments import *
from .recipe_image_types import *
from .recipe_ingredient import *
from .request_helpers import *

View File

@ -1,91 +1,34 @@
import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Optional
from fastapi_camelcase import CamelModel
from mealie.core.config import app_dirs, settings
from mealie.core.config import app_dirs
from mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, Field, validator
from pydantic.utils import GetterDict
from slugify import slugify
from .comments import CommentOut
from .units_and_foods import IngredientFood, IngredientUnit
from .recipe_asset import RecipeAsset
from .recipe_comments import CommentOut
from .recipe_ingredient import RecipeIngredient
from .recipe_notes import RecipeNote
from .recipe_nutrition import Nutrition
from .recipe_settings import RecipeSettings
from .recipe_step import RecipeStep
class CreateRecipeByURL(BaseModel):
url: str
class Config:
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
class CreateRecipe(CamelModel):
name: str
class RecipeImageTypes(str, Enum):
original = "original.webp"
min = "min-original.webp"
tiny = "tiny-original.webp"
class RecipeSettings(CamelModel):
public: bool = settings.RECIPE_PUBLIC
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
show_assets: bool = settings.RECIPE_SHOW_ASSETS
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
class Config:
orm_mode = True
class RecipeNote(BaseModel):
title: str
text: str
class Config:
orm_mode = True
class RecipeStep(CamelModel):
title: Optional[str] = ""
text: str
class Config:
orm_mode = True
class RecipeAsset(CamelModel):
name: str
icon: str
file_name: Optional[str]
class Config:
orm_mode = True
class Nutrition(CamelModel):
calories: Optional[str]
fat_content: Optional[str]
protein_content: Optional[str]
carbohydrate_content: Optional[str]
fiber_content: Optional[str]
sodium_content: Optional[str]
sugar_content: Optional[str]
class Config:
orm_mode = True
class RecipeIngredient(CamelModel):
title: Optional[str]
note: Optional[str]
unit: Optional[IngredientUnit]
food: Optional[IngredientFood]
disable_amount: bool = True
quantity: int = 1
class Config:
orm_mode = True
class RecipeSummary(CamelModel):
id: Optional[int]
name: Optional[str]
@ -196,15 +139,10 @@ class Recipe(RecipeSummary):
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
if not values["name"]:
if not values.get("name"):
return slug
name: str = values["name"]
calc_slug: str = slugify(name)
if slug != calc_slug:
slug = calc_slug
return slug
return slugify(values["name"])
@validator("recipe_ingredient", always=True, pre=True)
def validate_ingredients(recipe_ingredient, values):
@ -215,28 +153,3 @@ class Recipe(RecipeSummary):
return [RecipeIngredient(note=x) for x in recipe_ingredient]
return recipe_ingredient
class AllRecipeRequest(BaseModel):
properties: list[str]
limit: Optional[int]
class Config:
schema_extra = {
"example": {
"properties": ["name", "slug", "image"],
"limit": 100,
}
}
class RecipeURLIn(BaseModel):
url: str
class Config:
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
class SlugResponse(BaseModel):
class Config:
schema_extra = {"example": "adult-mac-and-cheese"}

View File

@ -0,0 +1,12 @@
from typing import Optional
from fastapi_camelcase import CamelModel
class RecipeAsset(CamelModel):
name: str
icon: str
file_name: Optional[str]
class Config:
orm_mode = True

View File

@ -0,0 +1,44 @@
from datetime import datetime
from typing import Optional
from fastapi_camelcase import CamelModel
from pydantic.utils import GetterDict
class UserBase(CamelModel):
id: int
username: Optional[str]
admin: bool
class Config:
orm_mode = True
class CreateComment(CamelModel):
text: str
class SaveComment(CreateComment):
recipe_slug: str
user: int
class Config:
orm_mode = True
class CommentOut(CreateComment):
id: int
uuid: str
recipe_slug: str
date_added: datetime
user: UserBase
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm):
return {
**GetterDict(name_orm),
"recipe_slug": name_orm.recipe.slug,
}

View File

@ -0,0 +1,7 @@
from enum import Enum
class RecipeImageTypes(str, Enum):
original = "original.webp"
min = "min-original.webp"
tiny = "tiny-original.webp"

View File

@ -0,0 +1,39 @@
from typing import Optional, Union
from fastapi_camelcase import CamelModel
class CreateIngredientFood(CamelModel):
name: str
description: str = ""
class CreateIngredientUnit(CreateIngredientFood):
fraction: bool = True
abbreviation: str = ""
class IngredientFood(CreateIngredientFood):
id: int
class Config:
orm_mode = True
class IngredientUnit(CreateIngredientUnit):
id: int
class Config:
orm_mode = True
class RecipeIngredient(CamelModel):
title: Optional[str]
note: Optional[str]
unit: Optional[Union[CreateIngredientUnit, IngredientUnit]]
food: Optional[Union[CreateIngredientFood, IngredientFood]]
disable_amount: bool = True
quantity: float = 1
class Config:
orm_mode = True

View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
class RecipeNote(BaseModel):
title: str
text: str
class Config:
orm_mode = True

View File

@ -0,0 +1,16 @@
from typing import Optional
from fastapi_camelcase import CamelModel
class Nutrition(CamelModel):
calories: Optional[str]
fat_content: Optional[str]
protein_content: Optional[str]
carbohydrate_content: Optional[str]
fiber_content: Optional[str]
sodium_content: Optional[str]
sugar_content: Optional[str]
class Config:
orm_mode = True

View File

@ -0,0 +1,14 @@
from fastapi_camelcase import CamelModel
from mealie.core.config import settings
class RecipeSettings(CamelModel):
public: bool = settings.RECIPE_PUBLIC
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
show_assets: bool = settings.RECIPE_SHOW_ASSETS
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
class Config:
orm_mode = True

View File

@ -0,0 +1,11 @@
from typing import Optional
from fastapi_camelcase import CamelModel
class RecipeStep(CamelModel):
title: Optional[str] = ""
text: str
class Config:
orm_mode = True

View File

@ -0,0 +1,13 @@
from fastapi_camelcase import CamelModel
from pydantic import BaseModel
# TODO: Should these exist?!?!?!?!?
class RecipeSlug(CamelModel):
slug: str
class SlugResponse(BaseModel):
class Config:
schema_extra = {"example": "adult-mac-and-cheese"}

View File

@ -0,0 +1,4 @@
from .category import *
from .comments import *
from .helpers import *
from .recipe import *

View File

@ -0,0 +1,48 @@
from typing import List, Optional
from fastapi_camelcase import CamelModel
from pydantic.utils import GetterDict
class CategoryIn(CamelModel):
name: str
class CategoryBase(CategoryIn):
id: int
slug: str
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm):
return {
**GetterDict(name_orm),
"total_recipes": len(name_orm.recipes),
}
class RecipeCategoryResponse(CategoryBase):
recipes: Optional[List["Recipe"]]
class Config:
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
class TagIn(CategoryIn):
pass
class TagBase(CategoryBase):
pass
class RecipeTagResponse(RecipeCategoryResponse):
pass
from .recipe import Recipe
RecipeCategoryResponse.update_forward_refs()
RecipeTagResponse.update_forward_refs()

View File

@ -0,0 +1,242 @@
import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Optional
from fastapi_camelcase import CamelModel
from mealie.core.config import app_dirs, settings
from mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, Field, validator
from pydantic.utils import GetterDict
from slugify import slugify
from .comments import CommentOut
from .units_and_foods import IngredientFood, IngredientUnit
class CreateRecipe(CamelModel):
name: str
class RecipeImageTypes(str, Enum):
original = "original.webp"
min = "min-original.webp"
tiny = "tiny-original.webp"
class RecipeSettings(CamelModel):
public: bool = settings.RECIPE_PUBLIC
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
show_assets: bool = settings.RECIPE_SHOW_ASSETS
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
class Config:
orm_mode = True
class RecipeNote(BaseModel):
title: str
text: str
class Config:
orm_mode = True
class RecipeStep(CamelModel):
title: Optional[str] = ""
text: str
class Config:
orm_mode = True
class RecipeAsset(CamelModel):
name: str
icon: str
file_name: Optional[str]
class Config:
orm_mode = True
class Nutrition(CamelModel):
calories: Optional[str]
fat_content: Optional[str]
protein_content: Optional[str]
carbohydrate_content: Optional[str]
fiber_content: Optional[str]
sodium_content: Optional[str]
sugar_content: Optional[str]
class Config:
orm_mode = True
class RecipeIngredient(CamelModel):
title: Optional[str]
note: Optional[str]
unit: Optional[IngredientUnit]
food: Optional[IngredientFood]
disable_amount: bool = True
quantity: int = 1
class Config:
orm_mode = True
class RecipeSummary(CamelModel):
id: Optional[int]
name: Optional[str]
slug: str = ""
image: Optional[Any]
description: Optional[str]
recipe_category: Optional[list[str]] = []
tags: Optional[list[str]] = []
rating: Optional[int]
date_added: Optional[datetime.date]
date_updated: Optional[datetime.datetime]
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm: RecipeModel):
return {
**GetterDict(name_orm),
"recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags],
}
class Recipe(RecipeSummary):
recipe_yield: Optional[str]
recipe_ingredient: Optional[list[RecipeIngredient]] = []
recipe_instructions: Optional[list[RecipeStep]] = []
nutrition: Optional[Nutrition]
tools: Optional[list[str]] = []
total_time: Optional[str] = None
prep_time: Optional[str] = None
perform_time: Optional[str] = None
# Mealie Specific
settings: Optional[RecipeSettings] = RecipeSettings()
assets: Optional[list[RecipeAsset]] = []
notes: Optional[list[RecipeNote]] = []
org_url: Optional[str] = Field(None, alias="orgURL")
extras: Optional[dict] = {}
comments: Optional[list[CommentOut]] = []
@staticmethod
def directory_from_slug(slug) -> Path:
return app_dirs.RECIPE_DATA_DIR.joinpath(slug)
@property
def directory(self) -> Path:
dir = app_dirs.RECIPE_DATA_DIR.joinpath(self.slug)
dir.mkdir(exist_ok=True, parents=True)
return dir
@property
def asset_dir(self) -> Path:
dir = self.directory.joinpath("assets")
dir.mkdir(exist_ok=True, parents=True)
return dir
@property
def image_dir(self) -> Path:
dir = self.directory.joinpath("images")
dir.mkdir(exist_ok=True, parents=True)
return dir
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm: RecipeModel):
return {
**GetterDict(name_orm),
# "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
"recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags],
"tools": [x.tool for x in name_orm.tools],
"extras": {x.key_name: x.value for x in name_orm.extras},
}
schema_extra = {
"example": {
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"recipe_yield": "4 Servings",
"recipe_ingredient": [
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
],
"recipe_instructions": [
{
"text": "Season chicken with salt and pepper.",
},
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"recipe_category": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,
"extras": {"message": "Don't forget to defrost the chicken!"},
}
}
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
if not values["name"]:
return slug
name: str = values["name"]
calc_slug: str = slugify(name)
if slug != calc_slug:
slug = calc_slug
return slug
@validator("recipe_ingredient", always=True, pre=True)
def validate_ingredients(recipe_ingredient, values):
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
return recipe_ingredient
if all(isinstance(elem, str) for elem in recipe_ingredient):
return [RecipeIngredient(note=x) for x in recipe_ingredient]
return recipe_ingredient
class AllRecipeRequest(BaseModel):
properties: list[str]
limit: Optional[int]
class Config:
schema_extra = {
"example": {
"properties": ["name", "slug", "image"],
"limit": 100,
}
}
class RecipeURLIn(BaseModel):
url: str
class Config:
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
class SlugResponse(BaseModel):
class Config:
schema_extra = {"example": "adult-mac-and-cheese"}