refactor(backend): ♻️ Update tag naming and reorganized some routes. Still WIP

This commit is contained in:
hay-kot 2021-08-21 21:49:14 -08:00
parent 19fb6af050
commit 62836e5381
23 changed files with 204 additions and 219 deletions

View File

@ -8,8 +8,6 @@ from mealie.routes import backup_routes, debug_routes, migration_routes, router,
from mealie.routes.about import about_router
from mealie.routes.mealplans import meal_plan_router
from mealie.routes.media import media_router
from mealie.routes.recipe import recipe_router
from mealie.routes.shopping_list import shopping_list_router
from mealie.routes.site_settings import settings_router
from mealie.services.events import create_general_event
from mealie.services.recipe.all_recipes import subscripte_to_recipe_events
@ -34,9 +32,7 @@ def start_scheduler():
def api_routers():
# Authentication
app.include_router(router)
app.include_router(shopping_list_router)
# Recipes
app.include_router(recipe_router)
app.include_router(media_router)
app.include_router(about_router)
# Meal Routes

View File

@ -1,9 +1,15 @@
from fastapi import APIRouter
from . import auth, groups, users
from . import auth, categories, groups, recipe, shopping_lists, tags, unit_and_foods, users
router = APIRouter(prefix="/api")
router.include_router(auth.router)
router.include_router(users.router)
router.include_router(groups.router)
router.include_router(recipe.router)
router.include_router(unit_and_foods.router)
router.include_router(categories.router)
router.include_router(tags.router)
router.include_router(shopping_lists.router)

View File

@ -1,8 +1,9 @@
from fastapi import APIRouter
from . import defaults, events
from . import defaults, events, notifications
about_router = APIRouter(prefix="/api/about")
about_router.include_router(events.router)
about_router.include_router(defaults.router)
about_router.include_router(events.router, tags=["Events: CRUD"])
about_router.include_router(notifications.router, tags=["Events: Notifications"])
about_router.include_router(defaults.router, tags=["Recipe: Defaults"])

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter
from mealie.schema.recipe import RecipeSettings
router = APIRouter(prefix="/recipes", tags=["About Recipes"])
router = APIRouter(prefix="/recipes")
@router.get("/defaults")

View File

@ -1,15 +1,12 @@
from http.client import HTTPException
from fastapi import Depends, status
from fastapi import Depends
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventNotificationIn, EventNotificationOut, EventsOut, TestEvent
from mealie.services.events import test_notification
from mealie.schema.events import EventsOut
from sqlalchemy.orm.session import Session
router = AdminAPIRouter(prefix="/events", tags=["App Events"])
router = AdminAPIRouter(prefix="/events")
logger = get_logger()
@ -32,52 +29,3 @@ async def delete_events(session: Session = Depends(generate_session)):
async def delete_event(id: int, session: Session = Depends(generate_session)):
""" Delete event from the Database """
return db.events.delete(session, id)
@router.post("/notifications")
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
return db.event_notifications.create(session, event_data)
@router.post("/notifications/test")
async def test_notification_route(
test_data: TestEvent,
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
if test_data.id:
event_obj: EventNotificationIn = db.event_notifications.get(session, test_data.id)
test_data.test_url = event_obj.notification_url
try:
test_notification(test_data.test_url)
except Exception as e:
logger.error(e)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(session: Session = Depends(generate_session)):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(id: int, session: Session = Depends(generate_session)):
""" Update event_notification in the Database """
# not yet implemented
raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
@router.delete("/notifications/{id}")
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)

View File

@ -0,0 +1,63 @@
from http.client import HTTPException
from fastapi import Depends, status
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent
from mealie.services.events import test_notification
from sqlalchemy.orm.session import Session
router = AdminAPIRouter()
logger = get_logger()
@router.post("/notifications")
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
return db.event_notifications.create(session, event_data)
@router.post("/notifications/test")
async def test_notification_route(
test_data: TestEvent,
session: Session = Depends(generate_session),
):
""" Create event_notification in the Database """
if test_data.id:
event_obj: EventNotificationIn = db.event_notifications.get(session, test_data.id)
test_data.test_url = event_obj.notification_url
try:
test_notification(test_data.test_url)
except Exception as e:
logger.error(e)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(session: Session = Depends(generate_session)):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(id: int, session: Session = Depends(generate_session)):
""" Update event_notification in the Database """
# not yet implemented
raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
@router.delete("/notifications/{id}")
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)

