fix categories database errors

This commit is contained in:
hayden 2021-01-31 19:10:21 -09:00
commit b6111afe69
9 changed files with 132 additions and 78 deletions

View file

@ -1,3 +1,4 @@
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from db.db_base import BaseDocument from db.db_base import BaseDocument
@ -28,13 +29,13 @@ class _Recipes(BaseDocument):
class _Categories(BaseDocument): class _Categories(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "name" self.primary_key = "slug"
self.sql_model = Category self.sql_model = Category
class _Tags(BaseDocument): class _Tags(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "name" self.primary_key = "slug"
self.sql_model = Tag self.sql_model = Tag

View file

@ -16,14 +16,43 @@ class BaseDocument:
def get_all( def get_all(
self, session: Session, limit: int = None, order_by: str = None self, session: Session, limit: int = None, order_by: str = None
) -> List[dict]: ) -> List[dict]:
list = [x.dict() for x in session.query(self.sql_model).all()] list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
if limit == 1: if limit == 1:
return list[0] return list[0]
return list return list
def get_all_primary_keys(self, session: Session): def get_all_limit_columns(
self, session: Session, fields: List[str], limit: int = None
) -> list[SqlAlchemyBase]:
"""Queries the database for the selected model. Restricts return responses to the
keys specified under "fields"
Args: \n
session (Session): Database Session Object
fields (List[str]): List of column names to query
limit (int): A limit of values to return
Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects
"""
results = (
session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
)
return results
def get_all_primary_keys(self, session: Session) -> List[str]:
"""Queries the database of the selected model and returns a list
of all primary_key values
Args: \n
session (Session): Database Session object
Returns:
list[str]:
"""
results = session.query(self.sql_model).options( results = session.query(self.sql_model).options(
load_only(str(self.primary_key)) load_only(str(self.primary_key))
) )
@ -36,7 +65,7 @@ class BaseDocument:
"""Query the sql database for one item an return the sql alchemy model """Query the sql database for one item an return the sql alchemy model
object. If no match key is provided the primary_key attribute will be used. object. If no match key is provided the primary_key attribute will be used.
Args: Args: \n
match_value (str): The value to use in the query match_value (str): The value to use in the query
match_key (str, optional): the key/property to match against. Defaults to None. match_key (str, optional): the key/property to match against. Defaults to None.
@ -80,7 +109,7 @@ class BaseDocument:
def save_new(self, session: Session, document: dict) -> dict: def save_new(self, session: Session, document: dict) -> dict:
"""Creates a new database entry for the given SQL Alchemy Model. """Creates a new database entry for the given SQL Alchemy Model.
Args: Args: \n
session (Session): A Database Session session (Session): A Database Session
document (dict): A python dictionary representing the data structure document (dict): A python dictionary representing the data structure
@ -97,7 +126,7 @@ class BaseDocument:
def update(self, session: Session, match_value: str, new_data: str) -> dict: def update(self, session: Session, match_value: str, new_data: str) -> dict:
"""Update a database entry. """Update a database entry.
Args: Args: \n
session (Session): Database Session session (Session): Database Session
match_value (str): Match "key" match_value (str): Match "key"
new_data (str): Match "value" new_data (str): Match "value"
@ -121,5 +150,4 @@ class BaseDocument:
) )
session.delete(result) session.delete(result)
session.commit() session.commit()

View file

