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 sqlalchemy.orm.session import Session
from ..data_access_layer import DatabaseAccessLayer 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.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter 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 mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -15,20 +15,20 @@ router = UserAPIRouter()
@router.post("/{slug}/comments") @router.post("/{slug}/comments")
async def create_comment( async def create_comment(
slug: str, slug: str,
new_comment: CommentIn, new_comment: CreateComment,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
): ):
""" Create comment in the Database """ """ 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) return db.comments.create(session, new_comment)
@router.put("/{slug}/comments/{id}") @router.put("/{slug}/comments/{id}")
async def update_comment( async def update_comment(
id: int, id: int,
new_comment: CommentIn, new_comment: CreateComment,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user), 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.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter 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 mealie.services.image.image import scrape_image, write_image
from slugify import slugify from slugify import slugify
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -16,7 +16,7 @@ user_router = UserAPIRouter()
@user_router.post("/{recipe_slug}/image") @user_router.post("/{recipe_slug}/image")
def scrape_image_url( def scrape_image_url(
recipe_slug: str, recipe_slug: str,
url: RecipeURLIn, url: CreateRecipeByURL,
): ):
""" Removes an existing image and replaces it with the incoming file. """ """ 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.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in, temporary_zip_path from mealie.routes.deps import get_current_user, is_logged_in, temporary_zip_path
from mealie.routes.routers import UserAPIRouter 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.recipe.recipe import CreateRecipe
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from mealie.services.events import create_recipe_event from mealie.services.events import create_recipe_event
@ -51,14 +51,14 @@ def create_from_name(
@user_router.post("/test-scrape-url") @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) return scrape_url(url.url)
@user_router.post("/create-url", status_code=201, response_model=str) @user_router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url( def parse_recipe_url(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
url: RecipeURLIn, url: CreateRecipeByURL,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user), 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.database import db
from mealie.db.db_setup import Session, generate_session from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter 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() router = UserAPIRouter()

View File

@ -2,7 +2,7 @@ from fastapi import Depends, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import Session, generate_session from mealie.db.db_setup import Session, generate_session
from mealie.routes.routers import UserAPIRouter 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() router = UserAPIRouter()

View File

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

View File

@ -1,4 +1,6 @@
from .category import *
from .comments import *
from .helpers import *
from .recipe 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 import datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from fastapi_camelcase import CamelModel 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 mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from pydantic.utils import GetterDict from pydantic.utils import GetterDict
from slugify import slugify from slugify import slugify
from .comments import CommentOut from .recipe_asset import RecipeAsset
from .units_and_foods import IngredientFood, IngredientUnit 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): class CreateRecipe(CamelModel):
name: str 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): class RecipeSummary(CamelModel):
id: Optional[int] id: Optional[int]
name: Optional[str] name: Optional[str]
@ -196,15 +139,10 @@ class Recipe(RecipeSummary):
@validator("slug", always=True, pre=True) @validator("slug", always=True, pre=True)
def validate_slug(slug: str, values): def validate_slug(slug: str, values):
if not values["name"]: if not values.get("name"):
return slug return slug
name: str = values["name"]
calc_slug: str = slugify(name)
if slug != calc_slug: return slugify(values["name"])
slug = calc_slug
return slug
@validator("recipe_ingredient", always=True, pre=True) @validator("recipe_ingredient", always=True, pre=True)
def validate_ingredients(recipe_ingredient, values): def validate_ingredients(recipe_ingredient, values):
@ -215,28 +153,3 @@ class Recipe(RecipeSummary):
return [RecipeIngredient(note=x) for x in recipe_ingredient] return [RecipeIngredient(note=x) for x in recipe_ingredient]
return 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"}