View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from . import categories
prefix = "/categories"
router = APIRouter()
router.include_router(categories.public_router, prefix=prefix, tags=["Categories: CRUD"])
router.include_router(categories.user_router, prefix=prefix, tags=["Categories: CRUD"])
router.include_router(categories.admin_router, prefix=prefix, tags=["Categories: CRUD"])

View File

@ -6,9 +6,9 @@ from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
from sqlalchemy.orm.session import Session
public_router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
user_router = UserAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
admin_router = AdminAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
public_router = APIRouter()
user_router = UserAPIRouter()
admin_router = AdminAPIRouter()
@public_router.get("")

View File

@ -7,8 +7,8 @@ from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.services.events import create_group_event
from sqlalchemy.orm.session import Session
admin_router = AdminAPIRouter(prefix="/api/groups", tags=["Groups: CRUD"])
user_router = UserAPIRouter(prefix="/api/groups", tags=["Groups: CRUD"])
admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
user_router = UserAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
@user_router.get("/self", response_model=GroupInDB)

View File

@ -2,6 +2,6 @@ from fastapi import APIRouter
from . import recipe
media_router = APIRouter(prefix="/api/media", tags=["Site Media"])
media_router = APIRouter(prefix="/api/media", tags=["Recipe: Images and Assets"])
media_router.include_router(recipe.router)

View File

@ -1,15 +1,12 @@
from fastapi import APIRouter
from mealie.routes.recipe import all_recipe_routes, category_routes, comments, recipe_crud_routes, tag_routes
from mealie.routes.recipe import all_recipe_routes, comments, image_and_assets, recipe_crud_routes
recipe_router = APIRouter()
prefix = "/recipes"
recipe_router.include_router(all_recipe_routes.router)
recipe_router.include_router(recipe_crud_routes.public_router)
recipe_router.include_router(recipe_crud_routes.user_router)
recipe_router.include_router(category_routes.public_router)
recipe_router.include_router(category_routes.user_router)
recipe_router.include_router(category_routes.admin_router)
recipe_router.include_router(tag_routes.admin_router)
recipe_router.include_router(tag_routes.user_router)
recipe_router.include_router(tag_routes.public_router)
recipe_router.include_router(comments.router)
router = APIRouter()
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Recipe: CRUD"])
router.include_router(recipe_crud_routes.public_router, prefix=prefix, tags=["Recipe: CRUD"])
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])

View File

@ -4,10 +4,9 @@ from mealie.db.db_setup import generate_session
from mealie.routes.deps import is_logged_in
from mealie.schema.recipe import RecipeSummary
from mealie.services.recipe.all_recipes import get_all_recipes_public, get_all_recipes_user
from slugify import slugify
from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Query All Recipes"])
router = APIRouter()
@router.get("/api/recipes")
@ -39,23 +38,3 @@ async def get_untagged_recipes(count: bool = False, session: Session = Depends(g
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
@router.post("/api/recipes/category", deprecated=True)
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """
# ! This should be refactored into a single database call, but I couldn't figure it out
in_category = [db.categories.get(session, slugify(cat), limit=1) for cat in categories]
in_category = [cat.recipes for cat in in_category if cat]
in_category = [item for sublist in in_category for item in sublist]
return in_category
@router.post("/api/recipes/tag", deprecated=True)
async def filter_by_tags(tags: list, session: Session = Depends(generate_session)):
""" pass a list of tags and get a list of recipes associated with those tags"""
# ! This should be refactored into a single database call, but I couldn't figure it out
in_tags = [db.tags.get(session, slugify(tag), limit=1) for tag in tags]
in_tags = [tag.recipes for tag in in_tags]
in_tags = [item for sublist in in_tags for item in sublist]
return in_tags

View File

@ -9,10 +9,10 @@ from mealie.schema.recipe import CommentIn, CommentOut, CommentSaveToDB
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = UserAPIRouter(prefix="/api", tags=["Recipe Comments"])
router = UserAPIRouter()
@router.post("/recipes/{slug}/comments")
@router.post("/{slug}/comments")
async def create_comment(
slug: str,
new_comment: CommentIn,
@ -25,7 +25,7 @@ async def create_comment(
return db.comments.create(session, new_comment)
@router.put("/recipes/{slug}/comments/{id}")
@router.put("/{slug}/comments/{id}")
async def update_comment(
id: int,
new_comment: CommentIn,
@ -41,7 +41,7 @@ async def update_comment(
return db.comments.update(session, id, new_comment)
@router.delete("/recipes/{slug}/comments/{id}")
@router.delete("/{slug}/comments/{id}")
async def delete_comment(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):

View File

@ -0,0 +1,63 @@
from shutil import copyfileobj
from fastapi import Depends, File, Form, HTTPException, status
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.services.image.image import scrape_image, write_image
from slugify import slugify
from sqlalchemy.orm.session import Session
user_router = UserAPIRouter()
@user_router.post("/{recipe_slug}/image")
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
):
""" Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug)
@user_router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
):
""" Removes an existing image and replaces it with the incoming file. """
write_image(recipe_slug, image, extension)
new_version = db.recipes.update_image(session, recipe_slug, extension)
return {"image": new_version}
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
file: UploadFile = File(...),
session: Session = Depends(generate_session),
):
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
with dest.open("wb") as buffer:
copyfileobj(file.file, buffer)
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
recipe: Recipe = db.recipes.get(session, recipe_slug)
recipe.assets.append(asset_in)
db.recipes.update(session, recipe_slug, recipe.dict())
return asset_in

View File

@ -1,9 +1,8 @@
import json
import shutil
from shutil import copyfileobj
from zipfile import ZipFile
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, status
from fastapi.datastructures import UploadFile
from mealie.core.config import settings
from mealie.core.root_logger import get_logger
@ -11,20 +10,19 @@ 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, RecipeAsset, RecipeImageTypes, RecipeURLIn
from mealie.schema.recipe import Recipe, RecipeImageTypes, RecipeURLIn
from mealie.schema.recipe.recipe import CreateRecipe
from mealie.schema.user import UserInDB
from mealie.services.events import create_recipe_event
from mealie.services.image.image import scrape_image, write_image
from mealie.services.image.image import write_image
from mealie.services.recipe.media import check_assets, delete_assets
from mealie.services.scraper.scraper import create_from_url
from scrape_schema_recipe import scrape_url
from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
user_router = UserAPIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
public_router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
user_router = UserAPIRouter()
public_router = APIRouter()
logger = get_logger()
@ -52,27 +50,6 @@ def create_from_name(
return recipe.slug
@user_router.post("/create", status_code=201, response_model=str)
def create_from_json(
background_tasks: BackgroundTasks,
data: Recipe,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
) -> str:
""" Takes in a JSON string and loads data into the database as a new entry"""
recipe: Recipe = db.recipes.create(session, data.dict())
background_tasks.add_task(
create_recipe_event,
"Recipe Created (URL)",
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
session=session,
attachment=recipe.image_dir.joinpath("min-original.webp"),
)
return recipe.slug
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: RecipeURLIn):
return scrape_url(url.url)
@ -217,53 +194,3 @@ def delete_recipe(
)
return recipe
@user_router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
):
""" Removes an existing image and replaces it with the incoming file. """
write_image(recipe_slug, image, extension)
new_version = db.recipes.update_image(session, recipe_slug, extension)
return {"image": new_version}
@user_router.post("/{recipe_slug}/image")
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
):
""" Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug)
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
file: UploadFile = File(...),
session: Session = Depends(generate_session),
):
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
with dest.open("wb") as buffer:
copyfileobj(file.file, buffer)
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
recipe: Recipe = db.recipes.get(session, recipe_slug)
recipe.assets.append(asset_in)
db.recipes.update(session, recipe_slug, recipe.dict())
return asset_in

View File

@ -7,10 +7,10 @@ from mealie.schema.meal_plan import ShoppingListIn, ShoppingListOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
shopping_list_router = UserAPIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
router = UserAPIRouter(prefix="/shopping-lists", tags=["Shopping Lists: CRUD"])
@shopping_list_router.post("", response_model=ShoppingListOut)
@router.post("", response_model=ShoppingListOut)
async def create_shopping_list(
list_in: ShoppingListIn,
current_user: UserInDB = Depends(get_current_user),
@ -23,19 +23,19 @@ async def create_shopping_list(
return db.shopping_lists.create(session, list_in)
@shopping_list_router.get("/{id}", response_model=ShoppingListOut)
@router.get("/{id}", response_model=ShoppingListOut)
async def get_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Get Shopping List from the Database """
return db.shopping_lists.get(session, id)
@shopping_list_router.put("/{id}", response_model=ShoppingListOut)
@router.put("/{id}", response_model=ShoppingListOut)
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """
return db.shopping_lists.update(session, id, new_data)
@shopping_list_router.delete("/{id}")
@router.delete("/{id}")
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """
return db.shopping_lists.delete(session, id)

View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from . import tags
prefix = "/tags"
router = APIRouter()
router.include_router(tags.public_router, prefix=prefix, tags=["Tags: CRUD"])
router.include_router(tags.user_router, prefix=prefix, tags=["Tags: CRUD"])
router.include_router(tags.admin_router, prefix=prefix, tags=["Tags: CRUD"])

View File

@ -6,9 +6,9 @@ from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import RecipeTagResponse, TagIn
from sqlalchemy.orm.session import Session
public_router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
user_router = UserAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
admin_router = AdminAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
public_router = APIRouter()
user_router = UserAPIRouter()
admin_router = AdminAPIRouter()
@public_router.get("")

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter
from . import food_routes, unit_routes
units_and_foods_router = APIRouter(tags=["Food and Units"])
router = APIRouter()
units_and_foods_router.include_router(food_routes.router)
units_and_foods_router.include_router(unit_routes.router)
router.include_router(food_routes.router, prefix="/foods", tags=["Recipes: Foods"])
router.include_router(unit_routes.router, prefix="/units", tags=["Recipes: Units"])

View File

@ -1,7 +1,7 @@
from mealie.routes.routers import UserAPIRouter
from mealie.core.root_logger import get_logger
from mealie.routes.routers import UserAPIRouter
router = UserAPIRouter(prefix="/api/foods")
router = UserAPIRouter()
logger = get_logger()

View File

@ -1,7 +1,7 @@
from mealie.routes.routers import UserAPIRouter
from mealie.core.root_logger import get_logger
from mealie.routes.routers import UserAPIRouter
router = UserAPIRouter(prefix="/api/units")
router = UserAPIRouter()
logger = get_logger()

View File

@ -6,8 +6,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_admin_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import (SignUpIn, SignUpOut, SignUpToken, UserIn,
UserInDB)
from mealie.schema.user import SignUpIn, SignUpOut, SignUpToken, UserIn, UserInDB
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session

View File

@ -19,22 +19,6 @@ def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_dat
assert json.loads(response.text) == recipe_data.expected_slug
def test_create_by_json(api_client: TestClient, api_routes: AppRoutes, user_token, raw_recipe):
recipe_url = api_routes.recipes_recipe_slug("banana-bread")
api_client.delete(recipe_url, headers=user_token)
response = api_client.post(api_routes.recipes_create, json=raw_recipe, headers=user_token)
assert response.status_code == 201
assert json.loads(response.text) == "banana-bread"
def test_create_no_image(api_client: TestClient, api_routes: AppRoutes, user_token, raw_recipe_no_image):
response = api_client.post(api_routes.recipes_create, json=raw_recipe_no_image, headers=user_token)
assert response.status_code == 201
assert json.loads(response.text) == "banana-bread-no-image"
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)