@ -5,6 +5,7 @@ from typing import List
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.orm as orm import sqlalchemy.orm as orm
from db.sql.model_base import BaseMixins, SqlAlchemyBase from db.sql.model_base import BaseMixins, SqlAlchemyBase
from slugify import slugify
from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.orderinglist import ordering_list
from utils.logger import logger from utils.logger import logger
@ -28,14 +29,14 @@ recipes2categories = sa.Table(
"recipes2categories", "recipes2categories",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")), sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("category_name", sa.String, sa.ForeignKey("categories.name")), sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
) )
recipes2tags = sa.Table( recipes2tags = sa.Table(
"recipes2tags", "recipes2tags",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")), sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")), sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
) )
@ -43,36 +44,46 @@ class Category(SqlAlchemyBase):
__tablename__ = "categories" __tablename__ = "categories"
id = sa.Column(sa.Integer, primary_key=True) id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True) name = sa.Column(sa.String, index=True)
slug = sa.Column(sa.String, index=True, unique=True)
recipes = orm.relationship( recipes = orm.relationship(
"RecipeModel", secondary=recipes2categories, back_populates="categories" "RecipeModel", secondary=recipes2categories, back_populates="categories"
) )
def __init__(self, name) -> None: def __init__(self, name) -> None:
self.name = name self.name = name.strip()
self.slug = slugify(name)
@classmethod
def create_if_not_exist(cls, session, name: str):
@staticmethod
def create_if_not_exist(session, name: str = None):
try: try:
result = session.query(Category).filter_by(**{"name": name}).one() result = session.query(Category).filter(Category.name == name.strip()).one()
logger.info("Category Exists, Associating Recipe") if result:
logger.info("Category exists, associating recipe")
return result return result
else:
logger.info("Category doesn't exists, creating tag")
return Category(name=name)
except: except:
logger.info("Category doesn't exists, creating category") logger.info("Category doesn't exists, creating category")
return cls(name=name) return Category(name=name)
def to_str(self): def to_str(self):
return self.name return self.name
def dict(self): def dict(self):
return {"id": self.id, "name": self.name, "recipes": [x.dict() for x in self.recipes]} return {
"id": self.id,
"slug": self.slug,
"name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
class Tag(SqlAlchemyBase): class Tag(SqlAlchemyBase):
__tablename__ = "tags" __tablename__ = "tags"
id = sa.Column(sa.Integer, primary_key=True) id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True) name = sa.Column(sa.String, index=True)
slug = sa.Column(sa.String, index=True, unique=True)
recipes = orm.relationship( recipes = orm.relationship(
"RecipeModel", secondary=recipes2tags, back_populates="tags" "RecipeModel", secondary=recipes2tags, back_populates="tags"
) )
@ -81,22 +92,32 @@ class Tag(SqlAlchemyBase):
return self.name return self.name
def __init__(self, name) -> None: def __init__(self, name) -> None:
self.name = name self.name = name.strip()
self.slug = slugify(self.name)
def dict(self): def dict(self):
return {"id": self.id, "name": self.name, "recipes": [x.dict() for x in self.recipes]} return {
"id": self.id,
@classmethod "slug": self.slug,
def create_if_not_exist(cls, session, name: str): "name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
@staticmethod
def create_if_not_exist(session, name: str = None):
try: try:
result = session.query(Tag).filter_by(**{"name": name}).one() result = session.query(Tag).filter(Tag.name == name.strip()).first()
logger.info("Tag Exists, Associating Recipe")
if result:
logger.info("Tag exists, associating recipe")
return result return result
else:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)
except: except:
logger.info("Tag doesn't exists, creating tag") logger.info("Tag doesn't exists, creating tag")
return cls(name=name) return Tag(name=name)
class Note(SqlAlchemyBase): class Note(SqlAlchemyBase):
@ -213,7 +234,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
] ]
self.recipeInstructions = [ self.recipeInstructions = [
RecipeInstruction(text=instruc.get("text"), type=instruc.get("text")) RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
for instruc in recipeInstructions for instruc in recipeInstructions
] ]
self.totalTime = totalTime self.totalTime = totalTime
@ -222,11 +243,12 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
# Mealie Specific # Mealie Specific
self.slug = slug self.slug = slug
self.categories = [ self.categories = [
(Category.create_if_not_exist(session, cat)) for cat in categories Category.create_if_not_exist(session=session, name=cat)
for cat in categories
] ]
self.tags = [Tag.create_if_not_exist(session, name=tag) for tag in tags]
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
self.dateAdded = dateAdded self.dateAdded = dateAdded
self.notes = [Note(**note) for note in notes] self.notes = [Note(**note) for note in notes]

View file

@ -7,6 +7,7 @@ from services.recipe_services import Recipe
class RecipeCategoryResponse(BaseModel): class RecipeCategoryResponse(BaseModel):
id: int id: int
name: str name: str
slug: str
recipes: List[Recipe] recipes: List[Recipe]
class Config: class Config:

View file

