diff --git a/mealie/db/data_access_layer/__init__.py b/mealie/db/data_access_layer/__init__.py new file mode 100644 index 00000000..69a2696f --- /dev/null +++ b/mealie/db/data_access_layer/__init__.py @@ -0,0 +1 @@ +from .db_access import DatabaseAccessLayer diff --git a/mealie/db/db_base.py b/mealie/db/data_access_layer/_base_access_model.py similarity index 90% rename from mealie/db/db_base.py rename to mealie/db/data_access_layer/_base_access_model.py index 8725c3b7..cda6986d 100644 --- a/mealie/db/db_base.py +++ b/mealie/db/data_access_layer/_base_access_model.py @@ -1,7 +1,7 @@ -from typing import Union +from typing import Callable, Union from mealie.core.root_logger import get_logger -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase from pydantic import BaseModel from sqlalchemy import func from sqlalchemy.orm import load_only @@ -10,16 +10,25 @@ from sqlalchemy.orm.session import Session logger = get_logger() -class BaseDocument: - def __init__(self) -> None: - self.primary_key: str - self.store: str - self.sql_model: SqlAlchemyBase - self.schema: BaseModel - self.observers: list = None +class BaseAccessModel: + def __init__(self, primary_key, sql_model, schema) -> None: + self.primary_key: str = primary_key + self.sql_model: SqlAlchemyBase = sql_model + self.schema: BaseModel = schema + + self.observers: list = [] + + def subscribe(self, func: Callable) -> None: + self.observers.append(func) + + # TODO: Run Observer in Async Background Task + def update_observers(self) -> None: + if self.observers: + for observer in self.observers: + observer() def get_all( - self, session: Session, limit: int = None, order_by: str = None, start=0, end=9999, override_schema=None + self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None ) -> list[dict]: eff_schema = override_schema or self.schema @@ -130,7 +139,7 @@ class BaseDocument: session.add(new_document) session.commit() - if hasattr(self, "update_observers"): + if self.observers: self.update_observers() return self.schema.from_orm(new_document) @@ -150,7 +159,7 @@ class BaseDocument: entry = self._query_one(session=session, match_value=match_value) entry.update(session=session, **new_data) - if hasattr(self, "update_observers"): + if self.observers: self.update_observers() session.commit() @@ -176,7 +185,7 @@ class BaseDocument: session.delete(result) session.commit() - if hasattr(self, "update_observers"): + if self.observers: self.update_observers() return results_as_model @@ -185,7 +194,7 @@ class BaseDocument: session.query(self.sql_model).delete() session.commit() - if hasattr(self, "update_observers"): + if self.observers: self.update_observers() def count_all(self, session: Session, match_key=None, match_value=None) -> int: diff --git a/mealie/db/data_access_layer/db_access.py b/mealie/db/data_access_layer/db_access.py new file mode 100644 index 00000000..41bf5c85 --- /dev/null +++ b/mealie/db/data_access_layer/db_access.py @@ -0,0 +1,86 @@ +from logging import getLogger + +from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel +from mealie.db.models.event import Event, EventNotification +from mealie.db.models.group import Group +from mealie.db.models.mealplan import MealPlan +from mealie.db.models.recipe.comment import RecipeComment +from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel +from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag +from mealie.db.models.settings import CustomPage, SiteSettings +from mealie.db.models.shopping_list import ShoppingList +from mealie.db.models.sign_up import SignUp +from mealie.db.models.theme import SiteThemeModel +from mealie.db.models.users import LongLiveToken, User +from mealie.schema.admin import CustomPageOut +from mealie.schema.admin import SiteSettings as SiteSettingsSchema +from mealie.schema.admin import SiteTheme +from mealie.schema.events import Event as EventSchema +from mealie.schema.events import EventNotificationIn +from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut +from mealie.schema.recipe import ( + CommentOut, + IngredientFood, + IngredientUnit, + Recipe, + RecipeCategoryResponse, + RecipeTagResponse, +) +from mealie.schema.user import GroupInDB, LongLiveTokenInDB, SignUpOut, UserInDB +from sqlalchemy.orm.session import Session + +from ._base_access_model import BaseAccessModel +from .recipe_access_model import RecipeDataAccessModel +from .user_access_model import UserDataAccessModel + +logger = getLogger() + + +DEFAULT_PK = "id" + + +class CategoryDataAccessModel(BaseAccessModel): + def get_empty(self, session: Session): + self.schema + return session.query(Category).filter(~Category.recipes.any()).all() + + +class TagsDataAccessModel(BaseAccessModel): + def get_empty(self, session: Session): + return session.query(Tag).filter(~Tag.recipes.any()).all() + + +class DatabaseAccessLayer: + """ + `DatabaseAccessLayer` class is the data access layer for all database actions within + Mealie. Database uses composition from classes derived from BaseAccessModel. These + can be substantiated from the BaseAccessModel class or through inheritance when + additional methods are required. + """ + + def __init__(self) -> None: + + # Recipes + self.recipes = RecipeDataAccessModel("slug", RecipeModel, Recipe) + self.ingredient_foods = BaseAccessModel(DEFAULT_PK, IngredientFoodModel, IngredientFood) + self.ingredient_units = BaseAccessModel(DEFAULT_PK, IngredientUnitModel, IngredientUnit) + self.comments = BaseAccessModel(DEFAULT_PK, RecipeComment, CommentOut) + + # Tags and Categories + self.categories = CategoryDataAccessModel("slug", Category, RecipeCategoryResponse) + self.tags = TagsDataAccessModel("slug", Tag, RecipeTagResponse) + + # Site + self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema) + self.themes = BaseAccessModel(DEFAULT_PK, SiteThemeModel, SiteTheme) + self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut) + self.custom_pages = BaseAccessModel(DEFAULT_PK, CustomPage, CustomPageOut) + self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn) + self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema) + + # Users / Groups + self.users = UserDataAccessModel(DEFAULT_PK, User, UserInDB) + self.api_tokens = BaseAccessModel(DEFAULT_PK, LongLiveToken, LongLiveTokenInDB) + self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB) + self.meals = BaseAccessModel("uid", MealPlan, MealPlanOut) + self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut) diff --git a/mealie/db/data_access_layer/group_access_model.py b/mealie/db/data_access_layer/group_access_model.py new file mode 100644 index 00000000..b54ab99a --- /dev/null +++ b/mealie/db/data_access_layer/group_access_model.py @@ -0,0 +1,22 @@ +from mealie.schema.meal_plan.meal import MealPlanOut +from mealie.schema.user.user import GroupInDB +from sqlalchemy.orm.session import Session + +from ._base_access_model import BaseAccessModel + + +class GroupDataAccessModel(BaseAccessModel): + def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanOut]: + """A Helper function to get the group from the database and return a sorted list of + + Args: + session (Session): SqlAlchemy Session + match_value (str): Match Value + match_key (str, optional): Match Key. Defaults to "name". + + Returns: + list[MealPlanOut]: [description] + """ + group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none() + + return group.mealplans diff --git a/mealie/db/data_access_layer/recipe_access_model.py b/mealie/db/data_access_layer/recipe_access_model.py new file mode 100644 index 00000000..1c34e4e8 --- /dev/null +++ b/mealie/db/data_access_layer/recipe_access_model.py @@ -0,0 +1,57 @@ +from random import randint + +from mealie.db.models.recipe.recipe import RecipeModel +from mealie.db.models.recipe.settings import RecipeSettings +from sqlalchemy.orm.session import Session + +from ._base_access_model import BaseAccessModel + + +class RecipeDataAccessModel(BaseAccessModel): + def get_all_public(self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None): + eff_schema = override_schema or self.schema + + if order_by: + order_attr = getattr(self.sql_model, str(order_by)) + + return [ + eff_schema.from_orm(x) + for x in session.query(self.sql_model) + .join(RecipeSettings) + .filter(RecipeSettings.public == True) # noqa: 711 + .order_by(order_attr.desc()) + .offset(start) + .limit(limit) + .all() + ] + + return [ + eff_schema.from_orm(x) + for x in session.query(self.sql_model) + .join(RecipeSettings) + .filter(RecipeSettings.public == True) # noqa: 711 + .offset(start) + .limit(limit) + .all() + ] + + def update_image(self, session: Session, slug: str, _: str = None) -> str: + entry: RecipeModel = self._query_one(session, match_value=slug) + entry.image = randint(0, 255) + session.commit() + + return entry.image + + def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int: + return self._count_attribute( + session, + attribute_name=RecipeModel.recipe_category, + attr_match=None, + count=count, + override_schema=override_schema, + ) + + def count_untagged(self, session: Session, count=True, override_schema=None) -> int: + return self._count_attribute( + session, attribute_name=RecipeModel.tags, attr_match=None, count=count, override_schema=override_schema + ) diff --git a/mealie/db/data_access_layer/user_access_model.py b/mealie/db/data_access_layer/user_access_model.py new file mode 100644 index 00000000..04c3ad48 --- /dev/null +++ b/mealie/db/data_access_layer/user_access_model.py @@ -0,0 +1,10 @@ +from ._base_access_model import BaseAccessModel + + +class UserDataAccessModel(BaseAccessModel): + def update_password(self, session, id, password: str): + entry = self._query_one(session=session, match_value=id) + entry.update_password(password) + session.commit() + + return self.schema.from_orm(entry) diff --git a/mealie/db/data_initialization/init_units_foods.py b/mealie/db/data_initialization/init_units_foods.py index 007d0873..71df21b8 100644 --- a/mealie/db/data_initialization/init_units_foods.py +++ b/mealie/db/data_initialization/init_units_foods.py @@ -1,4 +1,4 @@ -from mealie.schema.recipe.recipe import IngredientUnit +from mealie.schema.recipe.units_and_foods import CreateIngredientUnit from sqlalchemy.orm.session import Session from ..data_access_layer import DatabaseAccessLayer @@ -7,21 +7,21 @@ from ..data_access_layer import DatabaseAccessLayer def get_default_units(): return [ # Volume - IngredientUnit(name="teaspoon", abbreviation="tsp"), - IngredientUnit(name="tablespoon", abbreviation="tbsp"), - IngredientUnit(name="fluid ounce", abbreviation="fl oz"), - IngredientUnit(name="cup", abbreviation="cup"), - IngredientUnit(name="pint", abbreviation="pt"), - IngredientUnit(name="quart", abbreviation="qt"), - IngredientUnit(name="gallon", abbreviation="gal"), - IngredientUnit(name="milliliter", abbreviation="ml"), - IngredientUnit(name="liter", abbreviation="l"), + CreateIngredientUnit(name="teaspoon", abbreviation="tsp"), + CreateIngredientUnit(name="tablespoon", abbreviation="tbsp"), + CreateIngredientUnit(name="fluid ounce", abbreviation="fl oz"), + CreateIngredientUnit(name="cup", abbreviation="cup"), + CreateIngredientUnit(name="pint", abbreviation="pt"), + CreateIngredientUnit(name="quart", abbreviation="qt"), + CreateIngredientUnit(name="gallon", abbreviation="gal"), + CreateIngredientUnit(name="milliliter", abbreviation="ml"), + CreateIngredientUnit(name="liter", abbreviation="l"), # Mass Weight - IngredientUnit(name="pound", abbreviation="lb"), - IngredientUnit(name="ounce", abbreviation="oz"), - IngredientUnit(name="gram", abbreviation="g"), - IngredientUnit(name="kilogram", abbreviation="kg"), - IngredientUnit(name="milligram", abbreviation="mg"), + CreateIngredientUnit(name="pound", abbreviation="lb"), + CreateIngredientUnit(name="ounce", abbreviation="oz"), + CreateIngredientUnit(name="gram", abbreviation="g"), + CreateIngredientUnit(name="kilogram", abbreviation="kg"), + CreateIngredientUnit(name="milligram", abbreviation="mg"), ] diff --git a/mealie/db/database.py b/mealie/db/database.py index 79cc4b8e..cb558b82 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -1,268 +1,3 @@ -from logging import getLogger -from random import randint -from typing import Callable +from .data_access_layer import DatabaseAccessLayer -from mealie.db.db_base import BaseDocument -from mealie.db.models.event import Event, EventNotification -from mealie.db.models.group import Group -from mealie.db.models.mealplan import MealPlan -from mealie.db.models.recipe.comment import RecipeComment -from mealie.db.models.recipe.ingredient import IngredientFood, IngredientUnit -from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag -from mealie.db.models.recipe.settings import RecipeSettings -from mealie.db.models.settings import CustomPage, SiteSettings -from mealie.db.models.shopping_list import ShoppingList -from mealie.db.models.sign_up import SignUp -from mealie.db.models.theme import SiteThemeModel -from mealie.db.models.users import LongLiveToken, User -from mealie.schema.admin import CustomPageOut -from mealie.schema.admin import SiteSettings as SiteSettingsSchema -from mealie.schema.admin import SiteTheme -from mealie.schema.events import Event as EventSchema -from mealie.schema.events import EventNotificationIn -from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut -from mealie.schema.recipe import ( - CommentOut, - Recipe, - RecipeCategoryResponse, - RecipeIngredientFood, - RecipeIngredientUnit, - RecipeTagResponse, -) -from mealie.schema.user import GroupInDB, LongLiveTokenInDB, SignUpOut, UserInDB -from sqlalchemy.orm.session import Session - -logger = getLogger() - - -class _Recipes(BaseDocument): - def __init__(self) -> None: - self.primary_key = "slug" - self.sql_model: RecipeModel = RecipeModel - self.schema: Recipe = Recipe - - self.observers = [] - - def get_all_public(self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None): - eff_schema = override_schema or self.schema - - if order_by: - order_attr = getattr(self.sql_model, str(order_by)) - - return [ - eff_schema.from_orm(x) - for x in session.query(self.sql_model) - .join(RecipeSettings) - .filter(RecipeSettings.public == True) # noqa: 711 - .order_by(order_attr.desc()) - .offset(start) - .limit(limit) - .all() - ] - - return [ - eff_schema.from_orm(x) - for x in session.query(self.sql_model) - .join(RecipeSettings) - .filter(RecipeSettings.public == True) # noqa: 711 - .offset(start) - .limit(limit) - .all() - ] - - def update_image(self, session: Session, slug: str, _: str = None) -> str: - entry: RecipeModel = self._query_one(session, match_value=slug) - entry.image = randint(0, 255) - session.commit() - - return entry.image - - def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int: - return self._count_attribute( - session, - attribute_name=RecipeModel.recipe_category, - attr_match=None, - count=count, - override_schema=override_schema, - ) - - def count_untagged(self, session: Session, count=True, override_schema=None) -> int: - return self._count_attribute( - session, attribute_name=RecipeModel.tags, attr_match=None, count=count, override_schema=override_schema - ) - - def subscribe(self, func: Callable) -> None: - self.observers.append(func) - - def update_observers(self) -> None: - for observer in self.observers: - observer() - - -class _IngredientFoods(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = IngredientFood - self.schema = RecipeIngredientFood - - -class _IngredientUnits(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = IngredientUnit - self.schema = RecipeIngredientUnit - - -class _Categories(BaseDocument): - def __init__(self) -> None: - self.primary_key = "slug" - self.sql_model = Category - self.schema = RecipeCategoryResponse - - def get_empty(self, session: Session): - return session.query(Category).filter(~Category.recipes.any()).all() - - -class _Tags(BaseDocument): - def __init__(self) -> None: - self.primary_key = "slug" - self.sql_model = Tag - self.schema = RecipeTagResponse - - def get_empty(self, session: Session): - return session.query(Tag).filter(~Tag.recipes.any()).all() - - -class _Meals(BaseDocument): - def __init__(self) -> None: - self.primary_key = "uid" - self.sql_model = MealPlan - self.schema = MealPlanOut - - -class _Settings(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = SiteSettings - self.schema = SiteSettingsSchema - - -class _Themes(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = SiteThemeModel - self.schema = SiteTheme - - -class _Users(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = User - self.schema = UserInDB - - def update_password(self, session, id, password: str): - entry = self._query_one(session=session, match_value=id) - entry.update_password(password) - session.commit() - - return self.schema.from_orm(entry) - - -class _Comments(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = RecipeComment - self.schema = CommentOut - - -class _LongLiveToken(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = LongLiveToken - self.schema = LongLiveTokenInDB - - -class _Groups(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = Group - self.schema = GroupInDB - - def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanOut]: - """A Helper function to get the group from the database and return a sorted list of - - Args: - session (Session): SqlAlchemy Session - match_value (str): Match Value - match_key (str, optional): Match Key. Defaults to "name". - - Returns: - list[MealPlanOut]: [description] - """ - group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none() - - return group.mealplans - - -class _ShoppingList(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = ShoppingList - self.schema = ShoppingListOut - - -class _SignUps(BaseDocument): - def __init__(self) -> None: - self.primary_key = "token" - self.sql_model = SignUp - self.schema = SignUpOut - - -class _CustomPages(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = CustomPage - self.schema = CustomPageOut - - -class _Events(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = Event - self.schema = EventSchema - - -class _EventNotification(BaseDocument): - def __init__(self) -> None: - self.primary_key = "id" - self.sql_model = EventNotification - self.schema = EventNotificationIn - - -class Database: - def __init__(self) -> None: - # Recipes - self.recipes = _Recipes() - self.ingredient_foods = _IngredientUnits() - self.ingredient_units = _IngredientFoods() - self.categories = _Categories() - self.tags = _Tags() - self.comments = _Comments() - - # Site - self.settings = _Settings() - self.themes = _Themes() - self.sign_ups = _SignUps() - self.custom_pages = _CustomPages() - self.event_notifications = _EventNotification() - self.events = _Events() - - # Users / Groups - self.users = _Users() - self.api_tokens = _LongLiveToken() - self.groups = _Groups() - self.meals = _Meals() - self.shopping_lists = _ShoppingList() - - -db = Database() +db = DatabaseAccessLayer() diff --git a/mealie/db/db_setup.py b/mealie/db/db_setup.py index 654a6c2c..2eeccd61 100644 --- a/mealie/db/db_setup.py +++ b/mealie/db/db_setup.py @@ -1,8 +1,22 @@ +import sqlalchemy as sa from mealie.core.config import settings -from mealie.db.models.db_session import sql_global_init +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session -SessionLocal = sql_global_init(settings.DB_URL) + +def sql_global_init(db_url: str): + connect_args = {} + if "sqlite" in db_url: + connect_args["check_same_thread"] = False + + engine = sa.create_engine(db_url, echo=False, connect_args=connect_args) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + return SessionLocal, engine + + +SessionLocal, engine = sql_global_init(settings.DB_URL) def create_session() -> Session: diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py index 10b3eea5..70b4ca47 100644 --- a/mealie/db/init_db.py +++ b/mealie/db/init_db.py @@ -1,8 +1,10 @@ from mealie.core import root_logger from mealie.core.config import settings from mealie.core.security import get_password_hash +from mealie.db.data_initialization.init_units_foods import default_recipe_unit_init from mealie.db.database import db -from mealie.db.db_setup import create_session +from mealie.db.db_setup import create_session, engine +from mealie.db.models._model_base import SqlAlchemyBase from mealie.schema.admin import SiteSettings, SiteTheme from mealie.services.events import create_general_event from sqlalchemy.orm import Session @@ -10,16 +12,26 @@ from sqlalchemy.orm import Session logger = root_logger.get_logger("init_db") -def init_db(db: Session = None) -> None: - if not db: - db = create_session() +def create_all_models(): + import mealie.db.models._all_models # noqa: F401 - default_group_init(db) - default_settings_init(db) - default_theme_init(db) - default_user_init(db) + SqlAlchemyBase.metadata.create_all(engine) - db.close() + +def init_db(session: Session = None) -> None: + create_all_models() + + if not session: + session = create_session() + + default_group_init(session) + default_settings_init(session) + default_theme_init(session) + default_user_init(session) + + default_recipe_unit_init(db, session) + + session.close() def default_theme_init(session: Session): @@ -67,8 +79,12 @@ def default_user_init(session: Session): def main(): - session = create_session() - init_user = db.users.get(session, "1", "id") + try: + session = create_session() + init_user = db.users.get(session, "1", "id") + except Exception: + init_db() + return if init_user: logger.info("Database Exists") else: diff --git a/mealie/db/models/_model_base.py b/mealie/db/models/_model_base.py new file mode 100644 index 00000000..cea0ace3 --- /dev/null +++ b/mealie/db/models/_model_base.py @@ -0,0 +1,52 @@ +import uuid +from datetime import datetime + +from mealie.db.db_setup import SessionLocal +from sqlalchemy import Column, DateTime, Integer +from sqlalchemy.ext.declarative import as_declarative +from sqlalchemy.orm import declarative_base + + +def get_uuid_as_hex() -> str: + """ + Generate a UUID as a hex string. + :return: UUID as a hex string. + """ + return uuid.uuid4().hex + + +@as_declarative() +class Base: + id = Column(Integer, primary_key=True) + created_at = Column(DateTime, default=datetime.now()) + + # @declared_attr + # def __tablename__(cls): + # return cls.__name__.lower() + + +class BaseMixins: + """ + `self.update` method which directly passing arugments to the `__init__` + `cls.get_ref` method which will return the object from the database or none. Useful for many-to-many relationships. + """ + + class Config: + get_attr = "id" + + def update(self, *args, **kwarg): + self.__init__(*args, **kwarg) + + @classmethod + def get_ref(cls, match_value: str, match_attr: str = None): + match_attr = match_attr = cls.Config.get_attr + + if match_value is None: + return None + + with SessionLocal() as session: + eff_ref = getattr(cls, match_attr) + return session.query(cls).filter(eff_ref == match_value).one_or_none() + + +SqlAlchemyBase = declarative_base(cls=Base, constructor=None) diff --git a/mealie/db/models/db_session.py b/mealie/db/models/db_session.py deleted file mode 100644 index b5ac2ef3..00000000 --- a/mealie/db/models/db_session.py +++ /dev/null @@ -1,19 +0,0 @@ -import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase -from sqlalchemy.orm import sessionmaker - - -def sql_global_init(db_url: str): - connect_args = {} - if "sqlite" in db_url: - connect_args["check_same_thread"] = False - - engine = sa.create_engine(db_url, echo=False, connect_args=connect_args) - - SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - - import mealie.db.models._all_models # noqa: F401 - - SqlAlchemyBase.metadata.create_all(engine) - - return SessionLocal diff --git a/mealie/db/models/event.py b/mealie/db/models/event.py index cc68abdd..ad6fcdfd 100644 --- a/mealie/db/models/event.py +++ b/mealie/db/models/event.py @@ -1,4 +1,4 @@ -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from sqlalchemy import Boolean, Column, DateTime, Integer, String diff --git a/mealie/db/models/group.py b/mealie/db/models/group.py index 661038f6..ccd2b6de 100644 --- a/mealie/db/models/group.py +++ b/mealie/db/models/group.py @@ -1,7 +1,7 @@ import sqlalchemy as sa import sqlalchemy.orm as orm from mealie.core.config import settings -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.category import Category, group2categories from sqlalchemy.orm.session import Session diff --git a/mealie/db/models/mealplan.py b/mealie/db/models/mealplan.py index 629b3f02..861bff2a 100644 --- a/mealie/db/models/mealplan.py +++ b/mealie/db/models/mealplan.py @@ -1,6 +1,6 @@ import sqlalchemy.orm as orm from mealie.db.models.group import Group -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.recipe import RecipeModel from mealie.db.models.shopping_list import ShoppingList from sqlalchemy import Column, Date, ForeignKey, Integer, String diff --git a/mealie/db/models/model_base.py b/mealie/db/models/model_base.py deleted file mode 100644 index d3b23d3b..00000000 --- a/mealie/db/models/model_base.py +++ /dev/null @@ -1,14 +0,0 @@ -import sqlalchemy.ext.declarative as dec -from requests import Session - -SqlAlchemyBase = dec.declarative_base() - - -class BaseMixins: - def update(self, *args, **kwarg): - self.__init__(*args, **kwarg) - - @classmethod - def get_ref(cls_type, session: Session, match_value: str, match_attr: str = "id"): - eff_ref = getattr(cls_type, match_attr) - return session.query(cls_type).filter(eff_ref == match_value).one_or_none() diff --git a/mealie/db/models/recipe/api_extras.py b/mealie/db/models/recipe/api_extras.py index c4172cb4..c9038ba2 100644 --- a/mealie/db/models/recipe/api_extras.py +++ b/mealie/db/models/recipe/api_extras.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class ApiExtras(SqlAlchemyBase): diff --git a/mealie/db/models/recipe/assets.py b/mealie/db/models/recipe/assets.py index 0a240062..048dfdb6 100644 --- a/mealie/db/models/recipe/assets.py +++ b/mealie/db/models/recipe/assets.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class RecipeAsset(SqlAlchemyBase): diff --git a/mealie/db/models/recipe/category.py b/mealie/db/models/recipe/category.py index a449cf21..d898dbbe 100644 --- a/mealie/db/models/recipe/category.py +++ b/mealie/db/models/recipe/category.py @@ -1,7 +1,7 @@ import sqlalchemy as sa import sqlalchemy.orm as orm from mealie.core import root_logger -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from slugify import slugify from sqlalchemy.orm import validates @@ -36,29 +36,25 @@ custom_pages2categories = sa.Table( ) -class Category(SqlAlchemyBase): +class Category(SqlAlchemyBase, BaseMixins): __tablename__ = "categories" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String, index=True, nullable=False) slug = sa.Column(sa.String, index=True, unique=True, nullable=False) recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipe_category") + class Config: + get_attr = "slug" + @validates("name") def validate_name(self, key, name): assert name != "" return name - def __init__(self, name, session=None) -> None: + def __init__(self, name, **_) -> None: self.name = name.strip() self.slug = slugify(name) - def update(self, name, session=None) -> None: - self.__init__(name, session) - - @staticmethod - def get_ref(session, slug: str): - return session.query(Category).filter(Category.slug == slug).one() - @staticmethod def create_if_not_exist(session, name: str = None): test_slug = slugify(name) diff --git a/mealie/db/models/recipe/comment.py b/mealie/db/models/recipe/comment.py index 1813be29..1905901d 100644 --- a/mealie/db/models/recipe/comment.py +++ b/mealie/db/models/recipe/comment.py @@ -1,7 +1,7 @@ from datetime import datetime from uuid import uuid4 -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.recipe import RecipeModel from mealie.db.models.users import User from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, orm diff --git a/mealie/db/models/recipe/ingredient.py b/mealie/db/models/recipe/ingredient.py index 7235f752..8ca2da96 100644 --- a/mealie/db/models/recipe/ingredient.py +++ b/mealie/db/models/recipe/ingredient.py @@ -1,4 +1,4 @@ -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from requests import Session from sqlalchemy import Column, ForeignKey, Integer, String, Table, orm @@ -44,7 +44,7 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins): pass -class RecipeIngredient(SqlAlchemyBase): +class RecipeIngredient(SqlAlchemyBase, BaseMixins): __tablename__ = "recipes_ingredients" id = Column(Integer, primary_key=True) position = Column(Integer) @@ -63,6 +63,10 @@ class RecipeIngredient(SqlAlchemyBase): def __init__(self, title: str, note: str, unit: dict, food: dict, quantity: int, session: Session, **_) -> None: self.title = title self.note = note - self.unit = IngredientUnitModel.get_ref_or_create(session, unit) - self.food = IngredientFoodModel.get_ref_or_create(session, food) self.quantity = quantity + + if unit: + self.unit = IngredientUnitModel.get_ref(unit.get("id")) + + if food: + self.food = IngredientFoodModel.get_ref(unit.get("id")) diff --git a/mealie/db/models/recipe/instruction.py b/mealie/db/models/recipe/instruction.py index 59fb88d6..5915a8a0 100644 --- a/mealie/db/models/recipe/instruction.py +++ b/mealie/db/models/recipe/instruction.py @@ -1,4 +1,4 @@ -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase from sqlalchemy import Column, ForeignKey, Integer, String diff --git a/mealie/db/models/recipe/note.py b/mealie/db/models/recipe/note.py index 28ee4630..77422ddb 100644 --- a/mealie/db/models/recipe/note.py +++ b/mealie/db/models/recipe/note.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class Note(SqlAlchemyBase): diff --git a/mealie/db/models/recipe/nutrition.py b/mealie/db/models/recipe/nutrition.py index 5856e3de..9f04ad04 100644 --- a/mealie/db/models/recipe/nutrition.py +++ b/mealie/db/models/recipe/nutrition.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class Nutrition(SqlAlchemyBase): diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py index e55f36de..a3aaaad4 100644 --- a/mealie/db/models/recipe/recipe.py +++ b/mealie/db/models/recipe/recipe.py @@ -3,26 +3,24 @@ from datetime import date import sqlalchemy as sa import sqlalchemy.orm as orm -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase -from mealie.db.models.recipe.api_extras import ApiExtras -from mealie.db.models.recipe.assets import RecipeAsset -from mealie.db.models.recipe.category import Category, recipes2categories -from mealie.db.models.recipe.ingredient import RecipeIngredient -from mealie.db.models.recipe.instruction import RecipeInstruction -from mealie.db.models.recipe.note import Note -from mealie.db.models.recipe.nutrition import Nutrition -from mealie.db.models.recipe.settings import RecipeSettings -from mealie.db.models.recipe.tag import Tag, recipes2tags -from mealie.db.models.recipe.tool import Tool from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.orm import validates +from .._model_base import BaseMixins, SqlAlchemyBase +from .api_extras import ApiExtras +from .assets import RecipeAsset +from .category import Category, recipes2categories +from .ingredient import RecipeIngredient +from .instruction import RecipeInstruction +from .note import Note +from .nutrition import Nutrition +from .settings import RecipeSettings +from .tag import Tag, recipes2tags +from .tool import Tool + class RecipeModel(SqlAlchemyBase, BaseMixins): __tablename__ = "recipes" - # Database Specific - id = sa.Column(sa.Integer, primary_key=True) - # General Recipe Properties name = sa.Column(sa.String, nullable=False) description = sa.Column(sa.String) @@ -128,11 +126,11 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): self.perform_time = perform_time self.cook_time = cook_time - self.recipe_category = [Category.create_if_not_exist(session=session, name=cat) for cat in recipe_category] + self.recipe_category = [x for x in [Category.get_ref(cat) for cat in recipe_category] if x] # Mealie Specific self.settings = RecipeSettings(**settings) if settings else RecipeSettings() - self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] + self.tags = [x for x in [Tag.get_ref(tag) for tag in tags] if x] self.slug = slug self.notes = [Note(**note) for note in notes] self.rating = rating @@ -142,7 +140,3 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): # Time Stampes self.date_added = date_added self.date_updated = datetime.datetime.now() - - def update(self, **_): - """Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions""" - self.__init__(**_) diff --git a/mealie/db/models/recipe/settings.py b/mealie/db/models/recipe/settings.py index 9e332a06..96437cfe 100644 --- a/mealie/db/models/recipe/settings.py +++ b/mealie/db/models/recipe/settings.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class RecipeSettings(SqlAlchemyBase): diff --git a/mealie/db/models/recipe/tag.py b/mealie/db/models/recipe/tag.py index 435f22ba..45cce26c 100644 --- a/mealie/db/models/recipe/tag.py +++ b/mealie/db/models/recipe/tag.py @@ -1,7 +1,7 @@ import sqlalchemy as sa import sqlalchemy.orm as orm from mealie.core import root_logger -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from slugify import slugify from sqlalchemy.orm import validates @@ -15,13 +15,16 @@ recipes2tags = sa.Table( ) -class Tag(SqlAlchemyBase): +class Tag(SqlAlchemyBase, BaseMixins): __tablename__ = "tags" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String, index=True, nullable=False) slug = sa.Column(sa.String, index=True, unique=True, nullable=False) recipes = orm.relationship("RecipeModel", secondary=recipes2tags, back_populates="tags") + class Config: + get_attr = "slug" + @validates("name") def validate_name(self, key, name): assert name != "" @@ -31,9 +34,6 @@ class Tag(SqlAlchemyBase): self.name = name.strip() self.slug = slugify(self.name) - def update(self, name, session=None) -> None: - self.__init__(name, session) - @staticmethod def create_if_not_exist(session, name: str = None): test_slug = slugify(name) diff --git a/mealie/db/models/recipe/tool.py b/mealie/db/models/recipe/tool.py index 2406864f..b24beee7 100644 --- a/mealie/db/models/recipe/tool.py +++ b/mealie/db/models/recipe/tool.py @@ -1,5 +1,5 @@ import sqlalchemy as sa -from mealie.db.models.model_base import SqlAlchemyBase +from mealie.db.models._model_base import SqlAlchemyBase class Tool(SqlAlchemyBase): diff --git a/mealie/db/models/settings.py b/mealie/db/models/settings.py index efaacfef..fc34c907 100644 --- a/mealie/db/models/settings.py +++ b/mealie/db/models/settings.py @@ -1,6 +1,6 @@ import sqlalchemy as sa import sqlalchemy.orm as orm -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.category import Category, custom_pages2categories, site_settings2categories from sqlalchemy.orm import Session diff --git a/mealie/db/models/shopping_list.py b/mealie/db/models/shopping_list.py index 41d3e625..c9a45391 100644 --- a/mealie/db/models/shopping_list.py +++ b/mealie/db/models/shopping_list.py @@ -1,6 +1,6 @@ import sqlalchemy.orm as orm from mealie.db.models.group import Group -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from requests import Session from sqlalchemy import Boolean, Column, ForeignKey, Integer, String from sqlalchemy.ext.orderinglist import ordering_list diff --git a/mealie/db/models/sign_up.py b/mealie/db/models/sign_up.py index 7d8c32ab..55fb17b2 100644 --- a/mealie/db/models/sign_up.py +++ b/mealie/db/models/sign_up.py @@ -1,4 +1,4 @@ -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from sqlalchemy import Boolean, Column, Integer, String diff --git a/mealie/db/models/theme.py b/mealie/db/models/theme.py index 275afd41..d4139945 100644 --- a/mealie/db/models/theme.py +++ b/mealie/db/models/theme.py @@ -1,5 +1,5 @@ import sqlalchemy.orm as orm -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from sqlalchemy import Column, ForeignKey, Integer, String diff --git a/mealie/db/models/users.py b/mealie/db/models/users.py index 60ce7110..7c6f0638 100644 --- a/mealie/db/models/users.py +++ b/mealie/db/models/users.py @@ -1,6 +1,6 @@ from mealie.core.config import settings from mealie.db.models.group import Group -from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.recipe import RecipeModel from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm