prs-fleshgolem-2070: feat: sqlalchemy 2.0 (#2096)

* upgrade sqlalchemy to 2.0

* rewrite all db models to sqla 2.0 mapping api

* fix some importing and typing weirdness

* fix types of a lot of nullable columns

* remove get_ref methods

* fix issues found by tests

* rewrite all queries in repository_recipe to 2.0 style

* rewrite all repository queries to 2.0 api

* rewrite all remaining queries to 2.0 api

* remove now-unneeded __allow_unmapped__ flag

* remove and fix some unneeded cases of "# type: ignore"

* fix formatting

* bump black version

* run black

* can this please be the last one. okay. just. okay.

* fix repository errors

* remove return

* drop open API validator

---------

Co-authored-by: Sören Busch <fleshgolem@gmx.net>
This commit is contained in:
Hayden 2023-02-06 18:43:12 -09:00 committed by GitHub
parent 91cd00976a
commit 9e77a9f367
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1776 additions and 1572 deletions

2
.gitignore vendored
View File

@ -159,3 +159,5 @@ dev/code-generation/generated/test_routes.py
mealie/services/parser_services/crfpp/model.crfmodel
lcov.info
dev/code-generation/openapi.json
.run/

View File

@ -11,10 +11,6 @@ repos:
- id: trailing-whitespace
exclude: ^tests/data/
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.241
hooks:
- id: ruff

View File

@ -25,7 +25,6 @@ def get_path_objects(app: FastAPI):
for key, value in app.openapi().items():
if key == "paths":
for key, value in value.items():
paths.append(
PathObject(
route_object=RouteObject(key),
@ -50,7 +49,6 @@ def read_template(file: Path):
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
template = Template(read_template(CodeTemplates.pytest_routes))
content = template.render(
paths={

View File

@ -79,14 +79,12 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
modules: list[Modules] = []
for file in root.iterdir():
if file.is_dir() and file.name not in SKIP:
modules.append(Modules(directory=file))
return modules
def main():
modules = find_modules(SCHEMA_PATH)
for module in modules:

View File

@ -232,7 +232,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
def login(username="changeme@email.com", password="MyPassword"):
payload = {"username": username, "password": password}
r = requests.post("http://localhost:9000/api/auth/token", payload)

View File

@ -9,7 +9,6 @@ class GunicornConfig:
"""Configuration to generate the properties for Gunicorn"""
def __init__(self):
# Env Variables
self.host = os.getenv("HOST", "127.0.0.1")
self.port = os.getenv("API_PORT", "9000")

View File

@ -15,14 +15,9 @@ def sql_global_init(db_url: str):
if "sqlite" in db_url:
connect_args["check_same_thread"] = False
engine = sa.create_engine(
db_url,
echo=False,
connect_args=connect_args,
pool_pre_ping=True,
)
engine = sa.create_engine(db_url, echo=False, connect_args=connect_args, pool_pre_ping=True, future=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True)
return SessionLocal, engine

View File

@ -2,7 +2,7 @@ from collections.abc import Callable
from pathlib import Path
from time import sleep
from sqlalchemy import engine, orm
from sqlalchemy import engine, orm, text
from alembic import command, config, script
from alembic.config import Config
@ -59,7 +59,7 @@ def safe_try(func: Callable):
def connect(session: orm.Session) -> bool:
try:
session.execute("SELECT 1")
session.execute(text("SELECT 1"))
return True
except Exception as e:
logger.error(f"Error connecting to database: {e}")

View File

@ -1,5 +1,5 @@
from .group import *
from .labels import *
from .recipe.recipe import * # type: ignore
from .recipe import *
from .server import *
from .users import *

View File

@ -1,37 +1,19 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm.session import Session
from sqlalchemy import DateTime, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@as_declarative()
class Base:
id = Column(Integer, primary_key=True)
created_at = Column(DateTime, default=datetime.now)
update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class SqlAlchemyBase(DeclarativeBase):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now)
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now)
class BaseMixins:
"""
`self.update` method which directly passing arguments to the `__init__`
`cls.get_ref` method which will return the object from the database or none. Useful for many-to-many relationships.
"""
def update(self, *args, **kwarg):
self.__init__(*args, **kwarg)
@classmethod
def get_ref(cls, match_value: str, match_attr: str | None = None, session: Session | None = None):
match_attr = match_attr or cls.Config.get_attr # type: ignore
if match_value is None or session is None:
return None
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)

View File

@ -2,13 +2,13 @@ from functools import wraps
from uuid import UUID
from pydantic import BaseModel, Field, NoneStr
from sqlalchemy import select
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
from sqlalchemy.orm.decl_api import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.relationships import RelationshipProperty
from sqlalchemy.sql.base import ColumnCollection
from sqlalchemy.util._collections import ImmutableProperties
from .._model_base import SqlAlchemyBase
from .helpers import safe_call
@ -26,7 +26,7 @@ class AutoInitConfig(BaseModel):
# auto_create: bool = False
def _get_config(relation_cls: DeclarativeMeta) -> AutoInitConfig:
def _get_config(relation_cls: type[SqlAlchemyBase]) -> AutoInitConfig:
"""
Returns the config for the given class.
"""
@ -45,7 +45,7 @@ def _get_config(relation_cls: DeclarativeMeta) -> AutoInitConfig:
return cfg
def get_lookup_attr(relation_cls: DeclarativeMeta) -> str:
def get_lookup_attr(relation_cls: type[SqlAlchemyBase]) -> str:
"""Returns the primary key attribute of the related class as a string.
Args:
@ -73,7 +73,9 @@ def handle_many_to_many(session, get_attr, relation_cls, all_elements: list[dict
return handle_one_to_many_list(session, get_attr, relation_cls, all_elements)
def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elements: list[dict] | list[str]):
def handle_one_to_many_list(
session: Session, get_attr, relation_cls: type[SqlAlchemyBase], all_elements: list[dict] | list[str]
):
elems_to_create: list[dict] = []
updated_elems: list[dict] = []
@ -81,16 +83,15 @@ def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elemen
for elem in all_elements:
elem_id = elem.get(get_attr, None) if isinstance(elem, dict) else elem
existing_elem = session.query(relation_cls).filter_by(**{get_attr: elem_id}).one_or_none()
stmt = select(relation_cls).filter_by(**{get_attr: elem_id})
existing_elem = session.execute(stmt).scalars().one_or_none()
is_dict = isinstance(elem, dict)
if existing_elem is None and is_dict:
elems_to_create.append(elem) # type: ignore
if existing_elem is None and isinstance(elem, dict):
elems_to_create.append(elem)
continue
elif is_dict:
for key, value in elem.items(): # type: ignore
elif isinstance(elem, dict):
for key, value in elem.items():
if key not in cfg.exclude:
setattr(existing_elem, key, value)
@ -110,7 +111,7 @@ def auto_init(): # sourcery no-metrics
def decorator(init):
@wraps(init)
def wrapper(self: DeclarativeMeta, *args, **kwargs): # sourcery no-metrics
def wrapper(self: SqlAlchemyBase, *args, **kwargs): # sourcery no-metrics
"""
Custom initializer that allows nested children initialization.
Only keys that are present as instance's class attributes are allowed.
@ -120,14 +121,14 @@ def auto_init(): # sourcery no-metrics
Ref: https://github.com/tiangolo/fastapi/issues/2194
"""
cls = self.__class__
exclude = _get_config(cls).exclude
config = _get_config(cls)
exclude = config.exclude
alchemy_mapper: Mapper = self.__mapper__
model_columns: ColumnCollection = alchemy_mapper.columns
relationships: ImmutableProperties = alchemy_mapper.relationships
relationships = alchemy_mapper.relationships
session = kwargs.get("session", None)
session: Session = kwargs.get("session", None)
if session is None:
raise ValueError("Session is required to initialize the model with `auto_init`")
@ -151,7 +152,7 @@ def auto_init(): # sourcery no-metrics
relation_dir = prop.direction
# Identifies the parent class of the related object.
relation_cls: DeclarativeMeta = prop.mapper.entity
relation_cls: type[SqlAlchemyBase] = prop.mapper.entity
# Identifies if the relationship was declared with use_list=True
use_list: bool = prop.uselist
@ -174,7 +175,8 @@ def auto_init(): # sourcery no-metrics
raise ValueError(f"Expected 'id' to be provided for {key}")
if isinstance(val, (str, int, UUID)):
instance = session.query(relation_cls).filter_by(**{get_attr: val}).one_or_none()
stmt = select(relation_cls).filter_by(**{get_attr: val})
instance = session.execute(stmt).scalars().one_or_none()
setattr(self, key, instance)
else:
# If the value is not of the type defined above we assume that it isn't a valid id

View File

@ -1,4 +1,7 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init, guid
@ -6,28 +9,33 @@ from ..recipe.category import Category, cookbooks_to_categories
from ..recipe.tag import Tag, cookbooks_to_tags
from ..recipe.tool import Tool, cookbooks_to_tools
if TYPE_CHECKING:
from group import Group
class CookBook(SqlAlchemyBase, BaseMixins):
__tablename__ = "cookbooks"
id = Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
position = Column(Integer, nullable=False, default=1)
id: Mapped[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
group_id = Column(guid.GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="cookbooks")
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"))
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="cookbooks")
name = Column(String, nullable=False)
slug = Column(String, nullable=False)
description = Column(String, default="")
public = Column(Boolean, default=False)
name: Mapped[str] = mapped_column(String, nullable=False)
slug: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[str | None] = mapped_column(String, default="")
public: Mapped[str | None] = mapped_column(Boolean, default=False)
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
require_all_categories = Column(Boolean, default=True)
categories: Mapped[list[Category]] = orm.relationship(
Category, secondary=cookbooks_to_categories, single_parent=True
)
require_all_categories: Mapped[bool | None] = mapped_column(Boolean, default=True)
tags = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
require_all_tags = Column(Boolean, default=True)
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
require_all_tags: Mapped[bool | None] = mapped_column(Boolean, default=True)
tools = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
require_all_tools = Column(Boolean, default=True)
tools: Mapped[list[Tool]] = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
require_all_tools: Mapped[bool | None] = mapped_column(Boolean, default=True)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,42 +1,48 @@
from sqlalchemy import Boolean, Column, ForeignKey, String, orm
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
if TYPE_CHECKING:
from group import Group
class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_events_notifier_options"
id = Column(GUID, primary_key=True, default=GUID.generate)
event_notifier_id = Column(GUID, ForeignKey("group_events_notifiers.id"), nullable=False)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
event_notifier_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_events_notifiers.id"), nullable=False)
recipe_created = Column(Boolean, default=False, nullable=False)
recipe_updated = Column(Boolean, default=False, nullable=False)
recipe_deleted = Column(Boolean, default=False, nullable=False)
recipe_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
recipe_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
recipe_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
user_signup = Column(Boolean, default=False, nullable=False)
user_signup: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
data_migrations = Column(Boolean, default=False, nullable=False)
data_export = Column(Boolean, default=False, nullable=False)
data_import = Column(Boolean, default=False, nullable=False)
data_migrations: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
data_export: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
data_import: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
mealplan_entry_created = Column(Boolean, default=False, nullable=False)
mealplan_entry_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
shopping_list_created = Column(Boolean, default=False, nullable=False)
shopping_list_updated = Column(Boolean, default=False, nullable=False)
shopping_list_deleted = Column(Boolean, default=False, nullable=False)
shopping_list_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
shopping_list_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
shopping_list_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
cookbook_created = Column(Boolean, default=False, nullable=False)
cookbook_updated = Column(Boolean, default=False, nullable=False)
cookbook_deleted = Column(Boolean, default=False, nullable=False)
cookbook_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
cookbook_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
cookbook_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
tag_created = Column(Boolean, default=False, nullable=False)
tag_updated = Column(Boolean, default=False, nullable=False)
tag_deleted = Column(Boolean, default=False, nullable=False)
tag_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
tag_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
tag_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
category_created = Column(Boolean, default=False, nullable=False)
category_updated = Column(Boolean, default=False, nullable=False)
category_deleted = Column(Boolean, default=False, nullable=False)
category_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
category_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
category_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
@auto_init()
def __init__(self, **_) -> None:
@ -46,15 +52,19 @@ class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_events_notifiers"
id = Column(GUID, primary_key=True, default=GUID.generate)
name = Column(String, nullable=False)
enabled = Column(Boolean, default=True, nullable=False)
apprise_url = Column(String, nullable=False)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
name: Mapped[str] = mapped_column(String, nullable=False)
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
apprise_url: Mapped[str] = mapped_column(String, nullable=False)
group = orm.relationship("Group", back_populates="group_event_notifiers", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
group: Mapped[Optional["Group"]] = orm.relationship(
"Group", back_populates="group_event_notifiers", single_parent=True
)
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
options = orm.relationship(GroupEventNotifierOptionsModel, uselist=False, cascade="all, delete-orphan")
options: Mapped[GroupEventNotifierOptionsModel] = orm.relationship(
GroupEventNotifierOptionsModel, uselist=False, cascade="all, delete-orphan"
)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,21 +1,27 @@
from sqlalchemy import Column, ForeignKey, String, orm
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
if TYPE_CHECKING:
from group import Group
class GroupDataExportsModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_data_exports"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
group = orm.relationship("Group", back_populates="data_exports", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="data_exports", single_parent=True)
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
name = Column(String, nullable=False)
filename = Column(String, nullable=False)
path = Column(String, nullable=False)
size = Column(String, nullable=False)
expires = Column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
filename: Mapped[str] = mapped_column(String, nullable=False)
path: Mapped[str] = mapped_column(String, nullable=False)
size: Mapped[str] = mapped_column(String, nullable=False)
expires: Mapped[str] = mapped_column(String, nullable=False)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,5 +1,9 @@
from typing import TYPE_CHECKING, Optional
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy import select
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_settings
@ -15,18 +19,28 @@ from .cookbook import CookBook
from .mealplan import GroupMealPlan
from .preferences import GroupPreferencesModel
if TYPE_CHECKING:
from ..recipe import IngredientFoodModel, IngredientUnitModel, RecipeModel, Tag, Tool
from ..users import User
from .events import GroupEventNotifierModel
from .exports import GroupDataExportsModel
from .report import ReportModel
from .shopping_list import ShoppingList
class Group(SqlAlchemyBase, BaseMixins):
__tablename__ = "groups"
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
categories = orm.relationship(Category, secondary=group_to_categories, single_parent=True, uselist=True)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
users: Mapped[list["User"]] = orm.relationship("User", back_populates="group")
categories: Mapped[Category] = orm.relationship(
Category, secondary=group_to_categories, single_parent=True, uselist=True
)
invite_tokens = orm.relationship(
invite_tokens: Mapped[list[GroupInviteToken]] = orm.relationship(
GroupInviteToken, back_populates="group", cascade="all, delete-orphan", uselist=True
)
preferences = orm.relationship(
preferences: Mapped[GroupPreferencesModel] = orm.relationship(
GroupPreferencesModel,
back_populates="group",
uselist=False,
@ -35,7 +49,7 @@ class Group(SqlAlchemyBase, BaseMixins):
)
# Recipes
recipes = orm.relationship("RecipeModel", back_populates="group", uselist=True)
recipes: Mapped[list["RecipeModel"]] = orm.relationship("RecipeModel", back_populates="group", uselist=True)
# CRUD From Others
common_args = {
@ -44,23 +58,26 @@ class Group(SqlAlchemyBase, BaseMixins):
"single_parent": True,
}
labels = orm.relationship(MultiPurposeLabel, **common_args)
labels: Mapped[list[MultiPurposeLabel]] = orm.relationship(MultiPurposeLabel, **common_args)
mealplans = orm.relationship(GroupMealPlan, order_by="GroupMealPlan.date", **common_args)
webhooks = orm.relationship(GroupWebhooksModel, **common_args)
cookbooks = orm.relationship(CookBook, **common_args)
server_tasks = orm.relationship(ServerTaskModel, **common_args)
data_exports = orm.relationship("GroupDataExportsModel", **common_args)
shopping_lists = orm.relationship("ShoppingList", **common_args)
group_reports = orm.relationship("ReportModel", **common_args)
group_event_notifiers = orm.relationship("GroupEventNotifierModel", **common_args)
mealplans: Mapped[list[GroupMealPlan]] = orm.relationship(
GroupMealPlan, order_by="GroupMealPlan.date", **common_args
)
webhooks: Mapped[list[GroupWebhooksModel]] = orm.relationship(GroupWebhooksModel, **common_args)
cookbooks: Mapped[list[CookBook]] = orm.relationship(CookBook, **common_args)
server_tasks: Mapped[list[ServerTaskModel]] = orm.relationship(ServerTaskModel, **common_args)
data_exports: Mapped[list["GroupDataExportsModel"]] = orm.relationship("GroupDataExportsModel", **common_args)
shopping_lists: Mapped[list["ShoppingList"]] = orm.relationship("ShoppingList", **common_args)
group_reports: Mapped[list["ReportModel"]] = orm.relationship("ReportModel", **common_args)
group_event_notifiers: Mapped[list["GroupEventNotifierModel"]] = orm.relationship(
"GroupEventNotifierModel", **common_args
)
# Owned Models
ingredient_units = orm.relationship("IngredientUnitModel", **common_args)
ingredient_foods = orm.relationship("IngredientFoodModel", **common_args)
tools = orm.relationship("Tool", **common_args)
tags = orm.relationship("Tag", **common_args)
categories = orm.relationship("Category", **common_args)
ingredient_units: Mapped[list["IngredientUnitModel"]] = orm.relationship("IngredientUnitModel", **common_args)
ingredient_foods: Mapped[list["IngredientFoodModel"]] = orm.relationship("IngredientFoodModel", **common_args)
tools: Mapped[list["Tool"]] = orm.relationship("Tool", **common_args)
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
class Config:
exclude = {
@ -79,10 +96,10 @@ class Group(SqlAlchemyBase, BaseMixins):
pass
@staticmethod # TODO: Remove this
def get_ref(session: Session, name: str): # type: ignore
def get_by_name(session: Session, name: str) -> Optional["Group"]:
settings = get_app_settings()
item = session.query(Group).filter(Group.name == name).one_or_none()
item = session.execute(select(Group).filter(Group.name == name)).scalars().one_or_none()
if item is None:
item = session.query(Group).filter(Group.name == settings.DEFAULT_GROUP).one()
item = session.execute(select(Group).filter(Group.name == settings.DEFAULT_GROUP)).scalars().one_or_none()
return item

View File

@ -1,16 +1,22 @@
from sqlalchemy import Column, ForeignKey, Integer, String, orm
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init, guid
if TYPE_CHECKING:
from group import Group
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
__tablename__ = "invite_tokens"
token = Column(String, index=True, nullable=False, unique=True)
uses_left = Column(Integer, nullable=False, default=1)
token: Mapped[str] = mapped_column(String, index=True, nullable=False, unique=True)
uses_left: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
group_id = Column(guid.GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="invite_tokens")
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"))
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="invite_tokens")
@auto_init()
def __init__(self, **_):

View File

@ -1,4 +1,8 @@
from sqlalchemy import Column, Date, ForeignKey, String, orm
from datetime import date
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Date, ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
@ -6,18 +10,27 @@ from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
from ..recipe.category import Category, plan_rules_to_categories
if TYPE_CHECKING:
from group import Group
from ..recipe import RecipeModel
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
__tablename__ = "group_meal_plan_rules"
id = Column(GUID, primary_key=True, default=GUID.generate)
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
day = Column(String, nullable=False, default="unset") # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
entry_type = Column(String, nullable=False, default="") # "breakfast", "lunch", "dinner", "side"
day: Mapped[str] = mapped_column(
String, nullable=False, default="unset"
) # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
entry_type: Mapped[str] = mapped_column(
String, nullable=False, default=""
) # "breakfast", "lunch", "dinner", "side"
categories = orm.relationship(Category, secondary=plan_rules_to_categories, uselist=True)
tags = orm.relationship(Tag, secondary=plan_rules_to_tags, uselist=True)
categories: Mapped[Category] = orm.relationship(Category, secondary=plan_rules_to_categories, uselist=True)
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags, uselist=True)
@auto_init()
def __init__(self, **_) -> None:
@ -27,16 +40,18 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_meal_plans"
date = Column(Date, index=True, nullable=False)
entry_type = Column(String, index=True, nullable=False)
title = Column(String, index=True, nullable=False)
text = Column(String, nullable=False)
date: Mapped[date] = mapped_column(Date, index=True, nullable=False)
entry_type: Mapped[str] = mapped_column(String, index=True, nullable=False)
title: Mapped[str] = mapped_column(String, index=True, nullable=False)
text: Mapped[str] = mapped_column(String, nullable=False)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
group = orm.relationship("Group", back_populates="mealplans")
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="mealplans")
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
recipe = orm.relationship("RecipeModel", back_populates="meal_entries", uselist=False)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
"RecipeModel", back_populates="meal_entries", uselist=False
)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,29 +1,34 @@
from typing import TYPE_CHECKING, Optional
import sqlalchemy as sa
import sqlalchemy.orm as orm
from mealie.db.models._model_utils.guid import GUID
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils.guid import GUID
if TYPE_CHECKING:
from group import Group
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_preferences"
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="preferences")
group_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="preferences")
private_group: bool = sa.Column(sa.Boolean, default=True)
first_day_of_week = sa.Column(sa.Integer, default=0)
private_group: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
# Recipe Defaults
recipe_public: bool = sa.Column(sa.Boolean, default=True)
recipe_show_nutrition: bool = sa.Column(sa.Boolean, default=False)
recipe_show_assets: bool = sa.Column(sa.Boolean, default=False)
recipe_landscape_view: bool = sa.Column(sa.Boolean, default=False)
recipe_disable_comments: bool = sa.Column(sa.Boolean, default=False)
recipe_disable_amount: bool = sa.Column(sa.Boolean, default=True)
recipe_public: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
recipe_show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,6 +1,8 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Column, ForeignKey, orm
from sqlalchemy import ForeignKey, orm
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
@ -8,18 +10,21 @@ from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils.guid import GUID
if TYPE_CHECKING:
from group import Group
class ReportEntryModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "report_entries"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
success = Column(Boolean, default=False)
message = Column(String, nullable=True)
exception = Column(String, nullable=True)
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
success: Mapped[bool | None] = mapped_column(Boolean, default=False)
message: Mapped[str] = mapped_column(String, nullable=True)
exception: Mapped[str] = mapped_column(String, nullable=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
report_id = Column(GUID, ForeignKey("group_reports.id"), nullable=False)
report = orm.relationship("ReportModel", back_populates="entries")
report_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_reports.id"), nullable=False)
report: Mapped["ReportModel"] = orm.relationship("ReportModel", back_populates="entries")
@auto_init()
def __init__(self, **_) -> None:
@ -28,18 +33,20 @@ class ReportEntryModel(SqlAlchemyBase, BaseMixins):
class ReportModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_reports"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
name = Column(String, nullable=False)
status = Column(String, nullable=False)
category = Column(String, index=True, nullable=False)
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
name: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False)
category: Mapped[str] = mapped_column(String, index=True, nullable=False)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
entries = orm.relationship(ReportEntryModel, back_populates="report", cascade="all, delete-orphan")
entries: Mapped[list[ReportEntryModel]] = orm.relationship(
ReportEntryModel, back_populates="report", cascade="all, delete-orphan"
)
# Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="group_reports", single_parent=True)
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="group_reports", single_parent=True)
class Config:
exclude = ["entries"]

View File

@ -1,5 +1,8 @@
from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, orm
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, orm
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.api_extras import ShoppingListExtras, ShoppingListItemExtras, api_extras
@ -8,16 +11,21 @@ from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
if TYPE_CHECKING:
from group import Group
from ..recipe import RecipeModel
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
__tablename__ = "shopping_list_item_recipe_reference"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
shopping_list_item_id = Column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
recipe_quantity = Column(Float, nullable=False)
recipe_scale = Column(Float, nullable=False, default=1)
shopping_list_item_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
recipe_scale: Mapped[float | None] = mapped_column(Float, default=1)
@auto_init()
def __init__(self, **_) -> None:
@ -28,32 +36,38 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
__tablename__ = "shopping_list_items"
# Id's
id = Column(GUID, primary_key=True, default=GUID.generate)
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"))
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("shopping_lists.id"))
# Meta
is_ingredient = Column(Boolean, default=True)
position = Column(Integer, nullable=False, default=0)
checked = Column(Boolean, default=False)
is_ingredient: Mapped[bool | None] = mapped_column(Boolean, default=True)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
checked: Mapped[bool | None] = mapped_column(Boolean, default=False)
quantity = Column(Float, default=1)
note = Column(String)
quantity: Mapped[float | None] = mapped_column(Float, default=1)
note: Mapped[str | None] = mapped_column(String)
is_food = Column(Boolean, default=False)
extras: list[ShoppingListItemExtras] = orm.relationship("ShoppingListItemExtras", cascade="all, delete-orphan")
is_food: Mapped[bool | None] = mapped_column(Boolean, default=False)
extras: Mapped[list[ShoppingListItemExtras]] = orm.relationship(
"ShoppingListItemExtras", cascade="all, delete-orphan"
)
# Scaling Items
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
unit = orm.relationship(IngredientUnitModel, uselist=False)
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
food = orm.relationship(IngredientFoodModel, uselist=False)
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="shopping_list_items")
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
label: Mapped[MultiPurposeLabel | None] = orm.relationship(
MultiPurposeLabel, uselist=False, back_populates="shopping_list_items"
)
# Recipe Reference
recipe_references = orm.relationship(ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan")
recipe_references: Mapped[list[ShoppingListItemRecipeReference]] = orm.relationship(
ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan"
)
class Config:
exclude = {"id", "label", "food", "unit"}
@ -66,14 +80,16 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
__tablename__ = "shopping_list_recipe_reference"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
shopping_list_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
recipe = orm.relationship("RecipeModel", uselist=False, back_populates="shopping_list_refs")
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
"RecipeModel", uselist=False, back_populates="shopping_list_refs"
)
recipe_quantity = Column(Float, nullable=False)
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
class Config:
exclude = {"id", "recipe"}
@ -85,21 +101,23 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
class ShoppingList(SqlAlchemyBase, BaseMixins):
__tablename__ = "shopping_lists"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="shopping_lists")
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists")
name = Column(String)
list_items = orm.relationship(
name: Mapped[str | None] = mapped_column(String)
list_items: Mapped[ShoppingListItem] = orm.relationship(
ShoppingListItem,
cascade="all, delete, delete-orphan",
order_by="ShoppingListItem.position",
collection_class=ordering_list("position"),
)
recipe_references = orm.relationship(ShoppingListRecipeReference, cascade="all, delete, delete-orphan")
extras: list[ShoppingListExtras] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
recipe_references: Mapped[ShoppingListRecipeReference] = orm.relationship(
ShoppingListRecipeReference, cascade="all, delete, delete-orphan"
)
extras: Mapped[list[ShoppingListExtras]] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
class Config:
exclude = {"id", "list_items"}

View File

@ -1,29 +1,34 @@
from datetime import datetime
from datetime import datetime, time
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, Column, ForeignKey, String, Time, orm
from sqlalchemy import Boolean, ForeignKey, String, Time, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
if TYPE_CHECKING:
from group import Group
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "webhook_urls"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
group = orm.relationship("Group", back_populates="webhooks", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="webhooks", single_parent=True)
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
enabled = Column(Boolean, default=False)
name = Column(String)
url = Column(String)
enabled: Mapped[bool | None] = mapped_column(Boolean, default=False)
name: Mapped[str | None] = mapped_column(String)
url: Mapped[str | None] = mapped_column(String)
# New Fields
webhook_type = Column(String, default="") # Future use for different types of webhooks
scheduled_time = Column(Time, default=lambda: datetime.now().time())
webhook_type: Mapped[str | None] = mapped_column(String, default="") # Future use for different types of webhooks
scheduled_time: Mapped[time | None] = mapped_column(Time, default=lambda: datetime.now().time())
# Columne is no longer used but is kept for since it's super annoying to
# delete a column in SQLite and it's not a big deal to keep it around
time = Column(String, default="00:00")
time: Mapped[str | None] = mapped_column(String, default="00:00")
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,22 +1,29 @@
from sqlalchemy import Column, ForeignKey, String, orm
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from ._model_utils import auto_init
from ._model_utils.guid import GUID
if TYPE_CHECKING:
from group import Group, ShoppingListItem
from recipe import IngredientFoodModel
class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
__tablename__ = "multi_purpose_labels"
id = Column(GUID, default=GUID.generate, primary_key=True)
name = Column(String(255), nullable=False)
color = Column(String(10), nullable=False, default="")
id: Mapped[GUID] = mapped_column(GUID, default=GUID.generate, primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
color: Mapped[str] = mapped_column(String(10), nullable=False, default="")
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="labels")
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="labels")
shopping_list_items = orm.relationship("ShoppingListItem", back_populates="label")
foods = orm.relationship("IngredientFoodModel", back_populates="label")
shopping_list_items: Mapped["ShoppingListItem"] = orm.relationship("ShoppingListItem", back_populates="label")
foods: Mapped["IngredientFoodModel"] = orm.relationship("IngredientFoodModel", back_populates="label")
@auto_init()
def __init__(self, **_) -> None:

View File

@ -0,0 +1,14 @@
from .api_extras import *
from .assets import *
from .category import *
from .comment import *
from .ingredient import *
from .instruction import *
from .note import *
from .nutrition import *
from .recipe import *
from .recipe_timeline import *
from .settings import *
from .shared import *
from .tag import *
from .tool import *

View File

@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
@ -27,9 +28,9 @@ class ExtrasGeneric:
This class is not an actual table, so it does not inherit from SqlAlchemyBase
"""
id = sa.Column(sa.Integer, primary_key=True)
key_name = sa.Column(sa.String)
value = sa.Column(sa.String)
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
key_name: Mapped[str | None] = mapped_column(sa.String)
value: Mapped[str | None] = mapped_column(sa.String)
def __init__(self, key, value) -> None:
self.key_name = key
@ -39,19 +40,19 @@ class ExtrasGeneric:
# used specifically for recipe extras
class ApiExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "api_extras"
recipee_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
recipee_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
class IngredientFoodExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "ingredient_food_extras"
ingredient_food_id = sa.Column(GUID, sa.ForeignKey("ingredient_foods.id"))
ingredient_food_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("ingredient_foods.id"))
class ShoppingListExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "shopping_list_extras"
shopping_list_id = sa.Column(GUID, sa.ForeignKey("shopping_lists.id"))
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("shopping_lists.id"))
class ShoppingListItemExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "shopping_list_item_extras"
shopping_list_item_id = sa.Column(GUID, sa.ForeignKey("shopping_list_items.id"))
shopping_list_item_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("shopping_list_items.id"))

View File

@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
@ -6,11 +7,11 @@ from mealie.db.models._model_utils.guid import GUID
class RecipeAsset(SqlAlchemyBase):
__tablename__ = "recipe_assets"
id = sa.Column(sa.Integer, primary_key=True)
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
name = sa.Column(sa.String)
icon = sa.Column(sa.String)
file_name = sa.Column(sa.String)
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
name: Mapped[str | None] = mapped_column(sa.String)
icon: Mapped[str | None] = mapped_column(sa.String)
file_name: Mapped[str | None] = mapped_column(sa.String)
def __init__(self, name=None, icon=None, file_name=None) -> None:
self.name = name

View File

@ -1,13 +1,18 @@
from typing import TYPE_CHECKING
import sqlalchemy as sa
import sqlalchemy.orm as orm
from slugify import slugify
from sqlalchemy.orm import validates
from sqlalchemy.orm import Mapped, mapped_column, validates
from mealie.core import root_logger
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils.guid import GUID
if TYPE_CHECKING:
from ..group import Group
from . import RecipeModel
logger = root_logger.get_logger()
@ -45,13 +50,15 @@ class Category(SqlAlchemyBase, BaseMixins):
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="category_slug_group_id_key"),)
# ID Relationships
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, nullable=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category")
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category"
)
@validates("name")
def validate_name(self, key, name):
@ -62,18 +69,3 @@ class Category(SqlAlchemyBase, BaseMixins):
self.group_id = group_id
self.name = name.strip()
self.slug = slugify(name)
@classmethod # TODO: Remove this
def get_ref(cls, match_value: str, session=None): # type: ignore
if not session or not match_value:
return None
slug = slugify(match_value)
result = session.query(Category).filter(Category.slug == slug).one_or_none()
if result:
logger.debug("Category exists, associating recipe")
return result
else:
logger.debug("Category doesn't exists, creating Category")
return Category(name=match_value) # type: ignore

View File

@ -1,26 +1,35 @@
from sqlalchemy import Column, ForeignKey, String, orm
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils import auto_init
from mealie.db.models._model_utils.guid import GUID
if TYPE_CHECKING:
from ..users import User
from . import RecipeModel
class RecipeComment(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_comments"
id = Column(GUID, primary_key=True, default=GUID.generate)
text = Column(String)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
text: Mapped[str | None] = mapped_column(String)
# Recipe Link
recipe_id = Column(GUID, ForeignKey("recipes.id"), nullable=False)
recipe = orm.relationship("RecipeModel", back_populates="comments")
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False)
recipe: Mapped["RecipeModel"] = orm.relationship("RecipeModel", back_populates="comments")
# User Link
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
user = orm.relationship("User", back_populates="comments", single_parent=True, foreign_keys=[user_id])
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
user: Mapped["User"] = orm.relationship(
"User", back_populates="comments", single_parent=True, foreign_keys=[user_id]
)
@auto_init()
def __init__(self, **_) -> None:
pass
def update(self, text, **_) -> None: # type: ignore
def update(self, text, **_) -> None:
self.text = text

View File

@ -1,4 +1,7 @@
from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, orm
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.labels import MultiPurposeLabel
@ -7,21 +10,24 @@ from mealie.db.models.recipe.api_extras import IngredientFoodExtras, api_extras
from .._model_utils import auto_init
from .._model_utils.guid import GUID
if TYPE_CHECKING:
from ..group import Group
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "ingredient_units"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
# ID Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
group = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
name = Column(String)
description = Column(String)
abbreviation = Column(String)
use_abbreviation = Column(Boolean, default=False)
fraction = Column(Boolean, default=True)
ingredients = orm.relationship("RecipeIngredient", back_populates="unit")
name: Mapped[str | None] = mapped_column(String)
description: Mapped[str | None] = mapped_column(String)
abbreviation: Mapped[str | None] = mapped_column(String)
use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False)
fraction: Mapped[bool | None] = mapped_column(Boolean, default=True)
ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="unit")
@auto_init()
def __init__(self, **_) -> None:
@ -30,19 +36,19 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "ingredient_foods"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
# ID Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
group = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
name = Column(String)
description = Column(String)
ingredients = orm.relationship("RecipeIngredient", back_populates="food")
extras: list[IngredientFoodExtras] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
name: Mapped[str | None] = mapped_column(String)
description: Mapped[str | None] = mapped_column(String)
ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="food")
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
label: Mapped[MultiPurposeLabel | None] = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
@api_extras
@auto_init()
@ -52,24 +58,24 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
class RecipeIngredient(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipes_ingredients"
id = Column(Integer, primary_key=True)
position = Column(Integer)
recipe_id = Column(GUID, ForeignKey("recipes.id"))
id: Mapped[int] = mapped_column(Integer, primary_key=True)
position: Mapped[int | None] = mapped_column(Integer)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
title = Column(String) # Section Header - Shows if Present
note = Column(String) # Force Show Text - Overrides Concat
title: Mapped[str | None] = mapped_column(String) # Section Header - Shows if Present
note: Mapped[str | None] = mapped_column(String) # Force Show Text - Overrides Concat
# Scaling Items
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
unit = orm.relationship(IngredientUnitModel, uselist=False)
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
food = orm.relationship(IngredientFoodModel, uselist=False)
quantity = Column(Float)
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
quantity: Mapped[float | None] = mapped_column(Float)
original_text = Column(String)
original_text: Mapped[str | None] = mapped_column(String)
reference_id = Column(GUID) # Reference Links
reference_id: Mapped[GUID | None] = mapped_column(GUID) # Reference Links
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,4 +1,5 @@
from sqlalchemy import Column, ForeignKey, Integer, String, orm
from sqlalchemy import ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
@ -7,8 +8,8 @@ from .._model_utils.guid import GUID
class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_ingredient_ref_link"
instruction_id = Column(GUID, ForeignKey("recipe_instructions.id"))
reference_id = Column(GUID)
instruction_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipe_instructions.id"))
reference_id: Mapped[GUID | None] = mapped_column(GUID)
@auto_init()
def __init__(self, **_) -> None:
@ -17,14 +18,16 @@ class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = Column(GUID, primary_key=True, default=GUID.generate)
recipe_id = Column(GUID, ForeignKey("recipes.id"))
position = Column(Integer)
type = Column(String, default="")
title = Column(String)
text = Column(String)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
position: Mapped[int | None] = mapped_column(Integer)
type: Mapped[str | None] = mapped_column(String, default="")
title: Mapped[str | None] = mapped_column(String)
text: Mapped[str | None] = mapped_column(String)
ingredient_references = orm.relationship("RecipeIngredientRefLink", cascade="all, delete-orphan")
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
RecipeIngredientRefLink, cascade="all, delete-orphan"
)
class Config:
exclude = {

View File

@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
@ -6,10 +7,10 @@ from mealie.db.models._model_utils.guid import GUID
class Note(SqlAlchemyBase):
__tablename__ = "notes"
id = sa.Column(sa.Integer, primary_key=True)
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
title = sa.Column(sa.String)
text = sa.Column(sa.String)
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
title: Mapped[str | None] = mapped_column(sa.String)
text: Mapped[str | None] = mapped_column(sa.String)
def __init__(self, title, text) -> None:
self.title = title

View File

@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
@ -6,15 +7,15 @@ from mealie.db.models._model_utils.guid import GUID
class Nutrition(SqlAlchemyBase):
__tablename__ = "recipe_nutrition"
id = sa.Column(sa.Integer, primary_key=True)
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
calories = sa.Column(sa.String)
fat_content = sa.Column(sa.String)
fiber_content = sa.Column(sa.String)
protein_content = sa.Column(sa.String)
carbohydrate_content = sa.Column(sa.String)
sodium_content = sa.Column(sa.String)
sugar_content = sa.Column(sa.String)
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
calories: Mapped[str | None] = mapped_column(sa.String)
fat_content: Mapped[str | None] = mapped_column(sa.String)
fiber_content: Mapped[str | None] = mapped_column(sa.String)
protein_content: Mapped[str | None] = mapped_column(sa.String)
carbohydrate_content: Mapped[str | None] = mapped_column(sa.String)
sodium_content: Mapped[str | None] = mapped_column(sa.String)
sugar_content: Mapped[str | None] = mapped_column(sa.String)
def __init__(
self,

View File

@ -1,9 +1,10 @@
import datetime
from datetime import date, datetime
from typing import TYPE_CHECKING
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
from sqlalchemy.orm import Mapped, mapped_column, validates
from mealie.db.models._model_utils.guid import GUID
@ -24,90 +25,103 @@ from .shared import RecipeShareTokenModel
from .tag import recipes_to_tags
from .tool import recipes_to_tools
if TYPE_CHECKING:
from ..group import Group, GroupMealPlan, ShoppingListItemRecipeReference, ShoppingListRecipeReference
from ..users import User
from . import Category, Tag, Tool
class RecipeModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipes"
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
slug = sa.Column(sa.String, index=True)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
slug: Mapped[str | None] = mapped_column(sa.String, index=True)
# ID Relationships
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
user_id = sa.Column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
user_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
user: Mapped["User"] = orm.relationship("User", uselist=False, foreign_keys=[user_id])
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan")
meal_entries: Mapped["GroupMealPlan"] = orm.relationship(
"GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan"
)
favorited_by = orm.relationship("User", secondary=users_to_favorites, back_populates="favorite_recipes")
favorited_by: Mapped[list["User"]] = orm.relationship(
"User", secondary=users_to_favorites, back_populates="favorite_recipes"
)
# General Recipe Properties
name = sa.Column(sa.String, nullable=False)
description = sa.Column(sa.String)
image = sa.Column(sa.String)
name: Mapped[str] = mapped_column(sa.String, nullable=False)
description: Mapped[str | None] = mapped_column(sa.String)
image: Mapped[str | None] = mapped_column(sa.String)
# Time Related Properties
total_time = sa.Column(sa.String)
prep_time = sa.Column(sa.String)
perform_time = sa.Column(sa.String)
cook_time = sa.Column(sa.String)
total_time: Mapped[str | None] = mapped_column(sa.String)
prep_time: Mapped[str | None] = mapped_column(sa.String)
perform_time: Mapped[str | None] = mapped_column(sa.String)
cook_time: Mapped[str | None] = mapped_column(sa.String)
recipe_yield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
recipe_yield: Mapped[str | None] = mapped_column(sa.String)
recipeCuisine: Mapped[str | None] = mapped_column(sa.String)
assets = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
recipe_category = orm.relationship("Category", secondary=recipes_to_categories, back_populates="recipes")
tools = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes")
assets: Mapped[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
nutrition: Mapped[Nutrition] = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
recipe_category: Mapped[list["Category"]] = orm.relationship(
"Category", secondary=recipes_to_categories, back_populates="recipes"
)
tools: Mapped[list["Tool"]] = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes")
recipe_ingredient: list[RecipeIngredient] = orm.relationship(
recipe_ingredient: Mapped[list[RecipeIngredient]] = orm.relationship(
"RecipeIngredient",
cascade="all, delete-orphan",
order_by="RecipeIngredient.position",
collection_class=ordering_list("position"),
)
recipe_instructions: list[RecipeInstruction] = orm.relationship(
recipe_instructions: Mapped[list[RecipeInstruction]] = orm.relationship(
"RecipeInstruction",
cascade="all, delete-orphan",
order_by="RecipeInstruction.position",
collection_class=ordering_list("position"),
)
share_tokens = orm.relationship(
share_tokens: Mapped[list[RecipeShareTokenModel]] = orm.relationship(
RecipeShareTokenModel, back_populates="recipe", cascade="all, delete, delete-orphan"
)
comments: list[RecipeComment] = orm.relationship(
comments: Mapped[list[RecipeComment]] = orm.relationship(
"RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan"
)
timeline_events: list[RecipeTimelineEvent] = orm.relationship(
timeline_events: Mapped[list[RecipeTimelineEvent]] = orm.relationship(
"RecipeTimelineEvent", back_populates="recipe", cascade="all, delete, delete-orphan"
)
# Mealie Specific
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
tags = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
rating = sa.Column(sa.Integer)
org_url = sa.Column(sa.String)
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
is_ocr_recipe = sa.Column(sa.Boolean, default=False)
settings: Mapped[list["RecipeSettings"]] = orm.relationship(
"RecipeSettings", uselist=False, cascade="all, delete-orphan"
)
tags: Mapped[list["Tag"]] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
notes: Mapped[list[Note]] = orm.relationship("Note", cascade="all, delete-orphan")
rating: Mapped[int | None] = mapped_column(sa.Integer)
org_url: Mapped[str | None] = mapped_column(sa.String)
extras: Mapped[list[ApiExtras]] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
is_ocr_recipe: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
# Time Stamp Properties
date_added = sa.Column(sa.Date, default=datetime.date.today)
date_updated = sa.Column(sa.DateTime)
last_made = sa.Column(sa.DateTime)
date_added: Mapped[date | None] = mapped_column(sa.Date, default=date.today)
date_updated: Mapped[datetime | None] = mapped_column(sa.DateTime)
last_made: Mapped[datetime | None] = mapped_column(sa.DateTime)
# Shopping List Refs
shopping_list_refs = orm.relationship(
shopping_list_refs: Mapped[list["ShoppingListRecipeReference"]] = orm.relationship(
"ShoppingListRecipeReference",
back_populates="recipe",
cascade="all, delete-orphan",
)
shopping_list_item_refs = orm.relationship(
shopping_list_item_refs: Mapped[list["ShoppingListItemRecipeReference"]] = orm.relationship(
"ShoppingListItemRecipeReference",
back_populates="recipe",
cascade="all, delete-orphan",
@ -160,4 +174,4 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
if notes:
self.notes = [Note(**n) for n in notes]
self.date_updated = datetime.datetime.now()
self.date_updated = datetime.now()

View File

@ -1,33 +1,40 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, ForeignKey, String
from sqlalchemy.orm import relationship
from sqlalchemy import DateTime, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils.guid import GUID
if TYPE_CHECKING:
from ..users import User
from . import RecipeModel
class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_timeline_events"
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
# Parent Recipe
recipe_id = Column(GUID, ForeignKey("recipes.id"), nullable=False)
recipe = relationship("RecipeModel", back_populates="timeline_events")
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False)
recipe: Mapped["RecipeModel"] = relationship("RecipeModel", back_populates="timeline_events")
# Related User (Actor)
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
user = relationship("User", back_populates="recipe_timeline_events", single_parent=True, foreign_keys=[user_id])
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
user: Mapped["User"] = relationship(
"User", back_populates="recipe_timeline_events", single_parent=True, foreign_keys=[user_id]
)
# General Properties
subject = Column(String, nullable=False)
message = Column(String)
event_type = Column(String)
image = Column(String)
subject: Mapped[str] = mapped_column(String, nullable=False)
message: Mapped[str | None] = mapped_column(String)
event_type: Mapped[str | None] = mapped_column(String)
image: Mapped[str | None] = mapped_column(String)
# Timestamps
timestamp = Column(DateTime)
timestamp: Mapped[datetime | None] = mapped_column(DateTime)
@auto_init()
def __init__(

View File

@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
@ -6,15 +7,15 @@ from mealie.db.models._model_utils.guid import GUID
class RecipeSettings(SqlAlchemyBase):
__tablename__ = "recipe_settings"
id = sa.Column(sa.Integer, primary_key=True)
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
public = sa.Column(sa.Boolean)
show_nutrition = sa.Column(sa.Boolean)
show_assets = sa.Column(sa.Boolean)
landscape_view = sa.Column(sa.Boolean)
disable_amount = sa.Column(sa.Boolean, default=True)
disable_comments = sa.Column(sa.Boolean, default=False)
locked = sa.Column(sa.Boolean, default=False)
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
public: Mapped[bool | None] = mapped_column(sa.Boolean)
show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean)
show_assets: Mapped[bool | None] = mapped_column(sa.Boolean)
landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean)
disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
locked: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
def __init__(
self,

View File

@ -1,11 +1,16 @@
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from uuid import uuid4
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils import GUID, auto_init
if TYPE_CHECKING:
from . import RecipeModel
def defaut_expires_at_time() -> datetime:
return datetime.utcnow() + timedelta(days=30)
@ -13,14 +18,14 @@ def defaut_expires_at_time() -> datetime:
class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_share_tokens"
id = sa.Column(GUID, primary_key=True, default=uuid4)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=uuid4)
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"), nullable=False)
recipe = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
recipe_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("recipes.id"), nullable=False)
recipe: Mapped["RecipeModel"] = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
expires_at = sa.Column(sa.DateTime, nullable=False)
expires_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,12 +1,19 @@
from typing import TYPE_CHECKING
import sqlalchemy as sa
import sqlalchemy.orm as orm
from slugify import slugify
from sqlalchemy.orm import validates
from sqlalchemy.orm import Mapped, mapped_column, validates
from mealie.core import root_logger
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils import guid
if TYPE_CHECKING:
from ..group import Group
from . import RecipeModel
logger = root_logger.get_logger()
recipes_to_tags = sa.Table(
@ -34,15 +41,17 @@ cookbooks_to_tags = sa.Table(
class Tag(SqlAlchemyBase, BaseMixins):
__tablename__ = "tags"
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
id: Mapped[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
# ID Relationships
group_id = sa.Column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
group_id: Mapped[guid.GUID] = mapped_column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, nullable=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tags, back_populates="tags")
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=recipes_to_tags, back_populates="tags"
)
@validates("name")
def validate_name(self, key, name):
@ -53,17 +62,3 @@ class Tag(SqlAlchemyBase, BaseMixins):
self.group_id = group_id
self.name = name.strip()
self.slug = slugify(self.name)
@classmethod # TODO: Remove this
def get_ref(cls, match_value: str, session=None): # type: ignore
if not session or not match_value:
return None
slug = slugify(match_value)
if result := session.query(Tag).filter(Tag.slug == slug).one_or_none():
logger.debug("Category exists, associating recipe")
return result
else:
logger.debug("Category doesn't exists, creating Category")
return Tag(name=match_value) # type: ignore

View File

@ -1,10 +1,17 @@
from typing import TYPE_CHECKING
from slugify import slugify
from sqlalchemy import Boolean, Column, ForeignKey, String, Table, UniqueConstraint, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils import auto_init
from mealie.db.models._model_utils.guid import GUID
if TYPE_CHECKING:
from ..group import Group
from . import RecipeModel
recipes_to_tools = Table(
"recipes_to_tools",
SqlAlchemyBase.metadata,
@ -23,16 +30,18 @@ cookbooks_to_tools = Table(
class Tool(SqlAlchemyBase, BaseMixins):
__tablename__ = "tools"
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
id = Column(GUID, primary_key=True, default=GUID.generate)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
# ID Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
group = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
group: Mapped["Group"] = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
name = Column(String, index=True, unique=True, nullable=False)
slug = Column(String, index=True, unique=True, nullable=False)
on_hand = Column(Boolean, default=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools")
name: Mapped[str] = mapped_column(String, index=True, unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String, index=True, unique=True, nullable=False)
on_hand: Mapped[bool | None] = mapped_column(Boolean, default=False)
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=recipes_to_tools, back_populates="tools"
)
@auto_init()
def __init__(self, name, **_) -> None:

View File

@ -1,20 +1,27 @@
from sqlalchemy import Column, DateTime, ForeignKey, String, orm
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
from .._model_utils import auto_init
if TYPE_CHECKING:
from ..group import Group
class ServerTaskModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "server_tasks"
name = Column(String, nullable=False)
completed_date = Column(DateTime, nullable=True)
status = Column(String, nullable=False)
log = Column(String, nullable=True)
name: Mapped[str] = mapped_column(String, nullable=False)
completed_date: Mapped[datetime] = mapped_column(DateTime, nullable=True)
status: Mapped[str] = mapped_column(String, nullable=False)
log: Mapped[str] = mapped_column(String, nullable=True)
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="server_tasks")
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="server_tasks")
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,15 +1,21 @@
from sqlalchemy import Column, ForeignKey, String, orm
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID
if TYPE_CHECKING:
from .users import User
class PasswordResetModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "password_reset_tokens"
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
user = orm.relationship("User", back_populates="password_reset_tokens", uselist=False)
token = Column(String(64), unique=True, nullable=False)
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
user: Mapped["User"] = orm.relationship("User", back_populates="password_reset_tokens", uselist=False)
token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
def __init__(self, user_id, token, **_):
self.user_id = user_id

View File

@ -1,4 +1,8 @@
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, orm
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.core.config import get_app_settings
from mealie.db.models._model_utils.guid import GUID
@ -7,14 +11,19 @@ from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .user_to_favorite import users_to_favorites
if TYPE_CHECKING:
from ..group import Group
from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent
from .password_reset import PasswordResetModel
class LongLiveToken(SqlAlchemyBase, BaseMixins):
__tablename__ = "long_live_tokens"
name = Column(String, nullable=False)
token = Column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
token: Mapped[str] = mapped_column(String, nullable=False)
user_id = Column(GUID, ForeignKey("users.id"))
user = orm.relationship("User")
user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"))
user: Mapped[Optional["User"]] = orm.relationship("User")
def __init__(self, name, token, user_id, **_) -> None:
self.name = name
@ -24,25 +33,25 @@ class LongLiveToken(SqlAlchemyBase, BaseMixins):
class User(SqlAlchemyBase, BaseMixins):
__tablename__ = "users"
id = Column(GUID, primary_key=True, default=GUID.generate)
full_name = Column(String, index=True)
username = Column(String, index=True, unique=True)
email = Column(String, unique=True, index=True)
password = Column(String)
admin = Column(Boolean, default=False)
advanced = Column(Boolean, default=False)
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
full_name: Mapped[str | None] = mapped_column(String, index=True)
username: Mapped[str | None] = mapped_column(String, index=True, unique=True)
email: Mapped[str | None] = mapped_column(String, unique=True, index=True)
password: Mapped[str | None] = mapped_column(String)
admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
advanced: Mapped[bool | None] = mapped_column(Boolean, default=False)
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="users")
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="users")
cache_key = Column(String, default="1234")
login_attemps = Column(Integer, default=0)
locked_at = Column(DateTime, default=None)
cache_key: Mapped[str | None] = mapped_column(String, default="1234")
login_attemps: Mapped[int | None] = mapped_column(Integer, default=0)
locked_at: Mapped[datetime | None] = mapped_column(DateTime, default=None)
# Group Permissions
can_manage = Column(Boolean, default=False)
can_invite = Column(Boolean, default=False)
can_organize = Column(Boolean, default=False)
can_manage: Mapped[bool | None] = mapped_column(Boolean, default=False)
can_invite: Mapped[bool | None] = mapped_column(Boolean, default=False)
can_organize: Mapped[bool | None] = mapped_column(Boolean, default=False)
sp_args = {
"back_populates": "user",
@ -50,15 +59,19 @@ class User(SqlAlchemyBase, BaseMixins):
"single_parent": True,
}
tokens = orm.relationship(LongLiveToken, **sp_args)
comments = orm.relationship("RecipeComment", **sp_args)
recipe_timeline_events = orm.relationship("RecipeTimelineEvent", **sp_args)
password_reset_tokens = orm.relationship("PasswordResetModel", **sp_args)
tokens: Mapped[list[LongLiveToken]] = orm.relationship(LongLiveToken, **sp_args)
comments: Mapped[list["RecipeComment"]] = orm.relationship("RecipeComment", **sp_args)
recipe_timeline_events: Mapped[list["RecipeTimelineEvent"]] = orm.relationship("RecipeTimelineEvent", **sp_args)
password_reset_tokens: Mapped[list["PasswordResetModel"]] = orm.relationship("PasswordResetModel", **sp_args)
owned_recipes_id = Column(GUID, ForeignKey("recipes.id"))
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
owned_recipes_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
owned_recipes: Mapped[Optional["RecipeModel"]] = orm.relationship(
"RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id]
)
favorite_recipes = orm.relationship("RecipeModel", secondary=users_to_favorites, back_populates="favorited_by")
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
)
class Config:
exclude = {
@ -78,7 +91,7 @@ class User(SqlAlchemyBase, BaseMixins):
from mealie.db.models.group import Group
self.group = Group.get_ref(session, group)
self.group = Group.get_by_name(session, group)
self.favorite_recipes = []
@ -97,7 +110,7 @@ class User(SqlAlchemyBase, BaseMixins):
from mealie.db.models.group import Group
self.group = Group.get_ref(session, group)
self.group = Group.get_by_name(session, group)
if self.username is None:
self.username = full_name
@ -126,7 +139,3 @@ class User(SqlAlchemyBase, BaseMixins):
self.can_manage = can_manage
self.can_invite = can_invite
self.can_organize = can_organize
@staticmethod # TODO: Remove This
def get_ref(session, id: str): # type: ignore
return session.query(User).filter(User.id == id).one()

View File

@ -12,11 +12,11 @@ def pretty_size(size: int) -> str:
"""
if size < 1024:
return f"{size} bytes"
elif size < 1024 ** 2:
elif size < 1024**2:
return f"{round(size / 1024, 2)} KB"
elif size < 1024 ** 2 * 1024:
elif size < 1024**2 * 1024:
return f"{round(size / 1024 / 1024, 2)} MB"
elif size < 1024 ** 2 * 1024 * 1024:
elif size < 1024**2 * 1024 * 1024:
return f"{round(size / 1024 / 1024 / 1024, 2)} GB"
else:
return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"

View File

@ -1,5 +1,7 @@
from collections.abc import Sequence
from functools import cached_property
from sqlalchemy import select
from sqlalchemy.orm import Session
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
@ -70,13 +72,16 @@ PK_GROUP_ID = "group_id"
class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]):
def get_empty(self):
return self.session.query(Category).filter(~Category.recipes.any()).all()
def get_empty(self) -> Sequence[Category]:
stmt = select(Category).filter(~Category.recipes.any())
return self.session.execute(stmt).scalars().all()
class RepositoryTags(RepositoryGeneric[TagOut, Tag]):
def get_empty(self):
return self.session.query(Tag).filter(~Tag.recipes.any()).all()
def get_empty(self) -> Sequence[Tag]:
stmt = select(Tag).filter(~Tag.recipes.any())
return self.session.execute(stmt).scalars().all()
class AllRepositories:

View File

@ -1,4 +1,5 @@
from pydantic import UUID4
from sqlalchemy import select
from mealie.db.models.recipe.ingredient import IngredientFoodModel
from mealie.schema.recipe.recipe_ingredient import IngredientFood
@ -7,15 +8,13 @@ from .repository_generic import RepositoryGeneric
class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
def _get_food(self, id: UUID4) -> IngredientFoodModel:
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
return self.session.execute(stmt).scalars().one()
def merge(self, from_food: UUID4, to_food: UUID4) -> IngredientFood | None:
from_model: IngredientFoodModel = (
self.session.query(self.model).filter_by(**self._filter_builder(**{"id": from_food})).one()
)
to_model: IngredientFoodModel = (
self.session.query(self.model).filter_by(**self._filter_builder(**{"id": to_food})).one()
)
from_model = self._get_food(from_food)
to_model = self._get_food(to_food)
to_model.ingredients += from_model.ingredients
@ -29,4 +28,4 @@ class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
return self.get_one(to_food)
def by_group(self, group_id: UUID4) -> "RepositoryFood":
return super().by_group(group_id) # type: ignore
return super().by_group(group_id)

View File

@ -6,21 +6,19 @@ from typing import Any, Generic, TypeVar
from fastapi import HTTPException
from pydantic import UUID4, BaseModel
from sqlalchemy import func
from sqlalchemy.orm import Query
from sqlalchemy import Select, delete, func, select
from sqlalchemy.orm.session import Session
from sqlalchemy.sql import sqltypes
from mealie.core.root_logger import get_logger
from mealie.schema.response.pagination import (
OrderDirection,
PaginationBase,
PaginationQuery,
)
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.schema.response.pagination import OrderDirection, PaginationBase, PaginationQuery
from mealie.schema.response.query_filter import QueryFilter
Schema = TypeVar("Schema", bound=BaseModel)
Model = TypeVar("Model")
Model = TypeVar("Model", bound=SqlAlchemyBase)
T = TypeVar("T", bound="RepositoryGeneric")
class RepositoryGeneric(Generic[Schema, Model]):
@ -33,6 +31,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
user_id: UUID4 | None = None
group_id: UUID4 | None = None
session: Session
def __init__(self, session: Session, primary_key: str, sql_model: type[Model], schema: type[Schema]) -> None:
self.session = session
@ -42,11 +41,11 @@ class RepositoryGeneric(Generic[Schema, Model]):
self.logger = get_logger()
def by_user(self, user_id: UUID4) -> RepositoryGeneric[Schema, Model]:
def by_user(self: T, user_id: UUID4) -> T:
self.user_id = user_id
return self
def by_group(self, group_id: UUID4) -> RepositoryGeneric[Schema, Model]:
def by_group(self: T, group_id: UUID4) -> T:
self.group_id = group_id
return self
@ -55,7 +54,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
self.logger.error(e)
def _query(self):
return self.session.query(self.model)
return select(self.model)
def _filter_builder(self, **kwargs) -> dict[str, Any]:
dct = {}
@ -98,8 +97,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
except AttributeError:
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
return [eff_schema.from_orm(x) for x in q.offset(start).limit(limit).all()]
result = self.session.execute(q.offset(start).limit(limit)).scalars().all()
return [eff_schema.from_orm(x) for x in result]
def multi_query(
self,
@ -120,7 +119,9 @@ class RepositoryGeneric(Generic[Schema, Model]):
order_attr = order_attr.desc()
q = q.order_by(order_attr)
return [eff_schema.from_orm(x) for x in q.offset(start).limit(limit).all()]
q = q.offset(start).limit(limit)
result = self.session.execute(q).scalars().all()
return [eff_schema.from_orm(x) for x in result]
def _query_one(self, match_value: str | int | UUID4, match_key: str | None = None) -> Model:
"""
@ -131,14 +132,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
match_key = self.primary_key
fltr = self._filter_builder(**{match_key: match_value})
return self._query().filter_by(**fltr).one()
return self.session.execute(self._query().filter_by(**fltr)).scalars().one()
def get_one(
self, value: str | int | UUID4, key: str | None = None, any_case=False, override_schema=None
) -> Schema | None:
key = key or self.primary_key
q = self.session.query(self.model)
q = self._query()
if any_case:
search_attr = getattr(self.model, key)
@ -146,7 +147,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
else:
q = q.filter_by(**self._filter_builder(**{key: value}))
result = q.one_or_none()
result = self.session.execute(q).scalars().one_or_none()
if not result:
return None
@ -156,7 +157,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
def create(self, data: Schema | BaseModel | dict) -> Schema:
data = data if isinstance(data, dict) else data.dict()
new_document = self.model(session=self.session, **data) # type: ignore
new_document = self.model(session=self.session, **data)
self.session.add(new_document)
self.session.commit()
self.session.refresh(new_document)
@ -167,7 +168,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
new_documents = []
for document in data:
document = document if isinstance(document, dict) else document.dict()
new_document = self.model(session=self.session, **document) # type: ignore
new_document = self.model(session=self.session, **document)
new_documents.append(new_document)
self.session.add_all(new_documents)
@ -191,7 +192,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
entry = self._query_one(match_value=match_value)
entry.update(session=self.session, **new_data) # type: ignore
entry.update(session=self.session, **new_data)
self.session.commit()
return self.schema.from_orm(entry)
@ -202,7 +203,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
document_data = document if isinstance(document, dict) else document.dict()
document_data_by_id[document_data["id"]] = document_data
documents_to_update = self._query().filter(self.model.id.in_(list(document_data_by_id.keys()))) # type: ignore
documents_to_update_query = self._query().filter(self.model.id.in_(list(document_data_by_id.keys())))
documents_to_update = self.session.execute(documents_to_update_query).scalars().all()
updated_documents = []
for document_to_update in documents_to_update:
@ -226,7 +228,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
def delete(self, value, match_key: str | None = None) -> Schema:
match_key = match_key or self.primary_key
result = self._query().filter_by(**{match_key: value}).one()
result = self.session.execute(self._query().filter_by(**{match_key: value})).scalars().one()
results_as_model = self.schema.from_orm(result)
try:
@ -239,7 +241,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
return results_as_model
def delete_many(self, values: Iterable) -> Schema:
results = self._query().filter(self.model.id.in_(values)) # type: ignore
query = self._query().filter(self.model.id.in_(values)) # type: ignore
results = self.session.execute(query).scalars().all()
results_as_model = [self.schema.from_orm(result) for result in results]
try:
@ -256,14 +259,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
return results_as_model # type: ignore
def delete_all(self) -> None:
self._query().delete()
delete(self.model)
self.session.commit()
def count_all(self, match_key=None, match_value=None) -> int:
if None in [match_key, match_value]:
return self._query().count()
else:
return self._query().filter_by(**{match_key: match_value}).count()
q = select(func.count(self.model.id))
if None not in [match_key, match_value]:
q = q.filter_by(**{match_key: match_value})
return self.session.scalar(q)
def _count_attribute(
self,
@ -274,12 +277,12 @@ class RepositoryGeneric(Generic[Schema, Model]):
) -> int | list[Schema]: # sourcery skip: assign-if-exp
eff_schema = override_schema or self.schema
q = self._query().filter(attribute_name == attr_match)
if count:
return q.count()
q = select(func.count(self.model.id)).filter(attribute_name == attr_match)
return self.session.scalar(q)
else:
return [eff_schema.from_orm(x) for x in q.all()]
q = self._query().filter(attribute_name == attr_match)
return [eff_schema.from_orm(x) for x in self.session.execute(q).scalars().all()]
def page_all(self, pagination: PaginationQuery, override=None) -> PaginationBase[Schema]:
"""
@ -293,14 +296,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
"""
eff_schema = override or self.schema
q = self.session.query(self.model)
q = self._query()
fltr = self._filter_builder()
q = q.filter_by(**fltr)
q, count, total_pages = self.add_pagination_to_query(q, pagination)
try:
data = q.all()
data = self.session.execute(q).scalars().all()
except Exception as e:
self._log_exception(e)
self.session.rollback()
@ -314,7 +317,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
items=[eff_schema.from_orm(s) for s in data],
)
def add_pagination_to_query(self, query: Query, pagination: PaginationQuery) -> tuple[Query, int, int]:
def add_pagination_to_query(self, query: Select, pagination: PaginationQuery) -> tuple[Select, int, int]:
"""
Adds pagination data to an existing query.
@ -333,7 +336,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
self.logger.error(e)
raise HTTPException(status_code=400, detail=str(e)) from e
count = query.count()
count_query = select(func.count()).select_from(query)
count = self.session.scalar(count_query)
# interpret -1 as "get_all"
if pagination.per_page == -1:

View File

@ -1,4 +1,5 @@
from pydantic import UUID4
from sqlalchemy import func, select
from mealie.db.models.group import Group
from mealie.db.models.recipe.category import Category
@ -9,21 +10,26 @@ from mealie.db.models.users.users import User
from mealie.schema.group.group_statistics import GroupStatistics
from mealie.schema.user.user import GroupInDB
from ..db.models._model_base import SqlAlchemyBase
from .repository_generic import RepositoryGeneric
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
def get_by_name(self, name: str, limit=1) -> GroupInDB | Group | None:
dbgroup = self.session.query(self.model).filter_by(**{"name": name}).one_or_none()
def get_by_name(self, name: str) -> GroupInDB | None:
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
if dbgroup is None:
return None
return self.schema.from_orm(dbgroup)
def statistics(self, group_id: UUID4) -> GroupStatistics:
def model_count(model: type[SqlAlchemyBase]) -> int:
stmt = select(func.count(model.id)).filter_by(group_id=group_id)
return self.session.scalar(stmt)
return GroupStatistics(
total_recipes=self.session.query(RecipeModel).filter_by(group_id=group_id).count(),
total_users=self.session.query(User).filter_by(group_id=group_id).count(),
total_categories=self.session.query(Category).filter_by(group_id=group_id).count(),
total_tags=self.session.query(Tag).filter_by(group_id=group_id).count(),
total_tools=self.session.query(Tool).filter_by(group_id=group_id).count(),
total_recipes=model_count(RecipeModel),
total_users=model_count(User),
total_categories=model_count(Category),
total_tags=model_count(Tag),
total_tools=model_count(Tool),
)

View File

@ -1,6 +1,6 @@
from uuid import UUID
from sqlalchemy import or_
from sqlalchemy import or_, select
from mealie.db.models.group.mealplan import GroupMealPlanRules
from mealie.schema.meal_plan.plan_rules import PlanRulesDay, PlanRulesOut, PlanRulesType
@ -10,10 +10,10 @@ from .repository_generic import RepositoryGeneric
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
def by_group(self, group_id: UUID) -> "RepositoryMealPlanRules":
return super().by_group(group_id) # type: ignore
return super().by_group(group_id)
def get_rules(self, day: PlanRulesDay, entry_type: PlanRulesType) -> list[PlanRulesOut]:
qry = self.session.query(GroupMealPlanRules).filter(
stmt = select(GroupMealPlanRules).filter(
or_(
GroupMealPlanRules.day == day,
GroupMealPlanRules.day.is_(None),
@ -26,4 +26,6 @@ class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules
),
)
return [self.schema.from_orm(x) for x in qry.all()]
rules = self.session.execute(stmt).scalars().all()
return [self.schema.from_orm(x) for x in rules]

View File

@ -1,6 +1,8 @@
from datetime import date
from uuid import UUID
from sqlalchemy import select
from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
@ -9,10 +11,10 @@ from .repository_generic import RepositoryGeneric
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
def by_group(self, group_id: UUID) -> "RepositoryMeals":
return super().by_group(group_id) # type: ignore
return super().by_group(group_id)
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
today = date.today()
qry = self.session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
return [self.schema.from_orm(x) for x in qry.all()]
stmt = select(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
plans = self.session.execute(stmt).scalars().all()
return [self.schema.from_orm(x) for x in plans]

View File

@ -1,10 +1,10 @@
from collections.abc import Sequence
from random import randint
from typing import Any
from uuid import UUID
from pydantic import UUID4
from slugify import slugify
from sqlalchemy import and_, func
from sqlalchemy import and_, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
@ -18,13 +18,14 @@ from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.recipe import Recipe
from mealie.schema.recipe.recipe import (
RecipeCategory,
RecipePagination,
RecipeSummary,
RecipeSummaryWithIngredients,
RecipeTag,
RecipeTool,
)
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
from mealie.schema.response.pagination import PaginationQuery
from .repository_generic import RepositoryGeneric
@ -46,34 +47,31 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
raise
def by_group(self, group_id: UUID) -> "RepositoryRecipes":
return super().by_group(group_id) # type: ignore
return super().by_group(group_id)
def get_all_public(self, limit: int | None = None, order_by: str | None = None, start=0, override_schema=None):
eff_schema = override_schema or self.schema
if order_by:
order_attr = getattr(self.model, str(order_by))
return [
eff_schema.from_orm(x)
for x in self.session.query(self.model)
stmt = (
select(self.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 self.session.execute(stmt).scalars().all()]
return [
eff_schema.from_orm(x)
for x in self.session.query(self.model)
stmt = (
select(self.model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.offset(start)
.limit(limit)
.all()
]
)
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
def update_image(self, slug: str, _: str | None = None) -> int:
entry: RecipeModel = self._query_one(match_value=slug)
@ -100,7 +98,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
def summary(
self, group_id, start=0, limit=99999, load_foods=False, order_by="created_at", order_descending=True
) -> Any:
) -> Sequence[RecipeModel]:
args = [
joinedload(RecipeModel.recipe_category),
joinedload(RecipeModel.tags),
@ -126,15 +124,15 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
else:
order_attr = order_attr.asc()
return (
self.session.query(RecipeModel)
stmt = (
select(RecipeModel)
.options(*args)
.filter(RecipeModel.group_id == group_id)
.order_by(order_attr)
.offset(start)
.limit(limit)
.all()
)
return self.session.execute(stmt).scalars().all()
def page_all(
self,
@ -145,8 +143,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
categories: list[UUID4 | str] | None = None,
tags: list[UUID4 | str] | None = None,
tools: list[UUID4 | str] | None = None,
) -> PaginationBase[RecipeSummary]:
q = self.session.query(self.model)
) -> RecipePagination:
q = select(self.model)
args = [
joinedload(RecipeModel.recipe_category),
@ -154,6 +152,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
joinedload(RecipeModel.tools),
]
item_class: type[RecipeSummary | RecipeSummaryWithIngredients]
if load_food:
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.unit)))
@ -205,14 +205,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
q, count, total_pages = self.add_pagination_to_query(q, pagination)
try:
data = q.all()
data = self.session.execute(q).scalars().unique().all()
except Exception as e:
self._log_exception(e)
self.session.rollback()
raise e
items = [item_class.from_orm(item) for item in data]
return PaginationBase(
return RecipePagination(
page=pagination.page,
per_page=pagination.per_page,
total=count,
@ -226,14 +226,12 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
"""
ids = [x.id for x in categories]
return [
RecipeSummary.from_orm(x)
for x in self.session.query(RecipeModel)
stmt = (
select(RecipeModel)
.join(RecipeModel.recipe_category)
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
.all()
]
)
return [RecipeSummary.from_orm(x) for x in self.session.execute(stmt).unique().scalars().all()]
def _category_tag_filters(
self,
@ -284,8 +282,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
fltr = self._category_tag_filters(
categories, tags, tools, require_all_categories, require_all_tags, require_all_tools
)
return [self.schema.from_orm(x) for x in self.session.query(RecipeModel).filter(*fltr).all()]
stmt = select(RecipeModel).filter(*fltr)
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
def get_random_by_categories_and_tags(
self, categories: list[RecipeCategory], tags: list[RecipeTag]
@ -300,33 +298,27 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
# - https://stackoverflow.com/questions/60805/getting-random-row-through-sqlalchemy
filters = self._category_tag_filters(categories, tags) # type: ignore
return [
self.schema.from_orm(x)
for x in self.session.query(RecipeModel)
.filter(and_(*filters))
.order_by(func.random()) # Postgres and SQLite specific
.limit(1)
]
stmt = (
select(RecipeModel).filter(and_(*filters)).order_by(func.random()).limit(1) # Postgres and SQLite specific
)
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
def get_random(self, limit=1) -> list[Recipe]:
return [
self.schema.from_orm(x)
for x in self.session.query(RecipeModel)
stmt = (
select(RecipeModel)
.filter(RecipeModel.group_id == self.group_id)
.order_by(func.random()) # Postgres and SQLite specific
.limit(limit)
]
)
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
def get_by_slug(self, group_id: UUID4, slug: str, limit=1) -> Recipe | None:
dbrecipe = (
self.session.query(RecipeModel)
.filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
.one_or_none()
)
stmt = select(RecipeModel).filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
dbrecipe = self.session.execute(stmt).scalars().one_or_none()
if dbrecipe is None:
return None
return self.schema.from_orm(dbrecipe)
def all_ids(self, group_id: UUID4) -> list[UUID4]:
return [tpl[0] for tpl in self.session.query(RecipeModel.id).filter(RecipeModel.group_id == group_id).all()]
def all_ids(self, group_id: UUID4) -> Sequence[UUID4]:
stmt = select(RecipeModel.id).filter(RecipeModel.group_id == group_id)
return self.session.execute(stmt).scalars().all()

View File

@ -1,4 +1,5 @@
from pydantic import UUID4
from sqlalchemy import select
from mealie.db.models.recipe.ingredient import IngredientUnitModel
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
@ -7,15 +8,13 @@ from .repository_generic import RepositoryGeneric
class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
def _get_unit(self, id: UUID4) -> IngredientUnitModel:
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
return self.session.execute(stmt).scalars().one()
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
from_model: IngredientUnitModel = (
self.session.query(self.model).filter_by(**self._filter_builder(**{"id": from_unit})).one()
)
to_model: IngredientUnitModel = (
self.session.query(self.model).filter_by(**self._filter_builder(**{"id": to_unit})).one()
)
from_model = self._get_unit(from_unit)
to_model = self._get_unit(to_unit)
to_model.ingredients += from_model.ingredients
@ -29,4 +28,4 @@ class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
return self.get_one(to_unit)
def by_group(self, group_id: UUID4) -> "RepositoryUnit":
return super().by_group(group_id) # type: ignore
return super().by_group(group_id)

View File

@ -2,6 +2,7 @@ import random
import shutil
from pydantic import UUID4
from sqlalchemy import select
from mealie.assets import users as users_assets
from mealie.schema.user.user import PrivateUser, User
@ -35,12 +36,14 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
entry = super().delete(value, match_key)
# Delete the user's directory
shutil.rmtree(PrivateUser.get_directory(value))
return entry # type: ignore
return entry
def get_by_username(self, username: str) -> PrivateUser | None:
dbuser = self.session.query(User).filter(User.username == username).one_or_none()
stmt = select(User).filter(User.username == username)
dbuser = self.session.execute(stmt).scalars().one_or_none()
return None if dbuser is None else self.schema.from_orm(dbuser)
def get_locked_users(self) -> list[PrivateUser]:
results = self.session.query(User).filter(User.locked_at != None).all() # noqa E711
stmt = select(User).filter(User.locked_at != None) # noqa E711
results = self.session.execute(stmt).scalars().all()
return [self.schema.from_orm(x) for x in results]

View File

@ -37,7 +37,6 @@ class HttpRepo(Generic[C, R, U]):
exception_msgs: Callable[[type[Exception]], str] | None = None,
default_message: str | None = None,
) -> None:
self.repo = repo
self.logger = logger
self.exception_msgs = exception_msgs

View File

@ -39,7 +39,6 @@ class AdminAboutController(BaseAdminController):
@router.get("/statistics", response_model=AppStatistics)
def get_app_statistics(self):
return AppStatistics(
total_recipes=self.repos.recipes.count_all(),
uncategorized_recipes=self.repos.recipes.count_uncategorized(), # type: ignore

View File

@ -85,7 +85,6 @@ class AdminMaintenanceController(BaseAdminController):
@router.get("/logs", response_model=MaintenanceLogs)
def get_logs(self, lines: int = 200):
return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines))
@router.get("/storage", response_model=MaintenanceStorageDetails)

View File

@ -49,7 +49,6 @@ class MealieAuthToken(BaseModel):
@public_router.post("/token")
def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(generate_session)):
email = data.username
password = data.password

View File

@ -8,8 +8,7 @@ from typing import Any, TypeVar, cast
from dateutil import parser as date_parser
from dateutil.parser import ParserError
from humps import decamelize
from sqlalchemy import bindparam, text
from sqlalchemy.orm.query import Query
from sqlalchemy import Select, bindparam, text
from sqlalchemy.sql import sqltypes
from sqlalchemy.sql.expression import BindParameter
@ -72,7 +71,7 @@ class QueryFilter:
return f"<<{joined}>>"
def filter_query(self, query: Query, model: type[Model]) -> Query:
def filter_query(self, query: Select, model: type[Model]) -> Select:
segments: list[str] = []
params: list[BindParameter] = []
for i, component in enumerate(self.filter_components):

View File

@ -4,7 +4,7 @@ from pathlib import Path
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import MetaData, create_engine
from sqlalchemy import MetaData, create_engine, insert, text
from sqlalchemy.engine import base
from sqlalchemy.orm import sessionmaker
@ -85,41 +85,48 @@ class AlchemyExporter(BaseService):
Returns the schema of the SQLAlchemy database as a python dictionary. This dictionary is wrapped by
jsonable_encoder to ensure that the object can be converted to a json string.
"""
self.meta.reflect(bind=self.engine)
with self.engine.connect() as connection:
self.meta.reflect(bind=self.engine)
all_tables = self.meta.tables.values()
all_tables = self.meta.tables.values()
results = {
**{table.name: [] for table in all_tables},
"alembic_version": [dict(row) for row in self.engine.execute("SELECT * FROM alembic_version").fetchall()],
}
results = {
**{table.name: [] for table in all_tables},
"alembic_version": [
dict(row) for row in connection.execute(text("SELECT * FROM alembic_version")).mappings()
],
}
return jsonable_encoder(results)
return jsonable_encoder(results)
def dump(self) -> dict[str, list[dict]]:
"""
Returns the entire SQLAlchemy database as a python dictionary. This dictionary is wrapped by
jsonable_encoder to ensure that the object can be converted to a json string.
"""
self.meta.reflect(bind=self.engine) # http://docs.sqlalchemy.org/en/rel_0_9/core/reflection.html
result = {
table.name: [dict(row) for row in self.engine.execute(table.select())] for table in self.meta.sorted_tables
}
with self.engine.connect() as connection:
self.meta.reflect(bind=self.engine) # http://docs.sqlalchemy.org/en/rel_0_9/core/reflection.html
result = {
table.name: [dict(row) for row in connection.execute(table.select()).mappings()]
for table in self.meta.sorted_tables
}
return jsonable_encoder(result)
def restore(self, db_dump: dict) -> None:
"""Restores all data from dictionary into the database"""
data = AlchemyExporter.convert_to_datetime(db_dump)
with self.engine.begin() as connection:
data = AlchemyExporter.convert_to_datetime(db_dump)
self.meta.reflect(bind=self.engine)
for table_name, rows in data.items():
if not rows:
continue
self.meta.reflect(bind=self.engine)
for table_name, rows in data.items():
if not rows:
continue
table = self.meta.tables[table_name]
self.engine.execute(table.delete())
self.engine.execute(table.insert(), rows)
table = self.meta.tables[table_name]
connection.execute(table.delete())
connection.execute(insert(table), rows)
def drop_all(self) -> None:
"""Drops all data from the database"""
@ -129,11 +136,11 @@ class AlchemyExporter(BaseService):
try:
if is_postgres:
session.execute("SET session_replication_role = 'replica'")
session.execute(text("SET session_replication_role = 'replica'"))
for table in self.meta.sorted_tables:
session.execute(f"DELETE FROM {table.name}")
session.execute(text(f"DELETE FROM {table.name}"))
finally:
if is_postgres:
session.execute("SET session_replication_role = 'origin'")
session.execute(text("SET session_replication_role = 'origin'"))
session.commit()

View File

@ -69,7 +69,6 @@ class DefaultEmailSender(ABCEmailSender, BaseService):
"""
def send(self, email_to: str, subject: str, html: str) -> bool:
if self.settings.SMTP_FROM_EMAIL is None or self.settings.SMTP_FROM_NAME is None:
raise ValueError("SMTP_FROM_EMAIL and SMTP_FROM_NAME must be set in the config file.")

View File

@ -8,6 +8,7 @@ from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
from fastapi.encoders import jsonable_encoder
from pydantic import UUID4
from sqlalchemy import select
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import session_context
@ -143,12 +144,9 @@ class WebhookEventListener(EventListenerBase):
def get_scheduled_webhooks(self, start_dt: datetime, end_dt: datetime) -> list[ReadWebhook]:
"""Fetches all scheduled webhooks from the database"""
with self.ensure_session() as session:
return (
session.query(GroupWebhooksModel)
.where(
GroupWebhooksModel.enabled == True, # noqa: E712 - required for SQLAlchemy comparison
GroupWebhooksModel.scheduled_time > start_dt.astimezone(timezone.utc).time(),
GroupWebhooksModel.scheduled_time <= end_dt.astimezone(timezone.utc).time(),
)
.all()
stmt = select(GroupWebhooksModel).where(
GroupWebhooksModel.enabled == True, # noqa: E712 - required for SQLAlchemy comparison
GroupWebhooksModel.scheduled_time > start_dt.astimezone(timezone.utc).time(),
GroupWebhooksModel.scheduled_time <= end_dt.astimezone(timezone.utc).time(),
)
return session.execute(stmt).scalars().all()

View File

@ -67,7 +67,6 @@ class BaseMigrator(BaseService):
self.report_id = self.report.id
def _save_all_entries(self) -> None:
is_success = True
is_failure = True

View File

@ -17,7 +17,6 @@ replace_abbreviations = {
def replace_common_abbreviations(string: str) -> str:
for k, v in replace_abbreviations.items():
regex = rf"(?<=\d)\s?({k}\bs?)"
string = re.sub(regex, v, string)

View File

@ -180,7 +180,6 @@ def import_data(lines):
# otherwise it's a token
# e.g.: potato \t I2 \t L5 \t NoCAP \t B-NAME/0.978253
else:
columns = re.split("\t", line.strip())
token = columns[0].strip()

View File

@ -33,7 +33,6 @@ async def largest_content_len(urls: list[str]) -> tuple[str, int]:
tasks = [do(client, url) for url in urls]
responses: list[Response] = await gather_with_concurrency(10, *tasks)
for response in responses:
len_int = int(response.headers.get("Content-Length", 0))
if len_int > largest_len:
largest_url = str(response.url)

View File

@ -108,7 +108,6 @@ class RecipeService(BaseService):
return Recipe(**additional_attrs)
def create_one(self, create_data: Recipe | CreateRecipe) -> Recipe:
if create_data.name is None:
create_data.name = "New Recipe"

View File

@ -1,6 +1,8 @@
import datetime
from pathlib import Path
from sqlalchemy import select
from mealie.core import root_logger
from mealie.core.config import get_app_dirs
from mealie.db.db_setup import session_context
@ -17,7 +19,8 @@ def purge_group_data_exports(max_minutes_old=ONE_DAY_AS_MINUTES):
limit = datetime.datetime.now() - datetime.timedelta(minutes=max_minutes_old)
with session_context() as session:
results = session.query(GroupDataExportsModel).filter(GroupDataExportsModel.expires <= limit)
stmt = select(GroupDataExportsModel).filter(GroupDataExportsModel.expires <= limit)
results = session.execute(stmt).scalars().all()
total_removed = 0
for result in results:

View File

@ -1,5 +1,7 @@
import datetime
from sqlalchemy import delete
from mealie.core import root_logger
from mealie.db.db_setup import session_context
from mealie.db.models.users.password_reset import PasswordResetModel
@ -15,7 +17,8 @@ def purge_password_reset_tokens():
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
with session_context() as session:
session.query(PasswordResetModel).filter(PasswordResetModel.created_at <= limit).delete()
stmt = delete(PasswordResetModel).filter(PasswordResetModel.created_at <= limit)
session.execute(stmt)
session.commit()
session.close()
logger.info("password reset tokens purged")

View File

@ -1,5 +1,7 @@
import datetime
from sqlalchemy import delete
from mealie.core import root_logger
from mealie.db.db_setup import session_context
from mealie.db.models.group import GroupInviteToken
@ -15,7 +17,8 @@ def purge_group_registration():
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
with session_context() as session:
session.query(GroupInviteToken).filter(GroupInviteToken.created_at <= limit).delete()
stmt = delete(GroupInviteToken).filter(GroupInviteToken.created_at <= limit)
session.execute(stmt)
session.commit()
session.close()

View File

@ -83,7 +83,6 @@ class RecipeBulkScraperService(BaseService):
tasks = [_do(b.url) for b in urls.imports]
results = await gather(*tasks)
for b, recipe in zip(urls.imports, results, strict=True):
if not recipe:
continue

1825
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ start = "mealie.app:main"
Jinja2 = "^3.1.2"
Pillow = "^9.2.0"
PyYAML = "^5.3.1"
SQLAlchemy = "^1.4.29"
SQLAlchemy = "^2"
aiofiles = "^22.1.0"
alembic = "^1.7.5"
aniso8601 = "9.0.1"
@ -43,12 +43,11 @@ tzdata = "^2022.7"
uvicorn = {extras = ["standard"], version = "^0.20.0"}
[tool.poetry.group.dev.dependencies]
black = "^21.12b0"
black = "^23.1.0"
coverage = "^7.0"
coveragepy-lcov = "^0.1.1"
mkdocs-material = "^9.0.0"
mypy = "^0.991"
openapi-spec-validator = "^0.5.0"
pre-commit = "^3.0.4"
pydantic-to-typescript = "^1.0.7"
pylint = "^2.6.0"

View File

@ -30,7 +30,6 @@ def override_get_db():
@fixture(scope="session")
def api_client():
app.dependency_overrides[generate_session] = override_get_db
yield TestClient(app)

View File

@ -25,7 +25,6 @@ def create_item(list_id: UUID4) -> dict:
@pytest.fixture(scope="function")
def shopping_lists(database: AllRepositories, unique_user: TestUser):
models: list[ShoppingListOut] = []
for _ in range(3):
@ -46,7 +45,6 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
@pytest.fixture(scope="function")
def shopping_list(database: AllRepositories, unique_user: TestUser):
model = database.group_shopping_lists.create(
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id),
)

View File

@ -60,7 +60,6 @@ def test_group_invitation_link(api_client: TestClient, unique_user: TestUser, in
def test_group_invitation_delete_after_uses(api_client: TestClient, invite: str) -> None:
# Register First User
_, r = register_user(api_client, invite)
assert r.status_code == 201

View File

@ -85,7 +85,6 @@ def test_crud_mealplan(api_client: TestClient, unique_user: TestUser):
def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
for _ in range(3):
new_plan = CreatePlanEntry(
date=date.today(),

View File

@ -94,7 +94,6 @@ def test_shopping_list_items_get_one(
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
for _ in range(3):
item = random.choice(list_with_items.list_items)

View File

@ -19,7 +19,6 @@ from tests.utils.fixture_schemas import TestUser
def ten_slugs(
api_client: TestClient, unique_user: TestUser, database: AllRepositories
) -> Generator[list[str], None, None]:
slugs: list[str] = []
for _ in range(10):
@ -98,7 +97,6 @@ def test_bulk_delete_recipes(
database: AllRepositories,
ten_slugs: list[str],
):
payload = {"recipes": ten_slugs}
response = api_client.post(api_routes.recipes_bulk_actions_delete, json=payload, headers=unique_user.token)

View File

@ -13,7 +13,6 @@ from tests.utils.fixture_schemas import TestUser
@pytest.fixture(scope="function")
def slug(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> Generator[str, None, None]:
payload = {"name": random_string(length=20)}
response = api_client.post(api_routes.recipes, json=payload, headers=unique_user.token)
assert response.status_code == 201

View File

@ -25,13 +25,10 @@ def test_superuser_login(api_client: TestClient, admin_token):
response = api_client.post(api_routes.auth_token, data=form_data)
assert response.status_code == 200
new_token = json.loads(response.text).get("access_token")
response = api_client.get(api_routes.users_self, headers=admin_token)
assert response.status_code == 200
return {"Authorization": f"Bearer {new_token}"}
def test_user_token_refresh(api_client: TestClient, admin_user: TestUser):
response = api_client.post(api_routes.auth_refresh, headers=admin_user.token)

View File

@ -113,7 +113,6 @@ def test_recipe_repo_get_by_categories_multi(database: AllRepositories, unique_u
by_category = repo.get_by_categories(cast(list[RecipeCategory], created_categories))
assert len(by_category) == 10
for recipe_summary in by_category:
for recipe_category in recipe_summary.recipe_category:
assert recipe_category.id in known_category_ids

View File

@ -25,7 +25,6 @@ def test_camelize_variables():
def test_cast_to():
model = TestModel(long_name="Hello", long_int=1, long_float=1.1)
model2 = model.cast(TestModel2, another_str="World")
@ -37,7 +36,6 @@ def test_cast_to():
def test_map_to():
model = TestModel(long_name="Model1", long_int=100, long_float=1.5)
model2 = TestModel2(long_name="Model2", long_int=1, long_float=1.1, another_str="World")

View File

@ -60,7 +60,6 @@ def test_get_scheduled_webhooks_filter_query(database: AllRepositories, unique_u
assert result.enabled
for expected_item in expected:
if result.name == expected_item.name: # Names are uniquely generated so we can use this to compare
assert result.enabled == expected_item.enabled
break

View File

@ -256,7 +256,6 @@ ingredients_test_cases = (
@pytest.mark.parametrize("ingredients", ingredients_test_cases, ids=(x.test_id for x in ingredients_test_cases))
def test_cleaner_clean_ingredients(ingredients: CleanerCase):
if ingredients.exception:
with pytest.raises(ingredients.exception):
cleaner.clean_ingredients(ingredients.input)

View File

@ -11,7 +11,6 @@ SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Test Email"}
class TestEmailSender(ABCEmailSender):
def send(self, email_to: str, subject: str, html: str) -> bool:
# check email_to:
assert email_to == FAKE_ADDRESS

View File

@ -18,7 +18,6 @@ class TestIngredient:
def crf_exists() -> bool:
return shutil.which("crf_test") is not None

View File

@ -1,8 +0,0 @@
from openapi_spec_validator import openapi_v30_spec_validator, validate_spec
from mealie.app import app
def test_validate_open_api_spec():
open_api = app.openapi()
validate_spec(open_api, validator=openapi_v30_spec_validator)

View File

@ -9,9 +9,9 @@ from mealie.pkgs.stats.fs_stats import pretty_size
(0, "0 bytes"),
(1, "1 bytes"),
(1024, "1.0 KB"),
(1024 ** 2, "1.0 MB"),
(1024 ** 2 * 1024, "1.0 GB"),
(1024 ** 2 * 1024 * 1024, "1.0 TB"),
(1024**2, "1.0 MB"),
(1024**2 * 1024, "1.0 GB"),
(1024**2 * 1024 * 1024, "1.0 TB"),
],
)
def test_pretty_size(size: int, expected: str) -> None:

View File

@ -2,5 +2,4 @@ from fastapi.encoders import jsonable_encoder
def jsonify(data):
return jsonable_encoder(data)