@ -4,8 +4,8 @@ import pydantic
from pydantic.main import BaseModel from pydantic.main import BaseModel
class RecipeResponse(BaseModel): class AllRecipeResponse(BaseModel):
List
class Config: class Config:
schema_extra = { schema_extra = {

View file

@ -1,19 +1,19 @@
from typing import List, Optional from typing import List, Optional
from db.database import db
from db.db_setup import generate_session from db.db_setup import generate_session
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from models.recipe_models import AllRecipeRequest from models.recipe_models import AllRecipeRequest
from services.recipe_services import read_requested_values
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"]) router = APIRouter(tags=["Recipes"])
@router.get("/api/all-recipes/", response_model=List[dict]) @router.get("/api/all-recipes/")
def get_all_recipes( def get_all_recipes(
keys: Optional[List[str]] = Query(...), keys: Optional[List[str]] = Query(...),
num: Optional[int] = 100, num: Optional[int] = 100,
db: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" """
Returns key data for all recipes based off the query paramters provided. Returns key data for all recipes based off the query paramters provided.
@ -21,28 +21,51 @@ def get_all_recipes(
recipes containing the slug, image, and name property. By default, responses recipes containing the slug, image, and name property. By default, responses
are limited to 100. are limited to 100.
At this time you can only query top level values:
- slug
- name
- description
- image
- recipeYield
- totalTime
- prepTime
- performTime
- rating
- orgURL
**Note:** You may experience problems with with query parameters. As an alternative **Note:** You may experience problems with with query parameters. As an alternative
you may also use the post method and provide a body. you may also use the post method and provide a body.
See the *Post* method for more details. See the *Post* method for more details.
""" """
all_recipes = read_requested_values(db, keys, num) return db.recipes.get_all_limit_columns(session, keys, limit=num)
return all_recipes
@router.post("/api/all-recipes/", response_model=List[dict]) @router.post("/api/all-recipes/")
def get_all_recipes_post( def get_all_recipes_post(
body: AllRecipeRequest, db: Session = Depends(generate_session) body: AllRecipeRequest, session: Session = Depends(generate_session)
): ):
""" """
Returns key data for all recipes based off the body data provided. Returns key data for all recipes based off the body data provided.
For example, if slug, image, and name are provided you will recieve a list of For example, if slug, image, and name are provided you will recieve a list of
recipes containing the slug, image, and name property. recipes containing the slug, image, and name property.
At this time you can only query top level values:
- slug
- name
- description
- image
- recipeYield
- totalTime
- prepTime
- performTime
- rating
- orgURL
Refer to the body example for data formats. Refer to the body example for data formats.
""" """
all_recipes = read_requested_values(db, body.properties, body.limit) return db.recipes.get_all_limit_columns(session, body.properties, body.limit)
return all_recipes

View file

@ -13,8 +13,7 @@ router = APIRouter(
@router.get("/all/") @router.get("/all/")
async def get_all_recipe_categories(session: Session = Depends(generate_session)): async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """ """ Returns a list of available categories in the database """
return db.categories.get_all_limit_columns(session, ["slug", "name"])
return db.categories.get_all_primary_keys(session)
@router.get("/{category}/", response_model=RecipeCategoryResponse) @router.get("/{category}/", response_model=RecipeCategoryResponse)
@ -23,3 +22,12 @@ def get_all_recipes_by_category(
): ):
""" Returns a list of recipes associated with the provided category. """ """ Returns a list of recipes associated with the provided category. """
return db.categories.get(session, category) return db.categories.get(session, category)
@router.delete("/{category}/")
async def delete_recipe_category(
category: str, session: Session = Depends(generate_session)
):
""" Removes a recipe category from the database """
db.categories.delete(session, category)

View file

@ -80,7 +80,8 @@ class ImportDatabase:
recipe_obj.save_to_db(self.session) recipe_obj.save_to_db(self.session)
successful_imports.append(recipe.stem) successful_imports.append(recipe.stem)
logger.info(f"Imported: {recipe.stem}") logger.info(f"Imported: {recipe.stem}")
except: except Exception as inst:
logger.error(inst)
logger.info(f"Failed Import: {recipe.stem}") logger.info(f"Failed Import: {recipe.stem}")
failed_imports.append(recipe.stem) failed_imports.append(recipe.stem)

View file

@ -40,7 +40,6 @@ class Recipe(BaseModel):
dateAdded: Optional[datetime.date] dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = [] notes: Optional[List[RecipeNote]] = []
rating: Optional[int] rating: Optional[int]
rating: Optional[int]
orgURL: Optional[str] orgURL: Optional[str]
extras: Optional[dict] = {} extras: Optional[dict] = {}
@ -138,33 +137,4 @@ class Recipe(BaseModel):
return db.recipes.get_all(session) return db.recipes.get_all(session)
def read_requested_values(
session: Session, keys: list, max_results: int = 0
) -> List[dict]:
"""
Pass in a list of key values to be run against the database. If a match is found
it is then added to a dictionary inside of a list. If a key does not exist the
it will simply not be added to the return data.
Parameters:
keys: list
Returns: returns a list of dicts containing recipe data
"""
recipe_list = []
for recipe in db.recipes.get_all(
session=session, limit=max_results, order_by="dateAdded"
):
recipe_details = {}
for key in keys:
try:
recipe_key = {key: recipe[key]}
except:
continue
recipe_details.update(recipe_key)
recipe_list.append(recipe_details)
return recipe_list