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 db.db_base import BaseDocument
@ -28,13 +29,13 @@ class _Recipes(BaseDocument):
class _Categories(BaseDocument):
def __init__(self) -> None:
self.primary_key = "name"
self.primary_key = "slug"
self.sql_model = Category
class _Tags(BaseDocument):
def __init__(self) -> None:
self.primary_key = "name"
self.primary_key = "slug"
self.sql_model = Tag

View file

@ -16,14 +16,43 @@ class BaseDocument:
def get_all(
self, session: Session, limit: int = None, order_by: str = None
) -> 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:
return list[0]
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(
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
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_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:
"""Creates a new database entry for the given SQL Alchemy Model.
Args:
Args: \n
session (Session): A Database Session
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:
"""Update a database entry.
Args:
Args: \n
session (Session): Database Session
match_value (str): Match "key"
new_data (str): Match "value"
@ -121,5 +150,4 @@ class BaseDocument:
)
session.delete(result)
session.commit()

View file

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

View file

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

View file

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

View file

@ -1,19 +1,19 @@
from typing import List, Optional
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends, Query
from models.recipe_models import AllRecipeRequest
from services.recipe_services import read_requested_values
from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"])
@router.get("/api/all-recipes/", response_model=List[dict])
@router.get("/api/all-recipes/")
def get_all_recipes(
keys: Optional[List[str]] = Query(...),
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.
@ -21,28 +21,51 @@ def get_all_recipes(
recipes containing the slug, image, and name property. By default, responses
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
you may also use the post method and provide a body.
See the *Post* method for more details.
"""
all_recipes = read_requested_values(db, keys, num)
return all_recipes
return db.recipes.get_all_limit_columns(session, keys, limit=num)
@router.post("/api/all-recipes/", response_model=List[dict])
@router.post("/api/all-recipes/")
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.
For example, if slug, image, and name are provided you will recieve a list of
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.
"""
all_recipes = read_requested_values(db, body.properties, body.limit)
return all_recipes
return db.recipes.get_all_limit_columns(session, body.properties, body.limit)

View file

@ -13,8 +13,7 @@ router = APIRouter(
@router.get("/all/")
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """
return db.categories.get_all_primary_keys(session)
return db.categories.get_all_limit_columns(session, ["slug", "name"])
@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. """
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)
successful_imports.append(recipe.stem)
logger.info(f"Imported: {recipe.stem}")
except:
except Exception as inst:
logger.error(inst)
logger.info(f"Failed Import: {recipe.stem}")
failed_imports.append(recipe.stem)

View file

@ -40,7 +40,6 @@ class Recipe(BaseModel):
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
rating: Optional[int]
rating: Optional[int]
orgURL: Optional[str]
extras: Optional[dict] = {}
@ -138,33 +137,4 @@ class Recipe(BaseModel):
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