mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
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:
parent
91cd00976a
commit
9e77a9f367
2
.gitignore
vendored
2
.gitignore
vendored
@ -159,3 +159,5 @@ dev/code-generation/generated/test_routes.py
|
|||||||
mealie/services/parser_services/crfpp/model.crfmodel
|
mealie/services/parser_services/crfpp/model.crfmodel
|
||||||
lcov.info
|
lcov.info
|
||||||
dev/code-generation/openapi.json
|
dev/code-generation/openapi.json
|
||||||
|
|
||||||
|
.run/
|
||||||
|
@ -11,10 +11,6 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.3.0
|
rev: 23.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
|
||||||
rev: v0.0.241
|
|
||||||
hooks:
|
|
||||||
- id: ruff
|
|
||||||
|
@ -25,7 +25,6 @@ def get_path_objects(app: FastAPI):
|
|||||||
for key, value in app.openapi().items():
|
for key, value in app.openapi().items():
|
||||||
if key == "paths":
|
if key == "paths":
|
||||||
for key, value in value.items():
|
for key, value in value.items():
|
||||||
|
|
||||||
paths.append(
|
paths.append(
|
||||||
PathObject(
|
PathObject(
|
||||||
route_object=RouteObject(key),
|
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]):
|
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
|
||||||
|
|
||||||
template = Template(read_template(CodeTemplates.pytest_routes))
|
template = Template(read_template(CodeTemplates.pytest_routes))
|
||||||
content = template.render(
|
content = template.render(
|
||||||
paths={
|
paths={
|
||||||
|
@ -79,14 +79,12 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
|
|||||||
modules: list[Modules] = []
|
modules: list[Modules] = []
|
||||||
for file in root.iterdir():
|
for file in root.iterdir():
|
||||||
if file.is_dir() and file.name not in SKIP:
|
if file.is_dir() and file.name not in SKIP:
|
||||||
|
|
||||||
modules.append(Modules(directory=file))
|
modules.append(Modules(directory=file))
|
||||||
|
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
modules = find_modules(SCHEMA_PATH)
|
modules = find_modules(SCHEMA_PATH)
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
|
@ -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"):
|
def login(username="changeme@email.com", password="MyPassword"):
|
||||||
|
|
||||||
payload = {"username": username, "password": password}
|
payload = {"username": username, "password": password}
|
||||||
r = requests.post("http://localhost:9000/api/auth/token", payload)
|
r = requests.post("http://localhost:9000/api/auth/token", payload)
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ class GunicornConfig:
|
|||||||
"""Configuration to generate the properties for Gunicorn"""
|
"""Configuration to generate the properties for Gunicorn"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
# Env Variables
|
# Env Variables
|
||||||
self.host = os.getenv("HOST", "127.0.0.1")
|
self.host = os.getenv("HOST", "127.0.0.1")
|
||||||
self.port = os.getenv("API_PORT", "9000")
|
self.port = os.getenv("API_PORT", "9000")
|
||||||
|
@ -15,14 +15,9 @@ def sql_global_init(db_url: str):
|
|||||||
if "sqlite" in db_url:
|
if "sqlite" in db_url:
|
||||||
connect_args["check_same_thread"] = False
|
connect_args["check_same_thread"] = False
|
||||||
|
|
||||||
engine = sa.create_engine(
|
engine = sa.create_engine(db_url, echo=False, connect_args=connect_args, pool_pre_ping=True, future=True)
|
||||||
db_url,
|
|
||||||
echo=False,
|
|
||||||
connect_args=connect_args,
|
|
||||||
pool_pre_ping=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True)
|
||||||
|
|
||||||
return SessionLocal, engine
|
return SessionLocal, engine
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from collections.abc import Callable
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from sqlalchemy import engine, orm
|
from sqlalchemy import engine, orm, text
|
||||||
|
|
||||||
from alembic import command, config, script
|
from alembic import command, config, script
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
@ -59,7 +59,7 @@ def safe_try(func: Callable):
|
|||||||
|
|
||||||
def connect(session: orm.Session) -> bool:
|
def connect(session: orm.Session) -> bool:
|
||||||
try:
|
try:
|
||||||
session.execute("SELECT 1")
|
session.execute(text("SELECT 1"))
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error connecting to database: {e}")
|
logger.error(f"Error connecting to database: {e}")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from .group import *
|
from .group import *
|
||||||
from .labels import *
|
from .labels import *
|
||||||
from .recipe.recipe import * # type: ignore
|
from .recipe import *
|
||||||
from .server import *
|
from .server import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
@ -1,37 +1,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, Integer
|
from sqlalchemy import DateTime, Integer
|
||||||
from sqlalchemy.ext.declarative import as_declarative
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
from sqlalchemy.orm import declarative_base
|
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
|
|
||||||
|
|
||||||
@as_declarative()
|
class SqlAlchemyBase(DeclarativeBase):
|
||||||
class Base:
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
id = Column(Integer, primary_key=True)
|
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMixins:
|
class BaseMixins:
|
||||||
"""
|
"""
|
||||||
`self.update` method which directly passing arguments to the `__init__`
|
`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):
|
def update(self, *args, **kwarg):
|
||||||
self.__init__(*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)
|
|
||||||
|
@ -2,13 +2,13 @@ from functools import wraps
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, NoneStr
|
from pydantic import BaseModel, Field, NoneStr
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
||||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
|
||||||
from sqlalchemy.orm.mapper import Mapper
|
from sqlalchemy.orm.mapper import Mapper
|
||||||
from sqlalchemy.orm.relationships import RelationshipProperty
|
from sqlalchemy.orm.relationships import RelationshipProperty
|
||||||
from sqlalchemy.sql.base import ColumnCollection
|
from sqlalchemy.sql.base import ColumnCollection
|
||||||
from sqlalchemy.util._collections import ImmutableProperties
|
|
||||||
|
|
||||||
|
from .._model_base import SqlAlchemyBase
|
||||||
from .helpers import safe_call
|
from .helpers import safe_call
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class AutoInitConfig(BaseModel):
|
|||||||
# auto_create: bool = False
|
# 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.
|
Returns the config for the given class.
|
||||||
"""
|
"""
|
||||||
@ -45,7 +45,7 @@ def _get_config(relation_cls: DeclarativeMeta) -> AutoInitConfig:
|
|||||||
return cfg
|
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.
|
"""Returns the primary key attribute of the related class as a string.
|
||||||
|
|
||||||
Args:
|
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)
|
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] = []
|
elems_to_create: list[dict] = []
|
||||||
updated_elems: 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:
|
for elem in all_elements:
|
||||||
elem_id = elem.get(get_attr, None) if isinstance(elem, dict) else elem
|
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 isinstance(elem, dict):
|
||||||
|
elems_to_create.append(elem)
|
||||||
if existing_elem is None and is_dict:
|
|
||||||
elems_to_create.append(elem) # type: ignore
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif is_dict:
|
elif isinstance(elem, dict):
|
||||||
for key, value in elem.items(): # type: ignore
|
for key, value in elem.items():
|
||||||
if key not in cfg.exclude:
|
if key not in cfg.exclude:
|
||||||
setattr(existing_elem, key, value)
|
setattr(existing_elem, key, value)
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ def auto_init(): # sourcery no-metrics
|
|||||||
|
|
||||||
def decorator(init):
|
def decorator(init):
|
||||||
@wraps(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.
|
Custom initializer that allows nested children initialization.
|
||||||
Only keys that are present as instance's class attributes are allowed.
|
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
|
Ref: https://github.com/tiangolo/fastapi/issues/2194
|
||||||
"""
|
"""
|
||||||
cls = self.__class__
|
cls = self.__class__
|
||||||
|
config = _get_config(cls)
|
||||||
exclude = _get_config(cls).exclude
|
exclude = config.exclude
|
||||||
|
|
||||||
alchemy_mapper: Mapper = self.__mapper__
|
alchemy_mapper: Mapper = self.__mapper__
|
||||||
model_columns: ColumnCollection = alchemy_mapper.columns
|
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:
|
if session is None:
|
||||||
raise ValueError("Session is required to initialize the model with `auto_init`")
|
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
|
relation_dir = prop.direction
|
||||||
|
|
||||||
# Identifies the parent class of the related object.
|
# 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
|
# Identifies if the relationship was declared with use_list=True
|
||||||
use_list: bool = prop.uselist
|
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}")
|
raise ValueError(f"Expected 'id' to be provided for {key}")
|
||||||
|
|
||||||
if isinstance(val, (str, int, UUID)):
|
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)
|
setattr(self, key, instance)
|
||||||
else:
|
else:
|
||||||
# If the value is not of the type defined above we assume that it isn't a valid id
|
# If the value is not of the type defined above we assume that it isn't a valid id
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import auto_init, guid
|
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.tag import Tag, cookbooks_to_tags
|
||||||
from ..recipe.tool import Tool, cookbooks_to_tools
|
from ..recipe.tool import Tool, cookbooks_to_tools
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "cookbooks"
|
__tablename__ = "cookbooks"
|
||||||
id = 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)
|
||||||
position = Column(Integer, nullable=False, default=1)
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||||
|
|
||||||
group_id = Column(guid.GUID, ForeignKey("groups.id"))
|
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"))
|
||||||
group = orm.relationship("Group", back_populates="cookbooks")
|
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="cookbooks")
|
||||||
|
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
slug = Column(String, nullable=False)
|
slug: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
description = Column(String, default="")
|
description: Mapped[str | None] = mapped_column(String, default="")
|
||||||
public = Column(Boolean, default=False)
|
public: Mapped[str | None] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
|
categories: Mapped[list[Category]] = orm.relationship(
|
||||||
require_all_categories = Column(Boolean, default=True)
|
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)
|
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
|
||||||
require_all_tags = Column(Boolean, default=True)
|
require_all_tags: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
tools = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
|
tools: Mapped[list[Tool]] = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
|
||||||
require_all_tools = Column(Boolean, default=True)
|
require_all_tools: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import GUID, auto_init
|
from .._model_utils import GUID, auto_init
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
|
class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_events_notifier_options"
|
__tablename__ = "group_events_notifier_options"
|
||||||
|
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
event_notifier_id = Column(GUID, ForeignKey("group_events_notifiers.id"), nullable=False)
|
event_notifier_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_events_notifiers.id"), nullable=False)
|
||||||
|
|
||||||
recipe_created = Column(Boolean, default=False, nullable=False)
|
recipe_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
recipe_updated = Column(Boolean, default=False, nullable=False)
|
recipe_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
recipe_deleted = 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_migrations: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
data_export = Column(Boolean, default=False, nullable=False)
|
data_export: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
data_import = 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_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
shopping_list_updated = Column(Boolean, default=False, nullable=False)
|
shopping_list_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
shopping_list_deleted = 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_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
cookbook_updated = Column(Boolean, default=False, nullable=False)
|
cookbook_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
cookbook_deleted = 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_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
tag_updated = Column(Boolean, default=False, nullable=False)
|
tag_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
tag_deleted = 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_created: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
category_updated = Column(Boolean, default=False, nullable=False)
|
category_updated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
category_deleted = Column(Boolean, default=False, nullable=False)
|
category_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -46,15 +52,19 @@ class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
|
|||||||
class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins):
|
class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_events_notifiers"
|
__tablename__ = "group_events_notifiers"
|
||||||
|
|
||||||
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)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
enabled = Column(Boolean, default=True, nullable=False)
|
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
apprise_url = Column(String, nullable=False)
|
apprise_url: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
|
||||||
group = orm.relationship("Group", back_populates="group_event_notifiers", single_parent=True)
|
group: Mapped[Optional["Group"]] = orm.relationship(
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
"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()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import GUID, auto_init
|
from .._model_utils import GUID, auto_init
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupDataExportsModel(SqlAlchemyBase, BaseMixins):
|
class GroupDataExportsModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_data_exports"
|
__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: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="data_exports", single_parent=True)
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||||
|
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
filename = Column(String, nullable=False)
|
filename: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
path = Column(String, nullable=False)
|
path: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
size = Column(String, nullable=False)
|
size: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
expires = Column(String, nullable=False)
|
expires: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import get_app_settings
|
from mealie.core.config import get_app_settings
|
||||||
@ -15,18 +19,28 @@ from .cookbook import CookBook
|
|||||||
from .mealplan import GroupMealPlan
|
from .mealplan import GroupMealPlan
|
||||||
from .preferences import GroupPreferencesModel
|
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):
|
class Group(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "groups"
|
__tablename__ = "groups"
|
||||||
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
|
||||||
users = orm.relationship("User", back_populates="group")
|
users: Mapped[list["User"]] = orm.relationship("User", back_populates="group")
|
||||||
categories = orm.relationship(Category, secondary=group_to_categories, single_parent=True, uselist=True)
|
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
|
GroupInviteToken, back_populates="group", cascade="all, delete-orphan", uselist=True
|
||||||
)
|
)
|
||||||
preferences = orm.relationship(
|
preferences: Mapped[GroupPreferencesModel] = orm.relationship(
|
||||||
GroupPreferencesModel,
|
GroupPreferencesModel,
|
||||||
back_populates="group",
|
back_populates="group",
|
||||||
uselist=False,
|
uselist=False,
|
||||||
@ -35,7 +49,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Recipes
|
# 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
|
# CRUD From Others
|
||||||
common_args = {
|
common_args = {
|
||||||
@ -44,23 +58,26 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
"single_parent": True,
|
"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)
|
mealplans: Mapped[list[GroupMealPlan]] = orm.relationship(
|
||||||
webhooks = orm.relationship(GroupWebhooksModel, **common_args)
|
GroupMealPlan, order_by="GroupMealPlan.date", **common_args
|
||||||
cookbooks = orm.relationship(CookBook, **common_args)
|
)
|
||||||
server_tasks = orm.relationship(ServerTaskModel, **common_args)
|
webhooks: Mapped[list[GroupWebhooksModel]] = orm.relationship(GroupWebhooksModel, **common_args)
|
||||||
data_exports = orm.relationship("GroupDataExportsModel", **common_args)
|
cookbooks: Mapped[list[CookBook]] = orm.relationship(CookBook, **common_args)
|
||||||
shopping_lists = orm.relationship("ShoppingList", **common_args)
|
server_tasks: Mapped[list[ServerTaskModel]] = orm.relationship(ServerTaskModel, **common_args)
|
||||||
group_reports = orm.relationship("ReportModel", **common_args)
|
data_exports: Mapped[list["GroupDataExportsModel"]] = orm.relationship("GroupDataExportsModel", **common_args)
|
||||||
group_event_notifiers = orm.relationship("GroupEventNotifierModel", **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
|
# Owned Models
|
||||||
ingredient_units = orm.relationship("IngredientUnitModel", **common_args)
|
ingredient_units: Mapped[list["IngredientUnitModel"]] = orm.relationship("IngredientUnitModel", **common_args)
|
||||||
ingredient_foods = orm.relationship("IngredientFoodModel", **common_args)
|
ingredient_foods: Mapped[list["IngredientFoodModel"]] = orm.relationship("IngredientFoodModel", **common_args)
|
||||||
tools = orm.relationship("Tool", **common_args)
|
tools: Mapped[list["Tool"]] = orm.relationship("Tool", **common_args)
|
||||||
tags = orm.relationship("Tag", **common_args)
|
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
|
||||||
categories = orm.relationship("Category", **common_args)
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
exclude = {
|
exclude = {
|
||||||
@ -79,10 +96,10 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod # TODO: Remove this
|
@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()
|
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:
|
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
|
return item
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import auto_init, guid
|
from .._model_utils import auto_init, guid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "invite_tokens"
|
__tablename__ = "invite_tokens"
|
||||||
token = Column(String, index=True, nullable=False, unique=True)
|
token: Mapped[str] = mapped_column(String, index=True, nullable=False, unique=True)
|
||||||
uses_left = Column(Integer, nullable=False, default=1)
|
uses_left: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||||
|
|
||||||
group_id = Column(guid.GUID, ForeignKey("groups.id"))
|
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"))
|
||||||
group = orm.relationship("Group", back_populates="invite_tokens")
|
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="invite_tokens")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
|
@ -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
|
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 .._model_utils import GUID, auto_init
|
||||||
from ..recipe.category import Category, plan_rules_to_categories
|
from ..recipe.category import Category, plan_rules_to_categories
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
from ..recipe import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||||
__tablename__ = "group_meal_plan_rules"
|
__tablename__ = "group_meal_plan_rules"
|
||||||
|
|
||||||
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)
|
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
|
|
||||||
day = Column(String, nullable=False, default="unset") # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
|
day: Mapped[str] = mapped_column(
|
||||||
entry_type = Column(String, nullable=False, default="") # "breakfast", "lunch", "dinner", "side"
|
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)
|
categories: Mapped[Category] = orm.relationship(Category, secondary=plan_rules_to_categories, uselist=True)
|
||||||
tags = orm.relationship(Tag, secondary=plan_rules_to_tags, uselist=True)
|
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags, uselist=True)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -27,16 +40,18 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
|||||||
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
|
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_meal_plans"
|
__tablename__ = "group_meal_plans"
|
||||||
|
|
||||||
date = Column(Date, index=True, nullable=False)
|
date: Mapped[date] = mapped_column(Date, index=True, nullable=False)
|
||||||
entry_type = Column(String, index=True, nullable=False)
|
entry_type: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
title = Column(String, index=True, nullable=False)
|
title: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
text = Column(String, nullable=False)
|
text: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||||
group = orm.relationship("Group", back_populates="mealplans")
|
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="mealplans")
|
||||||
|
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="meal_entries", uselist=False)
|
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
|
||||||
|
"RecipeModel", back_populates="meal_entries", uselist=False
|
||||||
|
)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -1,29 +1,34 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
|
||||||
|
|
||||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import auto_init
|
from .._model_utils import auto_init
|
||||||
|
from .._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_preferences"
|
__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_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="preferences")
|
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="preferences")
|
||||||
|
|
||||||
private_group: bool = sa.Column(sa.Boolean, default=True)
|
private_group: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||||
first_day_of_week = sa.Column(sa.Integer, default=0)
|
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
|
||||||
|
|
||||||
# Recipe Defaults
|
# Recipe Defaults
|
||||||
recipe_public: bool = sa.Column(sa.Boolean, default=True)
|
recipe_public: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||||
recipe_show_nutrition: bool = sa.Column(sa.Boolean, default=False)
|
recipe_show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
recipe_show_assets: bool = sa.Column(sa.Boolean, default=False)
|
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
recipe_landscape_view: bool = sa.Column(sa.Boolean, default=False)
|
recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
recipe_disable_comments: bool = sa.Column(sa.Boolean, default=False)
|
recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
recipe_disable_amount: bool = sa.Column(sa.Boolean, default=True)
|
recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from datetime import datetime
|
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 sqlalchemy.sql.sqltypes import Boolean, DateTime, String
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
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 import auto_init
|
||||||
from .._model_utils.guid import GUID
|
from .._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class ReportEntryModel(SqlAlchemyBase, BaseMixins):
|
class ReportEntryModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "report_entries"
|
__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)
|
success: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
message = Column(String, nullable=True)
|
message: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
exception = Column(String, nullable=True)
|
exception: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
|
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
|
||||||
report_id = Column(GUID, ForeignKey("group_reports.id"), nullable=False)
|
report_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_reports.id"), nullable=False)
|
||||||
report = orm.relationship("ReportModel", back_populates="entries")
|
report: Mapped["ReportModel"] = orm.relationship("ReportModel", back_populates="entries")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -28,18 +33,20 @@ class ReportEntryModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class ReportModel(SqlAlchemyBase, BaseMixins):
|
class ReportModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_reports"
|
__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)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
status = Column(String, nullable=False)
|
status: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
category = Column(String, index=True, nullable=False)
|
category: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
|
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
|
# Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
exclude = ["entries"]
|
exclude = ["entries"]
|
||||||
|
@ -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.ext.orderinglist import ordering_list
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models.labels import MultiPurposeLabel
|
from mealie.db.models.labels import MultiPurposeLabel
|
||||||
from mealie.db.models.recipe.api_extras import ShoppingListExtras, ShoppingListItemExtras, api_extras
|
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 .._model_utils import GUID, auto_init
|
||||||
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
from ..recipe import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||||
__tablename__ = "shopping_list_item_recipe_reference"
|
__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)
|
shopping_list_item_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||||
recipe_quantity = Column(Float, nullable=False)
|
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
recipe_scale = Column(Float, nullable=False, default=1)
|
recipe_scale: Mapped[float | None] = mapped_column(Float, default=1)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -28,32 +36,38 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
|||||||
__tablename__ = "shopping_list_items"
|
__tablename__ = "shopping_list_items"
|
||||||
|
|
||||||
# Id's
|
# Id's
|
||||||
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"))
|
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("shopping_lists.id"))
|
||||||
|
|
||||||
# Meta
|
# Meta
|
||||||
is_ingredient = Column(Boolean, default=True)
|
is_ingredient: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||||
position = Column(Integer, nullable=False, default=0)
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
checked = Column(Boolean, default=False)
|
checked: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
quantity = Column(Float, default=1)
|
quantity: Mapped[float | None] = mapped_column(Float, default=1)
|
||||||
note = Column(String)
|
note: Mapped[str | None] = mapped_column(String)
|
||||||
|
|
||||||
is_food = Column(Boolean, default=False)
|
is_food: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
extras: list[ShoppingListItemExtras] = orm.relationship("ShoppingListItemExtras", cascade="all, delete-orphan")
|
extras: Mapped[list[ShoppingListItemExtras]] = orm.relationship(
|
||||||
|
"ShoppingListItemExtras", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
# Scaling Items
|
# Scaling Items
|
||||||
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
|
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
|
||||||
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
|
||||||
|
|
||||||
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
|
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
|
||||||
food = orm.relationship(IngredientFoodModel, uselist=False)
|
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
|
||||||
|
|
||||||
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
|
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||||
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="shopping_list_items")
|
label: Mapped[MultiPurposeLabel | None] = orm.relationship(
|
||||||
|
MultiPurposeLabel, uselist=False, back_populates="shopping_list_items"
|
||||||
|
)
|
||||||
|
|
||||||
# Recipe Reference
|
# 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:
|
class Config:
|
||||||
exclude = {"id", "label", "food", "unit"}
|
exclude = {"id", "label", "food", "unit"}
|
||||||
@ -66,14 +80,16 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||||
__tablename__ = "shopping_list_recipe_reference"
|
__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_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", uselist=False, back_populates="shopping_list_refs")
|
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:
|
class Config:
|
||||||
exclude = {"id", "recipe"}
|
exclude = {"id", "recipe"}
|
||||||
@ -85,21 +101,23 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
|||||||
|
|
||||||
class ShoppingList(SqlAlchemyBase, BaseMixins):
|
class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "shopping_lists"
|
__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_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="shopping_lists")
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists")
|
||||||
|
|
||||||
name = Column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
list_items = orm.relationship(
|
list_items: Mapped[ShoppingListItem] = orm.relationship(
|
||||||
ShoppingListItem,
|
ShoppingListItem,
|
||||||
cascade="all, delete, delete-orphan",
|
cascade="all, delete, delete-orphan",
|
||||||
order_by="ShoppingListItem.position",
|
order_by="ShoppingListItem.position",
|
||||||
collection_class=ordering_list("position"),
|
collection_class=ordering_list("position"),
|
||||||
)
|
)
|
||||||
|
|
||||||
recipe_references = orm.relationship(ShoppingListRecipeReference, cascade="all, delete, delete-orphan")
|
recipe_references: Mapped[ShoppingListRecipeReference] = orm.relationship(
|
||||||
extras: list[ShoppingListExtras] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
|
ShoppingListRecipeReference, cascade="all, delete, delete-orphan"
|
||||||
|
)
|
||||||
|
extras: Mapped[list[ShoppingListExtras]] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
exclude = {"id", "list_items"}
|
exclude = {"id", "list_items"}
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import GUID, auto_init
|
from .._model_utils import GUID, auto_init
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "webhook_urls"
|
__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: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="webhooks", single_parent=True)
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||||
|
|
||||||
enabled = Column(Boolean, default=False)
|
enabled: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
name = Column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
url = Column(String)
|
url: Mapped[str | None] = mapped_column(String)
|
||||||
|
|
||||||
# New Fields
|
# New Fields
|
||||||
webhook_type = Column(String, default="") # Future use for different types of webhooks
|
webhook_type: Mapped[str | None] = mapped_column(String, default="") # Future use for different types of webhooks
|
||||||
scheduled_time = Column(Time, default=lambda: datetime.now().time())
|
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
|
# 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
|
# 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()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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 mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
from ._model_utils import auto_init
|
from ._model_utils import auto_init
|
||||||
from ._model_utils.guid import GUID
|
from ._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from group import Group, ShoppingListItem
|
||||||
|
from recipe import IngredientFoodModel
|
||||||
|
|
||||||
|
|
||||||
class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "multi_purpose_labels"
|
__tablename__ = "multi_purpose_labels"
|
||||||
id = Column(GUID, default=GUID.generate, primary_key=True)
|
id: Mapped[GUID] = mapped_column(GUID, default=GUID.generate, primary_key=True)
|
||||||
name = Column(String(255), nullable=False)
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
color = Column(String(10), nullable=False, default="")
|
color: Mapped[str] = mapped_column(String(10), nullable=False, default="")
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="labels")
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="labels")
|
||||||
|
|
||||||
shopping_list_items = orm.relationship("ShoppingListItem", back_populates="label")
|
shopping_list_items: Mapped["ShoppingListItem"] = orm.relationship("ShoppingListItem", back_populates="label")
|
||||||
foods = orm.relationship("IngredientFoodModel", back_populates="label")
|
foods: Mapped["IngredientFoodModel"] = orm.relationship("IngredientFoodModel", back_populates="label")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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 *
|
@ -1,4 +1,5 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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
|
This class is not an actual table, so it does not inherit from SqlAlchemyBase
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||||
key_name = sa.Column(sa.String)
|
key_name: Mapped[str | None] = mapped_column(sa.String)
|
||||||
value = sa.Column(sa.String)
|
value: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
def __init__(self, key, value) -> None:
|
def __init__(self, key, value) -> None:
|
||||||
self.key_name = key
|
self.key_name = key
|
||||||
@ -39,19 +40,19 @@ class ExtrasGeneric:
|
|||||||
# used specifically for recipe extras
|
# used specifically for recipe extras
|
||||||
class ApiExtras(ExtrasGeneric, SqlAlchemyBase):
|
class ApiExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||||
__tablename__ = "api_extras"
|
__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):
|
class IngredientFoodExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||||
__tablename__ = "ingredient_food_extras"
|
__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):
|
class ShoppingListExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||||
__tablename__ = "shopping_list_extras"
|
__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):
|
class ShoppingListItemExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||||
__tablename__ = "shopping_list_item_extras"
|
__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"))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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):
|
class RecipeAsset(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_assets"
|
__tablename__ = "recipe_assets"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||||
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
name = sa.Column(sa.String)
|
name: Mapped[str | None] = mapped_column(sa.String)
|
||||||
icon = sa.Column(sa.String)
|
icon: Mapped[str | None] = mapped_column(sa.String)
|
||||||
file_name = sa.Column(sa.String)
|
file_name: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
def __init__(self, name=None, icon=None, file_name=None) -> None:
|
def __init__(self, name=None, icon=None, file_name=None) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from slugify import slugify
|
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.core import root_logger
|
||||||
|
|
||||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils.guid import GUID
|
from .._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..group import Group
|
||||||
|
from . import RecipeModel
|
||||||
logger = root_logger.get_logger()
|
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"),)
|
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="category_slug_group_id_key"),)
|
||||||
|
|
||||||
# ID Relationships
|
# ID Relationships
|
||||||
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)
|
||||||
group = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
|
||||||
|
|
||||||
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
name = sa.Column(sa.String, index=True, nullable=False)
|
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||||
slug = sa.Column(sa.String, index=True, nullable=False)
|
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category")
|
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||||
|
"RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category"
|
||||||
|
)
|
||||||
|
|
||||||
@validates("name")
|
@validates("name")
|
||||||
def validate_name(self, key, name):
|
def validate_name(self, key, name):
|
||||||
@ -62,18 +69,3 @@ class Category(SqlAlchemyBase, BaseMixins):
|
|||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(name)
|
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
|
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import auto_init
|
from mealie.db.models._model_utils import auto_init
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..users import User
|
||||||
|
from . import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
class RecipeComment(SqlAlchemyBase, BaseMixins):
|
class RecipeComment(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipe_comments"
|
__tablename__ = "recipe_comments"
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
text = Column(String)
|
text: Mapped[str | None] = mapped_column(String)
|
||||||
|
|
||||||
# Recipe Link
|
# Recipe Link
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), nullable=False)
|
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="comments")
|
recipe: Mapped["RecipeModel"] = orm.relationship("RecipeModel", back_populates="comments")
|
||||||
|
|
||||||
# User Link
|
# User Link
|
||||||
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
|
||||||
user = orm.relationship("User", back_populates="comments", single_parent=True, foreign_keys=[user_id])
|
user: Mapped["User"] = orm.relationship(
|
||||||
|
"User", back_populates="comments", single_parent=True, foreign_keys=[user_id]
|
||||||
|
)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update(self, text, **_) -> None: # type: ignore
|
def update(self, text, **_) -> None:
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -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._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.labels import MultiPurposeLabel
|
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 import auto_init
|
||||||
from .._model_utils.guid import GUID
|
from .._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..group import Group
|
||||||
|
|
||||||
|
|
||||||
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_units"
|
__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
|
# ID Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
group = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
||||||
|
|
||||||
name = Column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
description = Column(String)
|
description: Mapped[str | None] = mapped_column(String)
|
||||||
abbreviation = Column(String)
|
abbreviation: Mapped[str | None] = mapped_column(String)
|
||||||
use_abbreviation = Column(Boolean, default=False)
|
use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
fraction = Column(Boolean, default=True)
|
fraction: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||||
ingredients = orm.relationship("RecipeIngredient", back_populates="unit")
|
ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="unit")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -30,19 +36,19 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_foods"
|
__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
|
# ID Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
group = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
||||||
|
|
||||||
name = Column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
description = Column(String)
|
description: Mapped[str | None] = mapped_column(String)
|
||||||
ingredients = orm.relationship("RecipeIngredient", back_populates="food")
|
ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="food")
|
||||||
extras: list[IngredientFoodExtras] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
||||||
|
|
||||||
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
|
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||||
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
|
label: Mapped[MultiPurposeLabel | None] = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
|
||||||
|
|
||||||
@api_extras
|
@api_extras
|
||||||
@auto_init()
|
@auto_init()
|
||||||
@ -52,24 +58,24 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class RecipeIngredient(SqlAlchemyBase, BaseMixins):
|
class RecipeIngredient(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipes_ingredients"
|
__tablename__ = "recipes_ingredients"
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
position = Column(Integer)
|
position: Mapped[int | None] = mapped_column(Integer)
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||||
|
|
||||||
title = Column(String) # Section Header - Shows if Present
|
title: Mapped[str | None] = mapped_column(String) # Section Header - Shows if Present
|
||||||
note = Column(String) # Force Show Text - Overrides Concat
|
note: Mapped[str | None] = mapped_column(String) # Force Show Text - Overrides Concat
|
||||||
|
|
||||||
# Scaling Items
|
# Scaling Items
|
||||||
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
|
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
|
||||||
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
|
||||||
|
|
||||||
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
|
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
|
||||||
food = orm.relationship(IngredientFoodModel, uselist=False)
|
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
|
||||||
quantity = Column(Float)
|
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()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import auto_init
|
from .._model_utils import auto_init
|
||||||
@ -7,8 +8,8 @@ from .._model_utils.guid import GUID
|
|||||||
|
|
||||||
class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
|
class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipe_ingredient_ref_link"
|
__tablename__ = "recipe_ingredient_ref_link"
|
||||||
instruction_id = Column(GUID, ForeignKey("recipe_instructions.id"))
|
instruction_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipe_instructions.id"))
|
||||||
reference_id = Column(GUID)
|
reference_id: Mapped[GUID | None] = mapped_column(GUID)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
@ -17,14 +18,16 @@ class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class RecipeInstruction(SqlAlchemyBase):
|
class RecipeInstruction(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_instructions"
|
__tablename__ = "recipe_instructions"
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||||
position = Column(Integer)
|
position: Mapped[int | None] = mapped_column(Integer)
|
||||||
type = Column(String, default="")
|
type: Mapped[str | None] = mapped_column(String, default="")
|
||||||
title = Column(String)
|
title: Mapped[str | None] = mapped_column(String)
|
||||||
text = 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:
|
class Config:
|
||||||
exclude = {
|
exclude = {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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):
|
class Note(SqlAlchemyBase):
|
||||||
__tablename__ = "notes"
|
__tablename__ = "notes"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||||
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
title = sa.Column(sa.String)
|
title: Mapped[str | None] = mapped_column(sa.String)
|
||||||
text = sa.Column(sa.String)
|
text: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
def __init__(self, title, text) -> None:
|
def __init__(self, title, text) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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):
|
class Nutrition(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_nutrition"
|
__tablename__ = "recipe_nutrition"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||||
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
calories = sa.Column(sa.String)
|
calories: Mapped[str | None] = mapped_column(sa.String)
|
||||||
fat_content = sa.Column(sa.String)
|
fat_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
fiber_content = sa.Column(sa.String)
|
fiber_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
protein_content = sa.Column(sa.String)
|
protein_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
carbohydrate_content = sa.Column(sa.String)
|
carbohydrate_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
sodium_content = sa.Column(sa.String)
|
sodium_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
sugar_content = sa.Column(sa.String)
|
sugar_content: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import datetime
|
from datetime import date, datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
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
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
@ -24,90 +25,103 @@ from .shared import RecipeShareTokenModel
|
|||||||
from .tag import recipes_to_tags
|
from .tag import recipes_to_tags
|
||||||
from .tool import recipes_to_tools
|
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):
|
class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipes"
|
__tablename__ = "recipes"
|
||||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
|
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
|
||||||
|
|
||||||
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
slug = sa.Column(sa.String, index=True)
|
slug: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
|
||||||
# ID Relationships
|
# ID Relationships
|
||||||
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)
|
||||||
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
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_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
|
||||||
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
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
|
# General Recipe Properties
|
||||||
name = sa.Column(sa.String, nullable=False)
|
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
||||||
description = sa.Column(sa.String)
|
description: Mapped[str | None] = mapped_column(sa.String)
|
||||||
image = sa.Column(sa.String)
|
image: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
# Time Related Properties
|
# Time Related Properties
|
||||||
total_time = sa.Column(sa.String)
|
total_time: Mapped[str | None] = mapped_column(sa.String)
|
||||||
prep_time = sa.Column(sa.String)
|
prep_time: Mapped[str | None] = mapped_column(sa.String)
|
||||||
perform_time = sa.Column(sa.String)
|
perform_time: Mapped[str | None] = mapped_column(sa.String)
|
||||||
cook_time = sa.Column(sa.String)
|
cook_time: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
recipe_yield = sa.Column(sa.String)
|
recipe_yield: Mapped[str | None] = mapped_column(sa.String)
|
||||||
recipeCuisine = sa.Column(sa.String)
|
recipeCuisine: Mapped[str | None] = mapped_column(sa.String)
|
||||||
|
|
||||||
assets = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
assets: Mapped[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
||||||
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
nutrition: Mapped[Nutrition] = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
||||||
recipe_category = orm.relationship("Category", secondary=recipes_to_categories, back_populates="recipes")
|
recipe_category: Mapped[list["Category"]] = orm.relationship(
|
||||||
tools = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes")
|
"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",
|
"RecipeIngredient",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="RecipeIngredient.position",
|
order_by="RecipeIngredient.position",
|
||||||
collection_class=ordering_list("position"),
|
collection_class=ordering_list("position"),
|
||||||
)
|
)
|
||||||
recipe_instructions: list[RecipeInstruction] = orm.relationship(
|
recipe_instructions: Mapped[list[RecipeInstruction]] = orm.relationship(
|
||||||
"RecipeInstruction",
|
"RecipeInstruction",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="RecipeInstruction.position",
|
order_by="RecipeInstruction.position",
|
||||||
collection_class=ordering_list("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"
|
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"
|
"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"
|
"RecipeTimelineEvent", back_populates="recipe", cascade="all, delete, delete-orphan"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mealie Specific
|
# Mealie Specific
|
||||||
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
settings: Mapped[list["RecipeSettings"]] = orm.relationship(
|
||||||
tags = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
|
"RecipeSettings", uselist=False, cascade="all, delete-orphan"
|
||||||
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
|
)
|
||||||
rating = sa.Column(sa.Integer)
|
tags: Mapped[list["Tag"]] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
|
||||||
org_url = sa.Column(sa.String)
|
notes: Mapped[list[Note]] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||||
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
rating: Mapped[int | None] = mapped_column(sa.Integer)
|
||||||
is_ocr_recipe = sa.Column(sa.Boolean, default=False)
|
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
|
# Time Stamp Properties
|
||||||
date_added = sa.Column(sa.Date, default=datetime.date.today)
|
date_added: Mapped[date | None] = mapped_column(sa.Date, default=date.today)
|
||||||
date_updated = sa.Column(sa.DateTime)
|
date_updated: Mapped[datetime | None] = mapped_column(sa.DateTime)
|
||||||
last_made = sa.Column(sa.DateTime)
|
last_made: Mapped[datetime | None] = mapped_column(sa.DateTime)
|
||||||
|
|
||||||
# Shopping List Refs
|
# Shopping List Refs
|
||||||
shopping_list_refs = orm.relationship(
|
shopping_list_refs: Mapped[list["ShoppingListRecipeReference"]] = orm.relationship(
|
||||||
"ShoppingListRecipeReference",
|
"ShoppingListRecipeReference",
|
||||||
back_populates="recipe",
|
back_populates="recipe",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
shopping_list_item_refs = orm.relationship(
|
shopping_list_item_refs: Mapped[list["ShoppingListItemRecipeReference"]] = orm.relationship(
|
||||||
"ShoppingListItemRecipeReference",
|
"ShoppingListItemRecipeReference",
|
||||||
back_populates="recipe",
|
back_populates="recipe",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
@ -160,4 +174,4 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||||||
if notes:
|
if notes:
|
||||||
self.notes = [Note(**n) for n in notes]
|
self.notes = [Note(**n) for n in notes]
|
||||||
|
|
||||||
self.date_updated = datetime.datetime.now()
|
self.date_updated = datetime.now()
|
||||||
|
@ -1,33 +1,40 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey, String
|
from sqlalchemy import DateTime, ForeignKey, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import auto_init
|
from .._model_utils import auto_init
|
||||||
from .._model_utils.guid import GUID
|
from .._model_utils.guid import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..users import User
|
||||||
|
from . import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
|
class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipe_timeline_events"
|
__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
|
# Parent Recipe
|
||||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), nullable=False)
|
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False)
|
||||||
recipe = relationship("RecipeModel", back_populates="timeline_events")
|
recipe: Mapped["RecipeModel"] = relationship("RecipeModel", back_populates="timeline_events")
|
||||||
|
|
||||||
# Related User (Actor)
|
# Related User (Actor)
|
||||||
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
|
||||||
user = relationship("User", back_populates="recipe_timeline_events", single_parent=True, foreign_keys=[user_id])
|
user: Mapped["User"] = relationship(
|
||||||
|
"User", back_populates="recipe_timeline_events", single_parent=True, foreign_keys=[user_id]
|
||||||
|
)
|
||||||
|
|
||||||
# General Properties
|
# General Properties
|
||||||
subject = Column(String, nullable=False)
|
subject: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
message = Column(String)
|
message: Mapped[str | None] = mapped_column(String)
|
||||||
event_type = Column(String)
|
event_type: Mapped[str | None] = mapped_column(String)
|
||||||
image = Column(String)
|
image: Mapped[str | None] = mapped_column(String)
|
||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
timestamp = Column(DateTime)
|
timestamp: Mapped[datetime | None] = mapped_column(DateTime)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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):
|
class RecipeSettings(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_settings"
|
__tablename__ = "recipe_settings"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||||
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
public = sa.Column(sa.Boolean)
|
public: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||||
show_nutrition = sa.Column(sa.Boolean)
|
show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||||
show_assets = sa.Column(sa.Boolean)
|
show_assets: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||||
landscape_view = sa.Column(sa.Boolean)
|
landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||||
disable_amount = sa.Column(sa.Boolean, default=True)
|
disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||||
disable_comments = sa.Column(sa.Boolean, default=False)
|
disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
locked = sa.Column(sa.Boolean, default=False)
|
locked: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import sqlalchemy as sa
|
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_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import GUID, auto_init
|
from mealie.db.models._model_utils import GUID, auto_init
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
def defaut_expires_at_time() -> datetime:
|
def defaut_expires_at_time() -> datetime:
|
||||||
return datetime.utcnow() + timedelta(days=30)
|
return datetime.utcnow() + timedelta(days=30)
|
||||||
@ -13,14 +18,14 @@ def defaut_expires_at_time() -> datetime:
|
|||||||
|
|
||||||
class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
|
class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipe_share_tokens"
|
__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_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("recipes.id"), nullable=False)
|
||||||
recipe = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=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()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from slugify import slugify
|
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.core import root_logger
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import guid
|
from mealie.db.models._model_utils import guid
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..group import Group
|
||||||
|
from . import RecipeModel
|
||||||
|
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
recipes_to_tags = sa.Table(
|
recipes_to_tags = sa.Table(
|
||||||
@ -34,15 +41,17 @@ cookbooks_to_tags = sa.Table(
|
|||||||
class Tag(SqlAlchemyBase, BaseMixins):
|
class Tag(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "tags"
|
__tablename__ = "tags"
|
||||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
|
__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
|
# ID Relationships
|
||||||
group_id = sa.Column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
group_id: Mapped[guid.GUID] = mapped_column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
|
||||||
|
|
||||||
name = sa.Column(sa.String, index=True, nullable=False)
|
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||||
slug = sa.Column(sa.String, index=True, nullable=False)
|
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tags, back_populates="tags")
|
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||||
|
"RecipeModel", secondary=recipes_to_tags, back_populates="tags"
|
||||||
|
)
|
||||||
|
|
||||||
@validates("name")
|
@validates("name")
|
||||||
def validate_name(self, key, name):
|
def validate_name(self, key, name):
|
||||||
@ -53,17 +62,3 @@ class Tag(SqlAlchemyBase, BaseMixins):
|
|||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(self.name)
|
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
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy import Boolean, Column, ForeignKey, String, Table, UniqueConstraint, orm
|
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_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import auto_init
|
from mealie.db.models._model_utils import auto_init
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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 = Table(
|
||||||
"recipes_to_tools",
|
"recipes_to_tools",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
@ -23,16 +30,18 @@ cookbooks_to_tools = Table(
|
|||||||
class Tool(SqlAlchemyBase, BaseMixins):
|
class Tool(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "tools"
|
__tablename__ = "tools"
|
||||||
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
|
__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
|
# ID Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
group = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
|
||||||
|
|
||||||
name = Column(String, index=True, unique=True, nullable=False)
|
name: Mapped[str] = mapped_column(String, index=True, unique=True, nullable=False)
|
||||||
slug = Column(String, index=True, unique=True, nullable=False)
|
slug: Mapped[str] = mapped_column(String, index=True, unique=True, nullable=False)
|
||||||
on_hand = Column(Boolean, default=False)
|
on_hand: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools")
|
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||||
|
"RecipeModel", secondary=recipes_to_tools, back_populates="tools"
|
||||||
|
)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, name, **_) -> None:
|
def __init__(self, name, **_) -> None:
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
from .._model_utils import auto_init
|
from .._model_utils import auto_init
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..group import Group
|
||||||
|
|
||||||
|
|
||||||
class ServerTaskModel(SqlAlchemyBase, BaseMixins):
|
class ServerTaskModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "server_tasks"
|
__tablename__ = "server_tasks"
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
completed_date = Column(DateTime, nullable=True)
|
completed_date: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
status = Column(String, nullable=False)
|
status: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
log = Column(String, nullable=True)
|
log: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="server_tasks")
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="server_tasks")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -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_base import BaseMixins, SqlAlchemyBase
|
||||||
from .._model_utils import GUID
|
from .._model_utils import GUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .users import User
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetModel(SqlAlchemyBase, BaseMixins):
|
class PasswordResetModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "password_reset_tokens"
|
__tablename__ = "password_reset_tokens"
|
||||||
|
|
||||||
user_id = Column(GUID, ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False)
|
||||||
user = orm.relationship("User", back_populates="password_reset_tokens", uselist=False)
|
user: Mapped["User"] = orm.relationship("User", back_populates="password_reset_tokens", uselist=False)
|
||||||
token = Column(String(64), unique=True, nullable=False)
|
token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||||
|
|
||||||
def __init__(self, user_id, token, **_):
|
def __init__(self, user_id, token, **_):
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
|
@ -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.core.config import get_app_settings
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
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 .._model_utils import auto_init
|
||||||
from .user_to_favorite import users_to_favorites
|
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):
|
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "long_live_tokens"
|
__tablename__ = "long_live_tokens"
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
token = Column(String, nullable=False)
|
token: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
|
||||||
user_id = Column(GUID, ForeignKey("users.id"))
|
user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"))
|
||||||
user = orm.relationship("User")
|
user: Mapped[Optional["User"]] = orm.relationship("User")
|
||||||
|
|
||||||
def __init__(self, name, token, user_id, **_) -> None:
|
def __init__(self, name, token, user_id, **_) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -24,25 +33,25 @@ class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class User(SqlAlchemyBase, BaseMixins):
|
class User(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
full_name = Column(String, index=True)
|
full_name: Mapped[str | None] = mapped_column(String, index=True)
|
||||||
username = Column(String, index=True, unique=True)
|
username: Mapped[str | None] = mapped_column(String, index=True, unique=True)
|
||||||
email = Column(String, unique=True, index=True)
|
email: Mapped[str | None] = mapped_column(String, unique=True, index=True)
|
||||||
password = Column(String)
|
password: Mapped[str | None] = mapped_column(String)
|
||||||
admin = Column(Boolean, default=False)
|
admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
advanced = 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_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="users")
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="users")
|
||||||
|
|
||||||
cache_key = Column(String, default="1234")
|
cache_key: Mapped[str | None] = mapped_column(String, default="1234")
|
||||||
login_attemps = Column(Integer, default=0)
|
login_attemps: Mapped[int | None] = mapped_column(Integer, default=0)
|
||||||
locked_at = Column(DateTime, default=None)
|
locked_at: Mapped[datetime | None] = mapped_column(DateTime, default=None)
|
||||||
|
|
||||||
# Group Permissions
|
# Group Permissions
|
||||||
can_manage = Column(Boolean, default=False)
|
can_manage: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
can_invite = Column(Boolean, default=False)
|
can_invite: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
can_organize = Column(Boolean, default=False)
|
can_organize: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
sp_args = {
|
sp_args = {
|
||||||
"back_populates": "user",
|
"back_populates": "user",
|
||||||
@ -50,15 +59,19 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
"single_parent": True,
|
"single_parent": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens = orm.relationship(LongLiveToken, **sp_args)
|
tokens: Mapped[list[LongLiveToken]] = orm.relationship(LongLiveToken, **sp_args)
|
||||||
comments = orm.relationship("RecipeComment", **sp_args)
|
comments: Mapped[list["RecipeComment"]] = orm.relationship("RecipeComment", **sp_args)
|
||||||
recipe_timeline_events = orm.relationship("RecipeTimelineEvent", **sp_args)
|
recipe_timeline_events: Mapped[list["RecipeTimelineEvent"]] = orm.relationship("RecipeTimelineEvent", **sp_args)
|
||||||
password_reset_tokens = orm.relationship("PasswordResetModel", **sp_args)
|
password_reset_tokens: Mapped[list["PasswordResetModel"]] = orm.relationship("PasswordResetModel", **sp_args)
|
||||||
|
|
||||||
owned_recipes_id = Column(GUID, ForeignKey("recipes.id"))
|
owned_recipes_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||||
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_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:
|
class Config:
|
||||||
exclude = {
|
exclude = {
|
||||||
@ -78,7 +91,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
from mealie.db.models.group import Group
|
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 = []
|
self.favorite_recipes = []
|
||||||
|
|
||||||
@ -97,7 +110,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
from mealie.db.models.group import Group
|
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:
|
if self.username is None:
|
||||||
self.username = full_name
|
self.username = full_name
|
||||||
@ -126,7 +139,3 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
self.can_manage = can_manage
|
self.can_manage = can_manage
|
||||||
self.can_invite = can_invite
|
self.can_invite = can_invite
|
||||||
self.can_organize = can_organize
|
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()
|
|
||||||
|
@ -12,11 +12,11 @@ def pretty_size(size: int) -> str:
|
|||||||
"""
|
"""
|
||||||
if size < 1024:
|
if size < 1024:
|
||||||
return f"{size} bytes"
|
return f"{size} bytes"
|
||||||
elif size < 1024 ** 2:
|
elif size < 1024**2:
|
||||||
return f"{round(size / 1024, 2)} KB"
|
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"
|
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"
|
return f"{round(size / 1024 / 1024 / 1024, 2)} GB"
|
||||||
else:
|
else:
|
||||||
return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"
|
return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
|
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
|
||||||
@ -70,13 +72,16 @@ PK_GROUP_ID = "group_id"
|
|||||||
|
|
||||||
|
|
||||||
class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]):
|
class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]):
|
||||||
def get_empty(self):
|
def get_empty(self) -> Sequence[Category]:
|
||||||
return self.session.query(Category).filter(~Category.recipes.any()).all()
|
stmt = select(Category).filter(~Category.recipes.any())
|
||||||
|
|
||||||
|
return self.session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
|
|
||||||
class RepositoryTags(RepositoryGeneric[TagOut, Tag]):
|
class RepositoryTags(RepositoryGeneric[TagOut, Tag]):
|
||||||
def get_empty(self):
|
def get_empty(self) -> Sequence[Tag]:
|
||||||
return self.session.query(Tag).filter(~Tag.recipes.any()).all()
|
stmt = select(Tag).filter(~Tag.recipes.any())
|
||||||
|
return self.session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
|
|
||||||
class AllRepositories:
|
class AllRepositories:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel
|
from mealie.db.models.recipe.ingredient import IngredientFoodModel
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||||
@ -7,15 +8,13 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
|
|
||||||
class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
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:
|
def merge(self, from_food: UUID4, to_food: UUID4) -> IngredientFood | None:
|
||||||
|
from_model = self._get_food(from_food)
|
||||||
from_model: IngredientFoodModel = (
|
to_model = self._get_food(to_food)
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
to_model.ingredients += from_model.ingredients
|
to_model.ingredients += from_model.ingredients
|
||||||
|
|
||||||
@ -29,4 +28,4 @@ class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
|||||||
return self.get_one(to_food)
|
return self.get_one(to_food)
|
||||||
|
|
||||||
def by_group(self, group_id: UUID4) -> "RepositoryFood":
|
def by_group(self, group_id: UUID4) -> "RepositoryFood":
|
||||||
return super().by_group(group_id) # type: ignore
|
return super().by_group(group_id)
|
||||||
|
@ -6,21 +6,19 @@ from typing import Any, Generic, TypeVar
|
|||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
from sqlalchemy import func
|
from sqlalchemy import Select, delete, func, select
|
||||||
from sqlalchemy.orm import Query
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from sqlalchemy.sql import sqltypes
|
from sqlalchemy.sql import sqltypes
|
||||||
|
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.schema.response.pagination import (
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
OrderDirection,
|
from mealie.schema.response.pagination import OrderDirection, PaginationBase, PaginationQuery
|
||||||
PaginationBase,
|
|
||||||
PaginationQuery,
|
|
||||||
)
|
|
||||||
from mealie.schema.response.query_filter import QueryFilter
|
from mealie.schema.response.query_filter import QueryFilter
|
||||||
|
|
||||||
Schema = TypeVar("Schema", bound=BaseModel)
|
Schema = TypeVar("Schema", bound=BaseModel)
|
||||||
Model = TypeVar("Model")
|
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="RepositoryGeneric")
|
||||||
|
|
||||||
|
|
||||||
class RepositoryGeneric(Generic[Schema, Model]):
|
class RepositoryGeneric(Generic[Schema, Model]):
|
||||||
@ -33,6 +31,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
|
|
||||||
user_id: UUID4 | None = None
|
user_id: UUID4 | None = None
|
||||||
group_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:
|
def __init__(self, session: Session, primary_key: str, sql_model: type[Model], schema: type[Schema]) -> None:
|
||||||
self.session = session
|
self.session = session
|
||||||
@ -42,11 +41,11 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
|
|
||||||
self.logger = get_logger()
|
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
|
self.user_id = user_id
|
||||||
return self
|
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
|
self.group_id = group_id
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
|
|
||||||
def _query(self):
|
def _query(self):
|
||||||
return self.session.query(self.model)
|
return select(self.model)
|
||||||
|
|
||||||
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
||||||
dct = {}
|
dct = {}
|
||||||
@ -98,8 +97,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
|
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
|
||||||
|
result = self.session.execute(q.offset(start).limit(limit)).scalars().all()
|
||||||
return [eff_schema.from_orm(x) for x in q.offset(start).limit(limit).all()]
|
return [eff_schema.from_orm(x) for x in result]
|
||||||
|
|
||||||
def multi_query(
|
def multi_query(
|
||||||
self,
|
self,
|
||||||
@ -120,7 +119,9 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
order_attr = order_attr.desc()
|
order_attr = order_attr.desc()
|
||||||
q = q.order_by(order_attr)
|
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:
|
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
|
match_key = self.primary_key
|
||||||
|
|
||||||
fltr = self._filter_builder(**{match_key: match_value})
|
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(
|
def get_one(
|
||||||
self, value: str | int | UUID4, key: str | None = None, any_case=False, override_schema=None
|
self, value: str | int | UUID4, key: str | None = None, any_case=False, override_schema=None
|
||||||
) -> Schema | None:
|
) -> Schema | None:
|
||||||
key = key or self.primary_key
|
key = key or self.primary_key
|
||||||
|
|
||||||
q = self.session.query(self.model)
|
q = self._query()
|
||||||
|
|
||||||
if any_case:
|
if any_case:
|
||||||
search_attr = getattr(self.model, key)
|
search_attr = getattr(self.model, key)
|
||||||
@ -146,7 +147,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
else:
|
else:
|
||||||
q = q.filter_by(**self._filter_builder(**{key: value}))
|
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:
|
if not result:
|
||||||
return None
|
return None
|
||||||
@ -156,7 +157,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
|
|
||||||
def create(self, data: Schema | BaseModel | dict) -> Schema:
|
def create(self, data: Schema | BaseModel | dict) -> Schema:
|
||||||
data = data if isinstance(data, dict) else data.dict()
|
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.add(new_document)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.session.refresh(new_document)
|
self.session.refresh(new_document)
|
||||||
@ -167,7 +168,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
new_documents = []
|
new_documents = []
|
||||||
for document in data:
|
for document in data:
|
||||||
document = document if isinstance(document, dict) else document.dict()
|
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)
|
new_documents.append(new_document)
|
||||||
|
|
||||||
self.session.add_all(new_documents)
|
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()
|
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||||
|
|
||||||
entry = self._query_one(match_value=match_value)
|
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()
|
self.session.commit()
|
||||||
return self.schema.from_orm(entry)
|
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 = document if isinstance(document, dict) else document.dict()
|
||||||
document_data_by_id[document_data["id"]] = document_data
|
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 = []
|
updated_documents = []
|
||||||
for document_to_update in documents_to_update:
|
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:
|
def delete(self, value, match_key: str | None = None) -> Schema:
|
||||||
match_key = match_key or self.primary_key
|
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)
|
results_as_model = self.schema.from_orm(result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -239,7 +241,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
return results_as_model
|
return results_as_model
|
||||||
|
|
||||||
def delete_many(self, values: Iterable) -> Schema:
|
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]
|
results_as_model = [self.schema.from_orm(result) for result in results]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -256,14 +259,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
return results_as_model # type: ignore
|
return results_as_model # type: ignore
|
||||||
|
|
||||||
def delete_all(self) -> None:
|
def delete_all(self) -> None:
|
||||||
self._query().delete()
|
delete(self.model)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
def count_all(self, match_key=None, match_value=None) -> int:
|
def count_all(self, match_key=None, match_value=None) -> int:
|
||||||
if None in [match_key, match_value]:
|
q = select(func.count(self.model.id))
|
||||||
return self._query().count()
|
if None not in [match_key, match_value]:
|
||||||
else:
|
q = q.filter_by(**{match_key: match_value})
|
||||||
return self._query().filter_by(**{match_key: match_value}).count()
|
return self.session.scalar(q)
|
||||||
|
|
||||||
def _count_attribute(
|
def _count_attribute(
|
||||||
self,
|
self,
|
||||||
@ -274,12 +277,12 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
) -> int | list[Schema]: # sourcery skip: assign-if-exp
|
) -> int | list[Schema]: # sourcery skip: assign-if-exp
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
|
|
||||||
q = self._query().filter(attribute_name == attr_match)
|
|
||||||
|
|
||||||
if count:
|
if count:
|
||||||
return q.count()
|
q = select(func.count(self.model.id)).filter(attribute_name == attr_match)
|
||||||
|
return self.session.scalar(q)
|
||||||
else:
|
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]:
|
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
|
eff_schema = override or self.schema
|
||||||
|
|
||||||
q = self.session.query(self.model)
|
q = self._query()
|
||||||
|
|
||||||
fltr = self._filter_builder()
|
fltr = self._filter_builder()
|
||||||
q = q.filter_by(**fltr)
|
q = q.filter_by(**fltr)
|
||||||
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = q.all()
|
data = self.session.execute(q).scalars().all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log_exception(e)
|
self._log_exception(e)
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
@ -314,7 +317,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
items=[eff_schema.from_orm(s) for s in data],
|
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.
|
Adds pagination data to an existing query.
|
||||||
|
|
||||||
@ -333,7 +336,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
raise HTTPException(status_code=400, detail=str(e)) from 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"
|
# interpret -1 as "get_all"
|
||||||
if pagination.per_page == -1:
|
if pagination.per_page == -1:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.recipe.category import Category
|
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.group.group_statistics import GroupStatistics
|
||||||
from mealie.schema.user.user import GroupInDB
|
from mealie.schema.user.user import GroupInDB
|
||||||
|
|
||||||
|
from ..db.models._model_base import SqlAlchemyBase
|
||||||
from .repository_generic import RepositoryGeneric
|
from .repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
|
|
||||||
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||||
def get_by_name(self, name: str, limit=1) -> GroupInDB | Group | None:
|
def get_by_name(self, name: str) -> GroupInDB | None:
|
||||||
dbgroup = self.session.query(self.model).filter_by(**{"name": name}).one_or_none()
|
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
|
||||||
if dbgroup is None:
|
if dbgroup is None:
|
||||||
return None
|
return None
|
||||||
return self.schema.from_orm(dbgroup)
|
return self.schema.from_orm(dbgroup)
|
||||||
|
|
||||||
def statistics(self, group_id: UUID4) -> GroupStatistics:
|
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(
|
return GroupStatistics(
|
||||||
total_recipes=self.session.query(RecipeModel).filter_by(group_id=group_id).count(),
|
total_recipes=model_count(RecipeModel),
|
||||||
total_users=self.session.query(User).filter_by(group_id=group_id).count(),
|
total_users=model_count(User),
|
||||||
total_categories=self.session.query(Category).filter_by(group_id=group_id).count(),
|
total_categories=model_count(Category),
|
||||||
total_tags=self.session.query(Tag).filter_by(group_id=group_id).count(),
|
total_tags=model_count(Tag),
|
||||||
total_tools=self.session.query(Tool).filter_by(group_id=group_id).count(),
|
total_tools=model_count(Tool),
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_, select
|
||||||
|
|
||||||
from mealie.db.models.group.mealplan import GroupMealPlanRules
|
from mealie.db.models.group.mealplan import GroupMealPlanRules
|
||||||
from mealie.schema.meal_plan.plan_rules import PlanRulesDay, PlanRulesOut, PlanRulesType
|
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]):
|
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
|
||||||
def by_group(self, group_id: UUID) -> "RepositoryMealPlanRules":
|
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]:
|
def get_rules(self, day: PlanRulesDay, entry_type: PlanRulesType) -> list[PlanRulesOut]:
|
||||||
qry = self.session.query(GroupMealPlanRules).filter(
|
stmt = select(GroupMealPlanRules).filter(
|
||||||
or_(
|
or_(
|
||||||
GroupMealPlanRules.day == day,
|
GroupMealPlanRules.day == day,
|
||||||
GroupMealPlanRules.day.is_(None),
|
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]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.db.models.group import GroupMealPlan
|
from mealie.db.models.group import GroupMealPlan
|
||||||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||||
|
|
||||||
@ -9,10 +11,10 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||||
def by_group(self, group_id: UUID) -> "RepositoryMeals":
|
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]:
|
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
|
||||||
today = date.today()
|
today = date.today()
|
||||||
qry = self.session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
|
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 qry.all()]
|
return [self.schema.from_orm(x) for x in plans]
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func, select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import joinedload
|
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 import Recipe
|
||||||
from mealie.schema.recipe.recipe import (
|
from mealie.schema.recipe.recipe import (
|
||||||
RecipeCategory,
|
RecipeCategory,
|
||||||
|
RecipePagination,
|
||||||
RecipeSummary,
|
RecipeSummary,
|
||||||
RecipeSummaryWithIngredients,
|
RecipeSummaryWithIngredients,
|
||||||
RecipeTag,
|
RecipeTag,
|
||||||
RecipeTool,
|
RecipeTool,
|
||||||
)
|
)
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
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
|
from .repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
@ -46,34 +47,31 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def by_group(self, group_id: UUID) -> "RepositoryRecipes":
|
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):
|
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
|
eff_schema = override_schema or self.schema
|
||||||
|
|
||||||
if order_by:
|
if order_by:
|
||||||
order_attr = getattr(self.model, str(order_by))
|
order_attr = getattr(self.model, str(order_by))
|
||||||
|
stmt = (
|
||||||
return [
|
select(self.model)
|
||||||
eff_schema.from_orm(x)
|
|
||||||
for x in self.session.query(self.model)
|
|
||||||
.join(RecipeSettings)
|
.join(RecipeSettings)
|
||||||
.filter(RecipeSettings.public == True) # noqa: 711
|
.filter(RecipeSettings.public == True) # noqa: 711
|
||||||
.order_by(order_attr.desc())
|
.order_by(order_attr.desc())
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.all()
|
)
|
||||||
]
|
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||||
|
|
||||||
return [
|
stmt = (
|
||||||
eff_schema.from_orm(x)
|
select(self.model)
|
||||||
for x in self.session.query(self.model)
|
|
||||||
.join(RecipeSettings)
|
.join(RecipeSettings)
|
||||||
.filter(RecipeSettings.public == True) # noqa: 711
|
.filter(RecipeSettings.public == True) # noqa: 711
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(limit)
|
.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:
|
def update_image(self, slug: str, _: str | None = None) -> int:
|
||||||
entry: RecipeModel = self._query_one(match_value=slug)
|
entry: RecipeModel = self._query_one(match_value=slug)
|
||||||
@ -100,7 +98,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
|
|
||||||
def summary(
|
def summary(
|
||||||
self, group_id, start=0, limit=99999, load_foods=False, order_by="created_at", order_descending=True
|
self, group_id, start=0, limit=99999, load_foods=False, order_by="created_at", order_descending=True
|
||||||
) -> Any:
|
) -> Sequence[RecipeModel]:
|
||||||
args = [
|
args = [
|
||||||
joinedload(RecipeModel.recipe_category),
|
joinedload(RecipeModel.recipe_category),
|
||||||
joinedload(RecipeModel.tags),
|
joinedload(RecipeModel.tags),
|
||||||
@ -126,15 +124,15 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
else:
|
else:
|
||||||
order_attr = order_attr.asc()
|
order_attr = order_attr.asc()
|
||||||
|
|
||||||
return (
|
stmt = (
|
||||||
self.session.query(RecipeModel)
|
select(RecipeModel)
|
||||||
.options(*args)
|
.options(*args)
|
||||||
.filter(RecipeModel.group_id == group_id)
|
.filter(RecipeModel.group_id == group_id)
|
||||||
.order_by(order_attr)
|
.order_by(order_attr)
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
return self.session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
def page_all(
|
def page_all(
|
||||||
self,
|
self,
|
||||||
@ -145,8 +143,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
categories: list[UUID4 | str] | None = None,
|
categories: list[UUID4 | str] | None = None,
|
||||||
tags: list[UUID4 | str] | None = None,
|
tags: list[UUID4 | str] | None = None,
|
||||||
tools: list[UUID4 | str] | None = None,
|
tools: list[UUID4 | str] | None = None,
|
||||||
) -> PaginationBase[RecipeSummary]:
|
) -> RecipePagination:
|
||||||
q = self.session.query(self.model)
|
q = select(self.model)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
joinedload(RecipeModel.recipe_category),
|
joinedload(RecipeModel.recipe_category),
|
||||||
@ -154,6 +152,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
joinedload(RecipeModel.tools),
|
joinedload(RecipeModel.tools),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
item_class: type[RecipeSummary | RecipeSummaryWithIngredients]
|
||||||
|
|
||||||
if load_food:
|
if load_food:
|
||||||
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
|
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
|
||||||
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.unit)))
|
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)
|
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = q.all()
|
data = self.session.execute(q).scalars().unique().all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log_exception(e)
|
self._log_exception(e)
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
items = [item_class.from_orm(item) for item in data]
|
items = [item_class.from_orm(item) for item in data]
|
||||||
return PaginationBase(
|
return RecipePagination(
|
||||||
page=pagination.page,
|
page=pagination.page,
|
||||||
per_page=pagination.per_page,
|
per_page=pagination.per_page,
|
||||||
total=count,
|
total=count,
|
||||||
@ -226,14 +226,12 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ids = [x.id for x in categories]
|
ids = [x.id for x in categories]
|
||||||
|
stmt = (
|
||||||
return [
|
select(RecipeModel)
|
||||||
RecipeSummary.from_orm(x)
|
|
||||||
for x in self.session.query(RecipeModel)
|
|
||||||
.join(RecipeModel.recipe_category)
|
.join(RecipeModel.recipe_category)
|
||||||
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
.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(
|
def _category_tag_filters(
|
||||||
self,
|
self,
|
||||||
@ -284,8 +282,8 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
fltr = self._category_tag_filters(
|
fltr = self._category_tag_filters(
|
||||||
categories, tags, tools, require_all_categories, require_all_tags, require_all_tools
|
categories, tags, tools, require_all_categories, require_all_tags, require_all_tools
|
||||||
)
|
)
|
||||||
|
stmt = select(RecipeModel).filter(*fltr)
|
||||||
return [self.schema.from_orm(x) for x in self.session.query(RecipeModel).filter(*fltr).all()]
|
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||||
|
|
||||||
def get_random_by_categories_and_tags(
|
def get_random_by_categories_and_tags(
|
||||||
self, categories: list[RecipeCategory], tags: list[RecipeTag]
|
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
|
# - https://stackoverflow.com/questions/60805/getting-random-row-through-sqlalchemy
|
||||||
|
|
||||||
filters = self._category_tag_filters(categories, tags) # type: ignore
|
filters = self._category_tag_filters(categories, tags) # type: ignore
|
||||||
|
stmt = (
|
||||||
return [
|
select(RecipeModel).filter(and_(*filters)).order_by(func.random()).limit(1) # Postgres and SQLite specific
|
||||||
self.schema.from_orm(x)
|
)
|
||||||
for x in self.session.query(RecipeModel)
|
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||||
.filter(and_(*filters))
|
|
||||||
.order_by(func.random()) # Postgres and SQLite specific
|
|
||||||
.limit(1)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_random(self, limit=1) -> list[Recipe]:
|
def get_random(self, limit=1) -> list[Recipe]:
|
||||||
return [
|
stmt = (
|
||||||
self.schema.from_orm(x)
|
select(RecipeModel)
|
||||||
for x in self.session.query(RecipeModel)
|
|
||||||
.filter(RecipeModel.group_id == self.group_id)
|
.filter(RecipeModel.group_id == self.group_id)
|
||||||
.order_by(func.random()) # Postgres and SQLite specific
|
.order_by(func.random()) # Postgres and SQLite specific
|
||||||
.limit(limit)
|
.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:
|
def get_by_slug(self, group_id: UUID4, slug: str, limit=1) -> Recipe | None:
|
||||||
dbrecipe = (
|
stmt = select(RecipeModel).filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
|
||||||
self.session.query(RecipeModel)
|
dbrecipe = self.session.execute(stmt).scalars().one_or_none()
|
||||||
.filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
|
|
||||||
.one_or_none()
|
|
||||||
)
|
|
||||||
if dbrecipe is None:
|
if dbrecipe is None:
|
||||||
return None
|
return None
|
||||||
return self.schema.from_orm(dbrecipe)
|
return self.schema.from_orm(dbrecipe)
|
||||||
|
|
||||||
def all_ids(self, group_id: UUID4) -> list[UUID4]:
|
def all_ids(self, group_id: UUID4) -> Sequence[UUID4]:
|
||||||
return [tpl[0] for tpl in self.session.query(RecipeModel.id).filter(RecipeModel.group_id == group_id).all()]
|
stmt = select(RecipeModel.id).filter(RecipeModel.group_id == group_id)
|
||||||
|
return self.session.execute(stmt).scalars().all()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
||||||
@ -7,15 +8,13 @@ from .repository_generic import RepositoryGeneric
|
|||||||
|
|
||||||
|
|
||||||
class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
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:
|
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
|
||||||
|
from_model = self._get_unit(from_unit)
|
||||||
from_model: IngredientUnitModel = (
|
to_model = self._get_unit(to_unit)
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
to_model.ingredients += from_model.ingredients
|
to_model.ingredients += from_model.ingredients
|
||||||
|
|
||||||
@ -29,4 +28,4 @@ class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
|||||||
return self.get_one(to_unit)
|
return self.get_one(to_unit)
|
||||||
|
|
||||||
def by_group(self, group_id: UUID4) -> "RepositoryUnit":
|
def by_group(self, group_id: UUID4) -> "RepositoryUnit":
|
||||||
return super().by_group(group_id) # type: ignore
|
return super().by_group(group_id)
|
||||||
|
@ -2,6 +2,7 @@ import random
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.assets import users as users_assets
|
from mealie.assets import users as users_assets
|
||||||
from mealie.schema.user.user import PrivateUser, User
|
from mealie.schema.user.user import PrivateUser, User
|
||||||
@ -35,12 +36,14 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
|||||||
entry = super().delete(value, match_key)
|
entry = super().delete(value, match_key)
|
||||||
# Delete the user's directory
|
# Delete the user's directory
|
||||||
shutil.rmtree(PrivateUser.get_directory(value))
|
shutil.rmtree(PrivateUser.get_directory(value))
|
||||||
return entry # type: ignore
|
return entry
|
||||||
|
|
||||||
def get_by_username(self, username: str) -> PrivateUser | None:
|
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)
|
return None if dbuser is None else self.schema.from_orm(dbuser)
|
||||||
|
|
||||||
def get_locked_users(self) -> list[PrivateUser]:
|
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]
|
return [self.schema.from_orm(x) for x in results]
|
||||||
|
@ -37,7 +37,6 @@ class HttpRepo(Generic[C, R, U]):
|
|||||||
exception_msgs: Callable[[type[Exception]], str] | None = None,
|
exception_msgs: Callable[[type[Exception]], str] | None = None,
|
||||||
default_message: str | None = None,
|
default_message: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.repo = repo
|
self.repo = repo
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.exception_msgs = exception_msgs
|
self.exception_msgs = exception_msgs
|
||||||
|
@ -39,7 +39,6 @@ class AdminAboutController(BaseAdminController):
|
|||||||
|
|
||||||
@router.get("/statistics", response_model=AppStatistics)
|
@router.get("/statistics", response_model=AppStatistics)
|
||||||
def get_app_statistics(self):
|
def get_app_statistics(self):
|
||||||
|
|
||||||
return AppStatistics(
|
return AppStatistics(
|
||||||
total_recipes=self.repos.recipes.count_all(),
|
total_recipes=self.repos.recipes.count_all(),
|
||||||
uncategorized_recipes=self.repos.recipes.count_uncategorized(), # type: ignore
|
uncategorized_recipes=self.repos.recipes.count_uncategorized(), # type: ignore
|
||||||
|
@ -85,7 +85,6 @@ class AdminMaintenanceController(BaseAdminController):
|
|||||||
|
|
||||||
@router.get("/logs", response_model=MaintenanceLogs)
|
@router.get("/logs", response_model=MaintenanceLogs)
|
||||||
def get_logs(self, lines: int = 200):
|
def get_logs(self, lines: int = 200):
|
||||||
|
|
||||||
return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines))
|
return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines))
|
||||||
|
|
||||||
@router.get("/storage", response_model=MaintenanceStorageDetails)
|
@router.get("/storage", response_model=MaintenanceStorageDetails)
|
||||||
|
@ -49,7 +49,6 @@ class MealieAuthToken(BaseModel):
|
|||||||
|
|
||||||
@public_router.post("/token")
|
@public_router.post("/token")
|
||||||
def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(generate_session)):
|
def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(generate_session)):
|
||||||
|
|
||||||
email = data.username
|
email = data.username
|
||||||
password = data.password
|
password = data.password
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@ from typing import Any, TypeVar, cast
|
|||||||
from dateutil import parser as date_parser
|
from dateutil import parser as date_parser
|
||||||
from dateutil.parser import ParserError
|
from dateutil.parser import ParserError
|
||||||
from humps import decamelize
|
from humps import decamelize
|
||||||
from sqlalchemy import bindparam, text
|
from sqlalchemy import Select, bindparam, text
|
||||||
from sqlalchemy.orm.query import Query
|
|
||||||
from sqlalchemy.sql import sqltypes
|
from sqlalchemy.sql import sqltypes
|
||||||
from sqlalchemy.sql.expression import BindParameter
|
from sqlalchemy.sql.expression import BindParameter
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ class QueryFilter:
|
|||||||
|
|
||||||
return f"<<{joined}>>"
|
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] = []
|
segments: list[str] = []
|
||||||
params: list[BindParameter] = []
|
params: list[BindParameter] = []
|
||||||
for i, component in enumerate(self.filter_components):
|
for i, component in enumerate(self.filter_components):
|
||||||
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from pydantic import BaseModel
|
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.engine import base
|
||||||
from sqlalchemy.orm import sessionmaker
|
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
|
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.
|
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 = {
|
results = {
|
||||||
**{table.name: [] for table in all_tables},
|
**{table.name: [] for table in all_tables},
|
||||||
"alembic_version": [dict(row) for row in self.engine.execute("SELECT * FROM alembic_version").fetchall()],
|
"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]]:
|
def dump(self) -> dict[str, list[dict]]:
|
||||||
"""
|
"""
|
||||||
Returns the entire SQLAlchemy database as a python dictionary. This dictionary is wrapped by
|
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.
|
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
|
with self.engine.connect() as connection:
|
||||||
result = {
|
self.meta.reflect(bind=self.engine) # http://docs.sqlalchemy.org/en/rel_0_9/core/reflection.html
|
||||||
table.name: [dict(row) for row in self.engine.execute(table.select())] for table in self.meta.sorted_tables
|
|
||||||
}
|
result = {
|
||||||
|
table.name: [dict(row) for row in connection.execute(table.select()).mappings()]
|
||||||
|
for table in self.meta.sorted_tables
|
||||||
|
}
|
||||||
|
|
||||||
return jsonable_encoder(result)
|
return jsonable_encoder(result)
|
||||||
|
|
||||||
def restore(self, db_dump: dict) -> None:
|
def restore(self, db_dump: dict) -> None:
|
||||||
"""Restores all data from dictionary into the database"""
|
"""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)
|
self.meta.reflect(bind=self.engine)
|
||||||
for table_name, rows in data.items():
|
for table_name, rows in data.items():
|
||||||
if not rows:
|
if not rows:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
table = self.meta.tables[table_name]
|
table = self.meta.tables[table_name]
|
||||||
self.engine.execute(table.delete())
|
connection.execute(table.delete())
|
||||||
self.engine.execute(table.insert(), rows)
|
connection.execute(insert(table), rows)
|
||||||
|
|
||||||
def drop_all(self) -> None:
|
def drop_all(self) -> None:
|
||||||
"""Drops all data from the database"""
|
"""Drops all data from the database"""
|
||||||
@ -129,11 +136,11 @@ class AlchemyExporter(BaseService):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if is_postgres:
|
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:
|
for table in self.meta.sorted_tables:
|
||||||
session.execute(f"DELETE FROM {table.name}")
|
session.execute(text(f"DELETE FROM {table.name}"))
|
||||||
finally:
|
finally:
|
||||||
if is_postgres:
|
if is_postgres:
|
||||||
session.execute("SET session_replication_role = 'origin'")
|
session.execute(text("SET session_replication_role = 'origin'"))
|
||||||
session.commit()
|
session.commit()
|
||||||
|
@ -69,7 +69,6 @@ class DefaultEmailSender(ABCEmailSender, BaseService):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def send(self, email_to: str, subject: str, html: str) -> bool:
|
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:
|
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.")
|
raise ValueError("SMTP_FROM_EMAIL and SMTP_FROM_NAME must be set in the config file.")
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
|||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.db.db_setup import session_context
|
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]:
|
def get_scheduled_webhooks(self, start_dt: datetime, end_dt: datetime) -> list[ReadWebhook]:
|
||||||
"""Fetches all scheduled webhooks from the database"""
|
"""Fetches all scheduled webhooks from the database"""
|
||||||
with self.ensure_session() as session:
|
with self.ensure_session() as session:
|
||||||
return (
|
stmt = select(GroupWebhooksModel).where(
|
||||||
session.query(GroupWebhooksModel)
|
GroupWebhooksModel.enabled == True, # noqa: E712 - required for SQLAlchemy comparison
|
||||||
.where(
|
GroupWebhooksModel.scheduled_time > start_dt.astimezone(timezone.utc).time(),
|
||||||
GroupWebhooksModel.enabled == True, # noqa: E712 - required for SQLAlchemy comparison
|
GroupWebhooksModel.scheduled_time <= end_dt.astimezone(timezone.utc).time(),
|
||||||
GroupWebhooksModel.scheduled_time > start_dt.astimezone(timezone.utc).time(),
|
|
||||||
GroupWebhooksModel.scheduled_time <= end_dt.astimezone(timezone.utc).time(),
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
return session.execute(stmt).scalars().all()
|
||||||
|
@ -67,7 +67,6 @@ class BaseMigrator(BaseService):
|
|||||||
self.report_id = self.report.id
|
self.report_id = self.report.id
|
||||||
|
|
||||||
def _save_all_entries(self) -> None:
|
def _save_all_entries(self) -> None:
|
||||||
|
|
||||||
is_success = True
|
is_success = True
|
||||||
is_failure = True
|
is_failure = True
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ replace_abbreviations = {
|
|||||||
|
|
||||||
|
|
||||||
def replace_common_abbreviations(string: str) -> str:
|
def replace_common_abbreviations(string: str) -> str:
|
||||||
|
|
||||||
for k, v in replace_abbreviations.items():
|
for k, v in replace_abbreviations.items():
|
||||||
regex = rf"(?<=\d)\s?({k}\bs?)"
|
regex = rf"(?<=\d)\s?({k}\bs?)"
|
||||||
string = re.sub(regex, v, string)
|
string = re.sub(regex, v, string)
|
||||||
|
@ -180,7 +180,6 @@ def import_data(lines):
|
|||||||
# otherwise it's a token
|
# otherwise it's a token
|
||||||
# e.g.: potato \t I2 \t L5 \t NoCAP \t B-NAME/0.978253
|
# e.g.: potato \t I2 \t L5 \t NoCAP \t B-NAME/0.978253
|
||||||
else:
|
else:
|
||||||
|
|
||||||
columns = re.split("\t", line.strip())
|
columns = re.split("\t", line.strip())
|
||||||
token = columns[0].strip()
|
token = columns[0].strip()
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ async def largest_content_len(urls: list[str]) -> tuple[str, int]:
|
|||||||
tasks = [do(client, url) for url in urls]
|
tasks = [do(client, url) for url in urls]
|
||||||
responses: list[Response] = await gather_with_concurrency(10, *tasks)
|
responses: list[Response] = await gather_with_concurrency(10, *tasks)
|
||||||
for response in responses:
|
for response in responses:
|
||||||
|
|
||||||
len_int = int(response.headers.get("Content-Length", 0))
|
len_int = int(response.headers.get("Content-Length", 0))
|
||||||
if len_int > largest_len:
|
if len_int > largest_len:
|
||||||
largest_url = str(response.url)
|
largest_url = str(response.url)
|
||||||
|
@ -108,7 +108,6 @@ class RecipeService(BaseService):
|
|||||||
return Recipe(**additional_attrs)
|
return Recipe(**additional_attrs)
|
||||||
|
|
||||||
def create_one(self, create_data: Recipe | CreateRecipe) -> Recipe:
|
def create_one(self, create_data: Recipe | CreateRecipe) -> Recipe:
|
||||||
|
|
||||||
if create_data.name is None:
|
if create_data.name is None:
|
||||||
create_data.name = "New Recipe"
|
create_data.name = "New Recipe"
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import get_app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
from mealie.db.db_setup import session_context
|
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)
|
limit = datetime.datetime.now() - datetime.timedelta(minutes=max_minutes_old)
|
||||||
|
|
||||||
with session_context() as session:
|
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
|
total_removed = 0
|
||||||
for result in results:
|
for result in results:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import delete
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.db.db_setup import session_context
|
from mealie.db.db_setup import session_context
|
||||||
from mealie.db.models.users.password_reset import PasswordResetModel
|
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)
|
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||||
|
|
||||||
with session_context() as session:
|
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.commit()
|
||||||
session.close()
|
session.close()
|
||||||
logger.info("password reset tokens purged")
|
logger.info("password reset tokens purged")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import delete
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.db.db_setup import session_context
|
from mealie.db.db_setup import session_context
|
||||||
from mealie.db.models.group import GroupInviteToken
|
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)
|
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||||
|
|
||||||
with session_context() as session:
|
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.commit()
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
@ -83,7 +83,6 @@ class RecipeBulkScraperService(BaseService):
|
|||||||
tasks = [_do(b.url) for b in urls.imports]
|
tasks = [_do(b.url) for b in urls.imports]
|
||||||
results = await gather(*tasks)
|
results = await gather(*tasks)
|
||||||
for b, recipe in zip(urls.imports, results, strict=True):
|
for b, recipe in zip(urls.imports, results, strict=True):
|
||||||
|
|
||||||
if not recipe:
|
if not recipe:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
1825
poetry.lock
generated
1825
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@ start = "mealie.app:main"
|
|||||||
Jinja2 = "^3.1.2"
|
Jinja2 = "^3.1.2"
|
||||||
Pillow = "^9.2.0"
|
Pillow = "^9.2.0"
|
||||||
PyYAML = "^5.3.1"
|
PyYAML = "^5.3.1"
|
||||||
SQLAlchemy = "^1.4.29"
|
SQLAlchemy = "^2"
|
||||||
aiofiles = "^22.1.0"
|
aiofiles = "^22.1.0"
|
||||||
alembic = "^1.7.5"
|
alembic = "^1.7.5"
|
||||||
aniso8601 = "9.0.1"
|
aniso8601 = "9.0.1"
|
||||||
@ -43,12 +43,11 @@ tzdata = "^2022.7"
|
|||||||
uvicorn = {extras = ["standard"], version = "^0.20.0"}
|
uvicorn = {extras = ["standard"], version = "^0.20.0"}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^21.12b0"
|
black = "^23.1.0"
|
||||||
coverage = "^7.0"
|
coverage = "^7.0"
|
||||||
coveragepy-lcov = "^0.1.1"
|
coveragepy-lcov = "^0.1.1"
|
||||||
mkdocs-material = "^9.0.0"
|
mkdocs-material = "^9.0.0"
|
||||||
mypy = "^0.991"
|
mypy = "^0.991"
|
||||||
openapi-spec-validator = "^0.5.0"
|
|
||||||
pre-commit = "^3.0.4"
|
pre-commit = "^3.0.4"
|
||||||
pydantic-to-typescript = "^1.0.7"
|
pydantic-to-typescript = "^1.0.7"
|
||||||
pylint = "^2.6.0"
|
pylint = "^2.6.0"
|
||||||
|
@ -30,7 +30,6 @@ def override_get_db():
|
|||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def api_client():
|
def api_client():
|
||||||
|
|
||||||
app.dependency_overrides[generate_session] = override_get_db
|
app.dependency_overrides[generate_session] = override_get_db
|
||||||
|
|
||||||
yield TestClient(app)
|
yield TestClient(app)
|
||||||
|
2
tests/fixtures/fixture_shopping_lists.py
vendored
2
tests/fixtures/fixture_shopping_lists.py
vendored
@ -25,7 +25,6 @@ def create_item(list_id: UUID4) -> dict:
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def shopping_lists(database: AllRepositories, unique_user: TestUser):
|
def shopping_lists(database: AllRepositories, unique_user: TestUser):
|
||||||
|
|
||||||
models: list[ShoppingListOut] = []
|
models: list[ShoppingListOut] = []
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
@ -46,7 +45,6 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def shopping_list(database: AllRepositories, unique_user: TestUser):
|
def shopping_list(database: AllRepositories, unique_user: TestUser):
|
||||||
|
|
||||||
model = database.group_shopping_lists.create(
|
model = database.group_shopping_lists.create(
|
||||||
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id),
|
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id),
|
||||||
)
|
)
|
||||||
|
@ -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:
|
def test_group_invitation_delete_after_uses(api_client: TestClient, invite: str) -> None:
|
||||||
|
|
||||||
# Register First User
|
# Register First User
|
||||||
_, r = register_user(api_client, invite)
|
_, r = register_user(api_client, invite)
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
|
@ -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):
|
def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
new_plan = CreatePlanEntry(
|
new_plan = CreatePlanEntry(
|
||||||
date=date.today(),
|
date=date.today(),
|
||||||
|
@ -94,7 +94,6 @@ def test_shopping_list_items_get_one(
|
|||||||
unique_user: TestUser,
|
unique_user: TestUser,
|
||||||
list_with_items: ShoppingListOut,
|
list_with_items: ShoppingListOut,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
item = random.choice(list_with_items.list_items)
|
item = random.choice(list_with_items.list_items)
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ from tests.utils.fixture_schemas import TestUser
|
|||||||
def ten_slugs(
|
def ten_slugs(
|
||||||
api_client: TestClient, unique_user: TestUser, database: AllRepositories
|
api_client: TestClient, unique_user: TestUser, database: AllRepositories
|
||||||
) -> Generator[list[str], None, None]:
|
) -> Generator[list[str], None, None]:
|
||||||
|
|
||||||
slugs: list[str] = []
|
slugs: list[str] = []
|
||||||
|
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
@ -98,7 +97,6 @@ def test_bulk_delete_recipes(
|
|||||||
database: AllRepositories,
|
database: AllRepositories,
|
||||||
ten_slugs: list[str],
|
ten_slugs: list[str],
|
||||||
):
|
):
|
||||||
|
|
||||||
payload = {"recipes": ten_slugs}
|
payload = {"recipes": ten_slugs}
|
||||||
|
|
||||||
response = api_client.post(api_routes.recipes_bulk_actions_delete, json=payload, headers=unique_user.token)
|
response = api_client.post(api_routes.recipes_bulk_actions_delete, json=payload, headers=unique_user.token)
|
||||||
|
@ -13,7 +13,6 @@ from tests.utils.fixture_schemas import TestUser
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def slug(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> Generator[str, None, None]:
|
def slug(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> Generator[str, None, None]:
|
||||||
|
|
||||||
payload = {"name": random_string(length=20)}
|
payload = {"name": random_string(length=20)}
|
||||||
response = api_client.post(api_routes.recipes, json=payload, headers=unique_user.token)
|
response = api_client.post(api_routes.recipes, json=payload, headers=unique_user.token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
@ -25,13 +25,10 @@ def test_superuser_login(api_client: TestClient, admin_token):
|
|||||||
response = api_client.post(api_routes.auth_token, data=form_data)
|
response = api_client.post(api_routes.auth_token, data=form_data)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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)
|
response = api_client.get(api_routes.users_self, headers=admin_token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
return {"Authorization": f"Bearer {new_token}"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_token_refresh(api_client: TestClient, admin_user: TestUser):
|
def test_user_token_refresh(api_client: TestClient, admin_user: TestUser):
|
||||||
response = api_client.post(api_routes.auth_refresh, headers=admin_user.token)
|
response = api_client.post(api_routes.auth_refresh, headers=admin_user.token)
|
||||||
|
@ -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))
|
by_category = repo.get_by_categories(cast(list[RecipeCategory], created_categories))
|
||||||
|
|
||||||
assert len(by_category) == 10
|
assert len(by_category) == 10
|
||||||
|
|
||||||
for recipe_summary in by_category:
|
for recipe_summary in by_category:
|
||||||
for recipe_category in recipe_summary.recipe_category:
|
for recipe_category in recipe_summary.recipe_category:
|
||||||
assert recipe_category.id in known_category_ids
|
assert recipe_category.id in known_category_ids
|
||||||
|
@ -25,7 +25,6 @@ def test_camelize_variables():
|
|||||||
|
|
||||||
|
|
||||||
def test_cast_to():
|
def test_cast_to():
|
||||||
|
|
||||||
model = TestModel(long_name="Hello", long_int=1, long_float=1.1)
|
model = TestModel(long_name="Hello", long_int=1, long_float=1.1)
|
||||||
|
|
||||||
model2 = model.cast(TestModel2, another_str="World")
|
model2 = model.cast(TestModel2, another_str="World")
|
||||||
@ -37,7 +36,6 @@ def test_cast_to():
|
|||||||
|
|
||||||
|
|
||||||
def test_map_to():
|
def test_map_to():
|
||||||
|
|
||||||
model = TestModel(long_name="Model1", long_int=100, long_float=1.5)
|
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")
|
model2 = TestModel2(long_name="Model2", long_int=1, long_float=1.1, another_str="World")
|
||||||
|
@ -60,7 +60,6 @@ def test_get_scheduled_webhooks_filter_query(database: AllRepositories, unique_u
|
|||||||
assert result.enabled
|
assert result.enabled
|
||||||
|
|
||||||
for expected_item in expected:
|
for expected_item in expected:
|
||||||
|
|
||||||
if result.name == expected_item.name: # Names are uniquely generated so we can use this to compare
|
if result.name == expected_item.name: # Names are uniquely generated so we can use this to compare
|
||||||
assert result.enabled == expected_item.enabled
|
assert result.enabled == expected_item.enabled
|
||||||
break
|
break
|
||||||
|
@ -256,7 +256,6 @@ ingredients_test_cases = (
|
|||||||
|
|
||||||
@pytest.mark.parametrize("ingredients", ingredients_test_cases, ids=(x.test_id for x in 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):
|
def test_cleaner_clean_ingredients(ingredients: CleanerCase):
|
||||||
|
|
||||||
if ingredients.exception:
|
if ingredients.exception:
|
||||||
with pytest.raises(ingredients.exception):
|
with pytest.raises(ingredients.exception):
|
||||||
cleaner.clean_ingredients(ingredients.input)
|
cleaner.clean_ingredients(ingredients.input)
|
||||||
|
@ -11,7 +11,6 @@ SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Test Email"}
|
|||||||
|
|
||||||
class TestEmailSender(ABCEmailSender):
|
class TestEmailSender(ABCEmailSender):
|
||||||
def send(self, email_to: str, subject: str, html: str) -> bool:
|
def send(self, email_to: str, subject: str, html: str) -> bool:
|
||||||
|
|
||||||
# check email_to:
|
# check email_to:
|
||||||
assert email_to == FAKE_ADDRESS
|
assert email_to == FAKE_ADDRESS
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ class TestIngredient:
|
|||||||
|
|
||||||
|
|
||||||
def crf_exists() -> bool:
|
def crf_exists() -> bool:
|
||||||
|
|
||||||
return shutil.which("crf_test") is not None
|
return shutil.which("crf_test") is not None
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -9,9 +9,9 @@ from mealie.pkgs.stats.fs_stats import pretty_size
|
|||||||
(0, "0 bytes"),
|
(0, "0 bytes"),
|
||||||
(1, "1 bytes"),
|
(1, "1 bytes"),
|
||||||
(1024, "1.0 KB"),
|
(1024, "1.0 KB"),
|
||||||
(1024 ** 2, "1.0 MB"),
|
(1024**2, "1.0 MB"),
|
||||||
(1024 ** 2 * 1024, "1.0 GB"),
|
(1024**2 * 1024, "1.0 GB"),
|
||||||
(1024 ** 2 * 1024 * 1024, "1.0 TB"),
|
(1024**2 * 1024 * 1024, "1.0 TB"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_pretty_size(size: int, expected: str) -> None:
|
def test_pretty_size(size: int, expected: str) -> None:
|
||||||
|
@ -2,5 +2,4 @@ from fastapi.encoders import jsonable_encoder
|
|||||||
|
|
||||||
|
|
||||||
def jsonify(data):
|
def jsonify(data):
|
||||||
|
|
||||||
return jsonable_encoder(data)
|
return jsonable_encoder(data)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user