mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 18:23:18 -04:00 
			
		
		
		
	Remove all sqlalchemy lazy-loading from app (#2260)
* Remove some implicit lazy-loads from user serialization * implement full backup restore across different database versions * rework all custom getter dicts to not leak lazy loads * remove some occurances of lazy-loading * remove a lot of lazy loading from recipes * add more eager loading remove loading options from repository remove raiseload for checking * fix failing test * do not apply loader options for paging counts * try using selectinload a bit more instead of joinedload * linter fixes
This commit is contained in:
		| @@ -28,7 +28,9 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins): | |||||||
|     abbreviation: Mapped[str | None] = mapped_column(String) |     abbreviation: Mapped[str | None] = mapped_column(String) | ||||||
|     use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False) |     use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False) | ||||||
|     fraction: Mapped[bool | None] = mapped_column(Boolean, default=True) |     fraction: Mapped[bool | None] = mapped_column(Boolean, default=True) | ||||||
|     ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="unit") |     ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship( | ||||||
|  |         "RecipeIngredientModel", back_populates="unit" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     @auto_init() |     @auto_init() | ||||||
|     def __init__(self, **_) -> None: |     def __init__(self, **_) -> None: | ||||||
| @@ -45,7 +47,9 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins): | |||||||
|  |  | ||||||
|     name: Mapped[str | None] = mapped_column(String) |     name: Mapped[str | None] = mapped_column(String) | ||||||
|     description: Mapped[str | None] = mapped_column(String) |     description: Mapped[str | None] = mapped_column(String) | ||||||
|     ingredients: Mapped[list["RecipeIngredient"]] = orm.relationship("RecipeIngredient", back_populates="food") |     ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship( | ||||||
|  |         "RecipeIngredientModel", back_populates="food" | ||||||
|  |     ) | ||||||
|     extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan") |     extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan") | ||||||
|  |  | ||||||
|     label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True) |     label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True) | ||||||
| @@ -57,7 +61,7 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeIngredient(SqlAlchemyBase, BaseMixins): | class RecipeIngredientModel(SqlAlchemyBase, BaseMixins): | ||||||
|     __tablename__ = "recipes_ingredients" |     __tablename__ = "recipes_ingredients" | ||||||
|     id: Mapped[int] = mapped_column(Integer, primary_key=True) |     id: Mapped[int] = mapped_column(Integer, primary_key=True) | ||||||
|     position: Mapped[int | None] = mapped_column(Integer, index=True) |     position: Mapped[int | None] = mapped_column(Integer, index=True) | ||||||
| @@ -92,16 +96,16 @@ class RecipeIngredient(SqlAlchemyBase, BaseMixins): | |||||||
|             self.orginal_text = unidecode(orginal_text).lower().strip() |             self.orginal_text = unidecode(orginal_text).lower().strip() | ||||||
|  |  | ||||||
|  |  | ||||||
| @event.listens_for(RecipeIngredient.note, "set") | @event.listens_for(RecipeIngredientModel.note, "set") | ||||||
| def receive_note(target: RecipeIngredient, value: str, oldvalue, initiator): | def receive_note(target: RecipeIngredientModel, value: str, oldvalue, initiator): | ||||||
|     if value is not None: |     if value is not None: | ||||||
|         target.name_normalized = unidecode(value).lower().strip() |         target.name_normalized = unidecode(value).lower().strip() | ||||||
|     else: |     else: | ||||||
|         target.name_normalized = None |         target.name_normalized = None | ||||||
|  |  | ||||||
|  |  | ||||||
| @event.listens_for(RecipeIngredient.original_text, "set") | @event.listens_for(RecipeIngredientModel.original_text, "set") | ||||||
| def receive_original_text(target: RecipeIngredient, value: str, oldvalue, initiator): | def receive_original_text(target: RecipeIngredientModel, value: str, oldvalue, initiator): | ||||||
|     if value is not None: |     if value is not None: | ||||||
|         target.original_text_normalized = unidecode(value).lower().strip() |         target.original_text_normalized = unidecode(value).lower().strip() | ||||||
|     else: |     else: | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ from .api_extras import ApiExtras, api_extras | |||||||
| from .assets import RecipeAsset | from .assets import RecipeAsset | ||||||
| from .category import recipes_to_categories | from .category import recipes_to_categories | ||||||
| from .comment import RecipeComment | from .comment import RecipeComment | ||||||
| from .ingredient import RecipeIngredient | from .ingredient import RecipeIngredientModel | ||||||
| from .instruction import RecipeInstruction | from .instruction import RecipeInstruction | ||||||
| from .note import Note | from .note import Note | ||||||
| from .nutrition import Nutrition | from .nutrition import Nutrition | ||||||
| @@ -77,10 +77,10 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|     ) |     ) | ||||||
|     tools: Mapped[list["Tool"]] = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes") |     tools: Mapped[list["Tool"]] = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes") | ||||||
|  |  | ||||||
|     recipe_ingredient: Mapped[list[RecipeIngredient]] = orm.relationship( |     recipe_ingredient: Mapped[list[RecipeIngredientModel]] = orm.relationship( | ||||||
|         "RecipeIngredient", |         "RecipeIngredientModel", | ||||||
|         cascade="all, delete-orphan", |         cascade="all, delete-orphan", | ||||||
|         order_by="RecipeIngredient.position", |         order_by="RecipeIngredientModel.position", | ||||||
|         collection_class=ordering_list("position"), |         collection_class=ordering_list("position"), | ||||||
|     ) |     ) | ||||||
|     recipe_instructions: Mapped[list[RecipeInstruction]] = orm.relationship( |     recipe_instructions: Mapped[list[RecipeInstruction]] = orm.relationship( | ||||||
| @@ -173,7 +173,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|             self.recipe_instructions = [RecipeInstruction(**step, session=session) for step in recipe_instructions] |             self.recipe_instructions = [RecipeInstruction(**step, session=session) for step in recipe_instructions] | ||||||
|  |  | ||||||
|         if recipe_ingredient is not None: |         if recipe_ingredient is not None: | ||||||
|             self.recipe_ingredient = [RecipeIngredient(**ingr, session=session) for ingr in recipe_ingredient] |             self.recipe_ingredient = [RecipeIngredientModel(**ingr, session=session) for ingr in recipe_ingredient] | ||||||
|  |  | ||||||
|         if assets: |         if assets: | ||||||
|             self.assets = [RecipeAsset(**a) for a in assets] |             self.assets = [RecipeAsset(**a) for a in assets] | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
| from sqlalchemy import select | from sqlalchemy import select | ||||||
| from sqlalchemy.orm import joinedload |  | ||||||
| from sqlalchemy.orm.interfaces import LoaderOption |  | ||||||
|  |  | ||||||
| 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 | ||||||
| @@ -31,9 +29,3 @@ class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]): | |||||||
|  |  | ||||||
|     def by_group(self, group_id: UUID4) -> "RepositoryFood": |     def by_group(self, group_id: UUID4) -> "RepositoryFood": | ||||||
|         return super().by_group(group_id) |         return super().by_group(group_id) | ||||||
|  |  | ||||||
|     def paging_query_options(self) -> list[LoaderOption]: |  | ||||||
|         return [ |  | ||||||
|             joinedload(IngredientFoodModel.extras), |  | ||||||
|             joinedload(IngredientFoodModel.label), |  | ||||||
|         ] |  | ||||||
|   | |||||||
| @@ -7,16 +7,16 @@ 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 Select, delete, func, select | from sqlalchemy import Select, delete, func, select | ||||||
| from sqlalchemy.orm.interfaces import LoaderOption |  | ||||||
| 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.db.models._model_base import SqlAlchemyBase | from mealie.db.models._model_base import SqlAlchemyBase | ||||||
|  | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.response.pagination import OrderDirection, PaginationBase, PaginationQuery | from mealie.schema.response.pagination import OrderDirection, 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=MealieModel) | ||||||
| Model = TypeVar("Model", bound=SqlAlchemyBase) | Model = TypeVar("Model", bound=SqlAlchemyBase) | ||||||
|  |  | ||||||
| T = TypeVar("T", bound="RepositoryGeneric") | T = TypeVar("T", bound="RepositoryGeneric") | ||||||
| @@ -54,8 +54,13 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|         self.logger.error(f"Error processing query for Repo model={self.model.__name__} schema={self.schema.__name__}") |         self.logger.error(f"Error processing query for Repo model={self.model.__name__} schema={self.schema.__name__}") | ||||||
|         self.logger.error(e) |         self.logger.error(e) | ||||||
|  |  | ||||||
|     def _query(self): |     def _query(self, override_schema: type[MealieModel] | None = None, with_options=True): | ||||||
|         return select(self.model) |         q = select(self.model) | ||||||
|  |         if with_options: | ||||||
|  |             schema = override_schema or self.schema | ||||||
|  |             return q.options(*schema.loader_options()) | ||||||
|  |         else: | ||||||
|  |             return q | ||||||
|  |  | ||||||
|     def _filter_builder(self, **kwargs) -> dict[str, Any]: |     def _filter_builder(self, **kwargs) -> dict[str, Any]: | ||||||
|         dct = {} |         dct = {} | ||||||
| @@ -83,7 +88,7 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|  |  | ||||||
|         fltr = self._filter_builder() |         fltr = self._filter_builder() | ||||||
|  |  | ||||||
|         q = self._query().filter_by(**fltr) |         q = self._query(override_schema=eff_schema).filter_by(**fltr) | ||||||
|  |  | ||||||
|         if order_by: |         if order_by: | ||||||
|             try: |             try: | ||||||
| @@ -98,7 +103,7 @@ 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() |         result = self.session.execute(q.offset(start).limit(limit)).unique().scalars().all() | ||||||
|         return [eff_schema.from_orm(x) for x in result] |         return [eff_schema.from_orm(x) for x in result] | ||||||
|  |  | ||||||
|     def multi_query( |     def multi_query( | ||||||
| @@ -113,7 +118,7 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|         eff_schema = override_schema or self.schema |         eff_schema = override_schema or self.schema | ||||||
|  |  | ||||||
|         fltr = self._filter_builder(**query_by) |         fltr = self._filter_builder(**query_by) | ||||||
|         q = self._query().filter_by(**fltr) |         q = self._query(override_schema=eff_schema).filter_by(**fltr) | ||||||
|  |  | ||||||
|         if order_by: |         if order_by: | ||||||
|             if order_attr := getattr(self.model, str(order_by)): |             if order_attr := getattr(self.model, str(order_by)): | ||||||
| @@ -121,7 +126,7 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|                 q = q.order_by(order_attr) |                 q = q.order_by(order_attr) | ||||||
|  |  | ||||||
|         q = q.offset(start).limit(limit) |         q = q.offset(start).limit(limit) | ||||||
|         result = self.session.execute(q).scalars().all() |         result = self.session.execute(q).unique().scalars().all() | ||||||
|         return [eff_schema.from_orm(x) for x in result] |         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: | ||||||
| @@ -133,14 +138,15 @@ 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.session.execute(self._query().filter_by(**fltr)).scalars().one() |         return self.session.execute(self._query().filter_by(**fltr)).unique().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 | ||||||
|  |         eff_schema = override_schema or self.schema | ||||||
|  |  | ||||||
|         q = self._query() |         q = self._query(override_schema=eff_schema) | ||||||
|  |  | ||||||
|         if any_case: |         if any_case: | ||||||
|             search_attr = getattr(self.model, key) |             search_attr = getattr(self.model, key) | ||||||
| @@ -148,12 +154,11 @@ 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 = self.session.execute(q).scalars().one_or_none() |         result = self.session.execute(q).unique().scalars().one_or_none() | ||||||
|  |  | ||||||
|         if not result: |         if not result: | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|         eff_schema = override_schema or self.schema |  | ||||||
|         return eff_schema.from_orm(result) |         return eff_schema.from_orm(result) | ||||||
|  |  | ||||||
|     def create(self, data: Schema | BaseModel | dict) -> Schema: |     def create(self, data: Schema | BaseModel | dict) -> Schema: | ||||||
| @@ -205,7 +210,7 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|             document_data_by_id[document_data["id"]] = document_data |             document_data_by_id[document_data["id"]] = document_data | ||||||
|  |  | ||||||
|         documents_to_update_query = self._query().filter(self.model.id.in_(list(document_data_by_id.keys()))) |         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() |         documents_to_update = self.session.execute(documents_to_update_query).unique().scalars().all() | ||||||
|  |  | ||||||
|         updated_documents = [] |         updated_documents = [] | ||||||
|         for document_to_update in documents_to_update: |         for document_to_update in documents_to_update: | ||||||
| @@ -229,7 +234,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.session.execute(self._query().filter_by(**{match_key: value})).scalars().one() |         result = self._query_one(value, match_key) | ||||||
|         results_as_model = self.schema.from_orm(result) |         results_as_model = self.schema.from_orm(result) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
| @@ -243,7 +248,7 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|  |  | ||||||
|     def delete_many(self, values: Iterable) -> Schema: |     def delete_many(self, values: Iterable) -> Schema: | ||||||
|         query = 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 = self.session.execute(query).unique().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: | ||||||
| @@ -282,13 +287,9 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|             q = select(func.count(self.model.id)).filter(attribute_name == attr_match) |             q = select(func.count(self.model.id)).filter(attribute_name == attr_match) | ||||||
|             return self.session.scalar(q) |             return self.session.scalar(q) | ||||||
|         else: |         else: | ||||||
|             q = self._query().filter(attribute_name == attr_match) |             q = self._query(override_schema=eff_schema).filter(attribute_name == attr_match) | ||||||
|             return [eff_schema.from_orm(x) for x in self.session.execute(q).scalars().all()] |             return [eff_schema.from_orm(x) for x in self.session.execute(q).scalars().all()] | ||||||
|  |  | ||||||
|     def paging_query_options(self) -> list[LoaderOption]: |  | ||||||
|         # Override this in subclasses to specify joinedloads or similar for page_all |  | ||||||
|         return [] |  | ||||||
|  |  | ||||||
|     def page_all(self, pagination: PaginationQuery, override=None) -> PaginationBase[Schema]: |     def page_all(self, pagination: PaginationQuery, override=None) -> PaginationBase[Schema]: | ||||||
|         """ |         """ | ||||||
|         pagination is a method to interact with the filtered database table and return a paginated result |         pagination is a method to interact with the filtered database table and return a paginated result | ||||||
| @@ -301,12 +302,14 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|         """ |         """ | ||||||
|         eff_schema = override or self.schema |         eff_schema = override or self.schema | ||||||
|  |  | ||||||
|         q = self._query().options(*self.paging_query_options()) |         q = self._query(override_schema=eff_schema, with_options=False) | ||||||
|  |  | ||||||
|         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) | ||||||
|  |  | ||||||
|  |         # Apply options late, so they do not get used for counting | ||||||
|  |         q = q.options(*eff_schema.loader_options()) | ||||||
|         try: |         try: | ||||||
|             data = self.session.execute(q).unique().scalars().all() |             data = self.session.execute(q).unique().scalars().all() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ from sqlalchemy.orm import joinedload | |||||||
| from text_unidecode import unidecode | from text_unidecode import unidecode | ||||||
|  |  | ||||||
| from mealie.db.models.recipe.category import Category | from mealie.db.models.recipe.category import Category | ||||||
| from mealie.db.models.recipe.ingredient import RecipeIngredient | from mealie.db.models.recipe.ingredient import RecipeIngredientModel | ||||||
| from mealie.db.models.recipe.recipe import RecipeModel | from mealie.db.models.recipe.recipe import RecipeModel | ||||||
| from mealie.db.models.recipe.settings import RecipeSettings | from mealie.db.models.recipe.settings import RecipeSettings | ||||||
| from mealie.db.models.recipe.tag import Tag | from mealie.db.models.recipe.tag import Tag | ||||||
| @@ -108,7 +108,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         if load_foods: |         if load_foods: | ||||||
|             args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food))) |             args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredientModel.food))) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             if order_by: |             if order_by: | ||||||
| @@ -156,10 +156,10 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|         # that at least sqlite wont use indexes for that correctly anymore and takes a big hit, so prefiltering it is |         # that at least sqlite wont use indexes for that correctly anymore and takes a big hit, so prefiltering it is | ||||||
|         ingredient_ids = ( |         ingredient_ids = ( | ||||||
|             self.session.execute( |             self.session.execute( | ||||||
|                 select(RecipeIngredient.id).filter( |                 select(RecipeIngredientModel.id).filter( | ||||||
|                     or_( |                     or_( | ||||||
|                         RecipeIngredient.note_normalized.like(f"%{normalized_search}%"), |                         RecipeIngredientModel.note_normalized.like(f"%{normalized_search}%"), | ||||||
|                         RecipeIngredient.original_text_normalized.like(f"%{normalized_search}%"), |                         RecipeIngredientModel.original_text_normalized.like(f"%{normalized_search}%"), | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
| @@ -171,7 +171,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|             or_( |             or_( | ||||||
|                 RecipeModel.name_normalized.like(f"%{normalized_search}%"), |                 RecipeModel.name_normalized.like(f"%{normalized_search}%"), | ||||||
|                 RecipeModel.description_normalized.like(f"%{normalized_search}%"), |                 RecipeModel.description_normalized.like(f"%{normalized_search}%"), | ||||||
|                 RecipeModel.recipe_ingredient.any(RecipeIngredient.id.in_(ingredient_ids)), |                 RecipeModel.recipe_ingredient.any(RecipeIngredientModel.id.in_(ingredient_ids)), | ||||||
|             ) |             ) | ||||||
|         ).order_by(desc(RecipeModel.name_normalized.like(f"%{normalized_search}%"))) |         ).order_by(desc(RecipeModel.name_normalized.like(f"%{normalized_search}%"))) | ||||||
|         return q |         return q | ||||||
| @@ -303,9 +303,9 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|                 fltr.append(RecipeModel.tools.any(Tool.id.in_(tools))) |                 fltr.append(RecipeModel.tools.any(Tool.id.in_(tools))) | ||||||
|         if foods: |         if foods: | ||||||
|             if require_all_foods: |             if require_all_foods: | ||||||
|                 fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredient.food_id == food) for food in foods) |                 fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id == food) for food in foods) | ||||||
|             else: |             else: | ||||||
|                 fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredient.food_id.in_(foods))) |                 fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id.in_(foods))) | ||||||
|         return fltr |         return fltr | ||||||
|  |  | ||||||
|     def by_category_and_tags( |     def by_category_and_tags( | ||||||
|   | |||||||
| @@ -5,8 +5,9 @@ from pydantic import UUID4 | |||||||
| from sqlalchemy import select | 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 | ||||||
|  |  | ||||||
|  | from ..db.models.users import User | ||||||
| from .repository_generic import RepositoryGeneric | from .repository_generic import RepositoryGeneric | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ from typing import Protocol, TypeVar | |||||||
|  |  | ||||||
| from humps.main import camelize | from humps.main import camelize | ||||||
| from pydantic import UUID4, BaseModel | from pydantic import UUID4, BaseModel | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| T = TypeVar("T", bound=BaseModel) | T = TypeVar("T", bound=BaseModel) | ||||||
|  |  | ||||||
| @@ -54,6 +55,10 @@ class MealieModel(BaseModel): | |||||||
|             if field in self.__fields__ and (val is not None or replace_null): |             if field in self.__fields__ and (val is not None or replace_null): | ||||||
|                 setattr(self, field, val) |                 setattr(self, field, val) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |  | ||||||
| class HasUUID(Protocol): | class HasUUID(Protocol): | ||||||
|     id: UUID4 |     id: UUID4 | ||||||
|   | |||||||
| @@ -1,10 +1,13 @@ | |||||||
| from pydantic import UUID4, validator | from pydantic import UUID4, validator | ||||||
| from slugify import slugify | from slugify import slugify | ||||||
|  | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool | from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
|  | from ...db.models.group import CookBook | ||||||
| from ..recipe.recipe_category import CategoryBase, TagBase | from ..recipe.recipe_category import CategoryBase, TagBase | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -51,6 +54,10 @@ class ReadCookBook(UpdateCookBook): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(CookBook.categories), joinedload(CookBook.tags), joinedload(CookBook.tools)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class CookBookPagination(PaginationBase): | class CookBookPagination(PaginationBase): | ||||||
|     items: list[ReadCookBook] |     items: list[ReadCookBook] | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								mealie/schema/getter_dict.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mealie/schema/getter_dict.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | from collections.abc import Callable, Mapping | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from pydantic.utils import GetterDict | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomGetterDict(GetterDict): | ||||||
|  |     transformations: Mapping[str, Callable[[Any], Any]] | ||||||
|  |  | ||||||
|  |     def get(self, key: Any, default: Any = None) -> Any: | ||||||
|  |         # Transform extras into key-value dict | ||||||
|  |         if key in self.transformations: | ||||||
|  |             value = super().get(key, default) | ||||||
|  |             return self.transformations[key](value) | ||||||
|  |  | ||||||
|  |         # Keep all other fields as they are | ||||||
|  |         else: | ||||||
|  |             return super().get(key, default) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ExtrasGetterDict(CustomGetterDict): | ||||||
|  |     transformations = {"extras": lambda value: {x.key_name: x.value for x in value}} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GroupGetterDict(CustomGetterDict): | ||||||
|  |     transformations = {"group": lambda value: value.name} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserGetterDict(CustomGetterDict): | ||||||
|  |     transformations = { | ||||||
|  |         "group": lambda value: value.name, | ||||||
|  |         "favorite_recipes": lambda value: [x.slug for x in value], | ||||||
|  |     } | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| from pydantic import UUID4, NoneStr | from pydantic import UUID4, NoneStr | ||||||
|  | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.group import GroupEventNotifierModel | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
| @@ -86,6 +89,10 @@ class GroupEventNotifierOut(MealieModel): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(GroupEventNotifierModel.options)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupEventPagination(PaginationBase): | class GroupEventPagination(PaginationBase): | ||||||
|     items: list[GroupEventNotifierOut] |     items: list[GroupEventNotifierOut] | ||||||
|   | |||||||
| @@ -4,11 +4,19 @@ from datetime import datetime | |||||||
| from fractions import Fraction | from fractions import Fraction | ||||||
|  |  | ||||||
| from pydantic import UUID4, validator | from pydantic import UUID4, validator | ||||||
| from pydantic.utils import GetterDict | from sqlalchemy.orm import joinedload, selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListItem | from mealie.db.models.group import ( | ||||||
|  |     ShoppingList, | ||||||
|  |     ShoppingListItem, | ||||||
|  |     ShoppingListMultiPurposeLabel, | ||||||
|  |     ShoppingListRecipeReference, | ||||||
|  | ) | ||||||
|  | from mealie.db.models.recipe import IngredientFoodModel, RecipeModel | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema._mealie.types import NoneFloat | from mealie.schema._mealie.types import NoneFloat | ||||||
|  | from mealie.schema.getter_dict import ExtrasGetterDict | ||||||
| from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary | from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary | ||||||
| from mealie.schema.recipe.recipe import RecipeSummary | from mealie.schema.recipe.recipe import RecipeSummary | ||||||
| from mealie.schema.recipe.recipe_ingredient import ( | from mealie.schema.recipe.recipe_ingredient import ( | ||||||
| @@ -171,13 +179,18 @@ class ShoppingListItemOut(ShoppingListItemBase): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = ExtrasGetterDict | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|         def getter_dict(cls, name_orm: ShoppingListItem): |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|             return { |         return [ | ||||||
|                 **GetterDict(name_orm), |             selectinload(ShoppingListItem.extras), | ||||||
|                 "extras": {x.key_name: x.value for x in name_orm.extras}, |             selectinload(ShoppingListItem.food).joinedload(IngredientFoodModel.extras), | ||||||
|             } |             selectinload(ShoppingListItem.food).joinedload(IngredientFoodModel.label), | ||||||
|  |             joinedload(ShoppingListItem.label), | ||||||
|  |             joinedload(ShoppingListItem.unit), | ||||||
|  |             selectinload(ShoppingListItem.recipe_references), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShoppingListItemsCollectionOut(MealieModel): | class ShoppingListItemsCollectionOut(MealieModel): | ||||||
| @@ -204,6 +217,10 @@ class ShoppingListMultiPurposeLabelOut(ShoppingListMultiPurposeLabelUpdate): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(ShoppingListMultiPurposeLabel.label)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShoppingListItemPagination(PaginationBase): | class ShoppingListItemPagination(PaginationBase): | ||||||
|     items: list[ShoppingListItemOut] |     items: list[ShoppingListItemOut] | ||||||
| @@ -229,6 +246,14 @@ class ShoppingListRecipeRefOut(MealieModel): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.tools), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShoppingListSave(ShoppingListCreate): | class ShoppingListSave(ShoppingListCreate): | ||||||
|     group_id: UUID4 |     group_id: UUID4 | ||||||
| @@ -241,13 +266,23 @@ class ShoppingListSummary(ShoppingListSave): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = ExtrasGetterDict | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|         def getter_dict(cls, name_orm: ShoppingList): |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|             return { |         return [ | ||||||
|                 **GetterDict(name_orm), |             selectinload(ShoppingList.extras), | ||||||
|                 "extras": {x.key_name: x.value for x in name_orm.extras}, |             selectinload(ShoppingList.recipe_references) | ||||||
|             } |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(ShoppingList.recipe_references) | ||||||
|  |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(ShoppingList.recipe_references) | ||||||
|  |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.tools), | ||||||
|  |             selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShoppingListPagination(PaginationBase): | class ShoppingListPagination(PaginationBase): | ||||||
| @@ -265,13 +300,33 @@ class ShoppingListOut(ShoppingListUpdate): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = ExtrasGetterDict | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|         def getter_dict(cls, name_orm: ShoppingList): |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|             return { |         return [ | ||||||
|                 **GetterDict(name_orm), |             selectinload(ShoppingList.extras), | ||||||
|                 "extras": {x.key_name: x.value for x in name_orm.extras}, |             selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.extras), | ||||||
|             } |             selectinload(ShoppingList.list_items) | ||||||
|  |             .joinedload(ShoppingListItem.food) | ||||||
|  |             .joinedload(IngredientFoodModel.extras), | ||||||
|  |             selectinload(ShoppingList.list_items) | ||||||
|  |             .joinedload(ShoppingListItem.food) | ||||||
|  |             .joinedload(IngredientFoodModel.label), | ||||||
|  |             selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.label), | ||||||
|  |             selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.unit), | ||||||
|  |             selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.recipe_references), | ||||||
|  |             selectinload(ShoppingList.recipe_references) | ||||||
|  |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(ShoppingList.recipe_references) | ||||||
|  |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(ShoppingList.recipe_references) | ||||||
|  |             .joinedload(ShoppingListRecipeReference.recipe) | ||||||
|  |             .joinedload(RecipeModel.tools), | ||||||
|  |             selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShoppingListAddRecipeParams(MealieModel): | class ShoppingListAddRecipeParams(MealieModel): | ||||||
|   | |||||||
| @@ -3,7 +3,11 @@ from enum import Enum | |||||||
| from uuid import UUID | from uuid import UUID | ||||||
|  |  | ||||||
| from pydantic import validator | from pydantic import validator | ||||||
|  | from sqlalchemy.orm import selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.group import GroupMealPlan | ||||||
|  | from mealie.db.models.recipe import RecipeModel | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.recipe.recipe import RecipeSummary | from mealie.schema.recipe.recipe import RecipeSummary | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
| @@ -57,6 +61,14 @@ class ReadPlanEntry(UpdatePlanEntry): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.tools), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlanEntryPagination(PaginationBase): | class PlanEntryPagination(PaginationBase): | ||||||
|     items: list[ReadPlanEntry] |     items: list[ReadPlanEntry] | ||||||
|   | |||||||
| @@ -2,7 +2,10 @@ import datetime | |||||||
| from enum import Enum | from enum import Enum | ||||||
|  |  | ||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
|  | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.group import GroupMealPlanRules | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
| @@ -65,6 +68,10 @@ class PlanRulesOut(PlanRulesSave): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(GroupMealPlanRules.categories), joinedload(GroupMealPlanRules.tags)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlanRulesPagination(PaginationBase): | class PlanRulesPagination(PaginationBase): | ||||||
|     items: list[PlanRulesOut] |     items: list[PlanRulesOut] | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| from pydantic.utils import GetterDict |  | ||||||
|  |  | ||||||
| from mealie.db.models.group.shopping_list import ShoppingList |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  | from mealie.schema.getter_dict import GroupGetterDict | ||||||
|  |  | ||||||
|  |  | ||||||
| class ListItem(MealieModel): | class ListItem(MealieModel): | ||||||
| @@ -25,10 +23,4 @@ class ShoppingListOut(ShoppingListIn): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = GroupGetterDict | ||||||
|         @classmethod |  | ||||||
|         def getter_dict(cls, ormModel: ShoppingList): |  | ||||||
|             return { |  | ||||||
|                 **GetterDict(ormModel), |  | ||||||
|                 "group": ormModel.group.name, |  | ||||||
|             } |  | ||||||
|   | |||||||
| @@ -6,14 +6,22 @@ from typing import Any | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from pydantic import UUID4, BaseModel, Field, validator | from pydantic import UUID4, BaseModel, Field, validator | ||||||
| from pydantic.utils import GetterDict |  | ||||||
| from slugify import slugify | from slugify import slugify | ||||||
|  | from sqlalchemy.orm import joinedload, selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.core.config import get_app_dirs | from mealie.core.config import get_app_dirs | ||||||
| from mealie.db.models.recipe.recipe import RecipeModel |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
|  | from ...db.models.recipe import ( | ||||||
|  |     IngredientFoodModel, | ||||||
|  |     RecipeComment, | ||||||
|  |     RecipeIngredientModel, | ||||||
|  |     RecipeInstruction, | ||||||
|  |     RecipeModel, | ||||||
|  | ) | ||||||
|  | from ..getter_dict import ExtrasGetterDict | ||||||
| from .recipe_asset import RecipeAsset | from .recipe_asset import RecipeAsset | ||||||
| from .recipe_comments import RecipeCommentOut | from .recipe_comments import RecipeCommentOut | ||||||
| from .recipe_notes import RecipeNote | from .recipe_notes import RecipeNote | ||||||
| @@ -147,16 +155,7 @@ class Recipe(RecipeSummary): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = ExtrasGetterDict | ||||||
|         @classmethod |  | ||||||
|         def getter_dict(cls, name_orm: RecipeModel): |  | ||||||
|             return { |  | ||||||
|                 **GetterDict(name_orm), |  | ||||||
|                 # "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient], |  | ||||||
|                 # "recipe_category": [x.name for x in name_orm.recipe_category], |  | ||||||
|                 # "tags": [x.name for x in name_orm.tags], |  | ||||||
|                 "extras": {x.key_name: x.value for x in name_orm.extras}, |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|     @validator("slug", always=True, pre=True, allow_reuse=True) |     @validator("slug", always=True, pre=True, allow_reuse=True) | ||||||
|     def validate_slug(slug: str, values):  # type: ignore |     def validate_slug(slug: str, values):  # type: ignore | ||||||
| @@ -199,6 +198,29 @@ class Recipe(RecipeSummary): | |||||||
|             return uuid4() |             return uuid4() | ||||||
|         return user_id |         return user_id | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(RecipeModel.assets), | ||||||
|  |             selectinload(RecipeModel.comments).joinedload(RecipeComment.user), | ||||||
|  |             selectinload(RecipeModel.extras), | ||||||
|  |             joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(RecipeModel.tags), | ||||||
|  |             selectinload(RecipeModel.tools), | ||||||
|  |             selectinload(RecipeModel.recipe_ingredient).joinedload(RecipeIngredientModel.unit), | ||||||
|  |             selectinload(RecipeModel.recipe_ingredient) | ||||||
|  |             .joinedload(RecipeIngredientModel.food) | ||||||
|  |             .joinedload(IngredientFoodModel.extras), | ||||||
|  |             selectinload(RecipeModel.recipe_ingredient) | ||||||
|  |             .joinedload(RecipeIngredientModel.food) | ||||||
|  |             .joinedload(IngredientFoodModel.label), | ||||||
|  |             selectinload(RecipeModel.recipe_instructions).joinedload(RecipeInstruction.ingredient_references), | ||||||
|  |             joinedload(RecipeModel.nutrition), | ||||||
|  |             joinedload(RecipeModel.settings), | ||||||
|  |             # for whatever reason, joinedload can mess up the order here, so use selectinload just this once | ||||||
|  |             selectinload(RecipeModel.notes), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeLastMade(BaseModel): | class RecipeLastMade(BaseModel): | ||||||
|     timestamp: datetime.datetime |     timestamp: datetime.datetime | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
| from pydantic.utils import GetterDict | from sqlalchemy.orm import selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.recipe import RecipeModel, Tag | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -19,12 +21,6 @@ class CategoryBase(CategoryIn): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|         @classmethod |  | ||||||
|         def getter_dict(_cls, name_orm): |  | ||||||
|             return { |  | ||||||
|                 **GetterDict(name_orm), |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CategoryOut(CategoryBase): | class CategoryOut(CategoryBase): | ||||||
|     slug: str |     slug: str | ||||||
| @@ -62,7 +58,13 @@ class TagOut(TagSave): | |||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeTagResponse(RecipeCategoryResponse): | class RecipeTagResponse(RecipeCategoryResponse): | ||||||
|     pass |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(Tag.recipes).joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(Tag.recipes).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(Tag.recipes).joinedload(RecipeModel.tools), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| from mealie.schema.recipe.recipe import RecipeSummary  # noqa: E402 | from mealie.schema.recipe.recipe import RecipeSummary  # noqa: E402 | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
|  | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.recipe import RecipeComment | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
| @@ -40,6 +43,10 @@ class RecipeCommentOut(RecipeCommentCreate): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(RecipeComment.user)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeCommentPagination(PaginationBase): | class RecipeCommentPagination(PaginationBase): | ||||||
|     items: list[RecipeCommentOut] |     items: list[RecipeCommentOut] | ||||||
|   | |||||||
| @@ -2,14 +2,16 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| import datetime | import datetime | ||||||
| import enum | import enum | ||||||
| from typing import Any |  | ||||||
| from uuid import UUID, uuid4 | from uuid import UUID, uuid4 | ||||||
|  |  | ||||||
| from pydantic import UUID4, Field, validator | from pydantic import UUID4, Field, validator | ||||||
| from pydantic.utils import GetterDict | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.recipe import IngredientFoodModel | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema._mealie.types import NoneFloat | from mealie.schema._mealie.types import NoneFloat | ||||||
|  | from mealie.schema.getter_dict import ExtrasGetterDict | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
| INGREDIENT_QTY_PRECISION = 3 | INGREDIENT_QTY_PRECISION = 3 | ||||||
| @@ -37,19 +39,12 @@ class IngredientFood(CreateIngredientFood): | |||||||
|     update_at: datetime.datetime | None |     update_at: datetime.datetime | None | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         class _FoodGetter(GetterDict): |  | ||||||
|             def get(self, key: Any, default: Any = None) -> Any: |  | ||||||
|                 # Transform extras into key-value dict |  | ||||||
|                 if key == "extras": |  | ||||||
|                     value = super().get(key, default) |  | ||||||
|                     return {x.key_name: x.value for x in value} |  | ||||||
|  |  | ||||||
|                 # Keep all other fields as they are |  | ||||||
|                 else: |  | ||||||
|                     return super().get(key, default) |  | ||||||
|  |  | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|         getter_dict = _FoodGetter |         getter_dict = ExtrasGetterDict | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(IngredientFoodModel.extras), joinedload(IngredientFoodModel.label)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class IngredientFoodPagination(PaginationBase): | class IngredientFoodPagination(PaginationBase): | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
|  |  | ||||||
| from pydantic import UUID4, Field | from pydantic import UUID4, Field | ||||||
|  | from sqlalchemy.orm import selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  |  | ||||||
|  | from ...db.models.recipe import RecipeIngredientModel, RecipeInstruction, RecipeModel, RecipeShareTokenModel | ||||||
| from .recipe import Recipe | from .recipe import Recipe | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -33,3 +36,26 @@ class RecipeShareToken(RecipeShareTokenSummary): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.tools), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.nutrition), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.settings), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.assets), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.notes), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.extras), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe).joinedload(RecipeModel.comments), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe) | ||||||
|  |             .joinedload(RecipeModel.recipe_instructions) | ||||||
|  |             .joinedload(RecipeInstruction.ingredient_references), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe) | ||||||
|  |             .joinedload(RecipeModel.recipe_ingredient) | ||||||
|  |             .joinedload(RecipeIngredientModel.unit), | ||||||
|  |             selectinload(RecipeShareTokenModel.recipe) | ||||||
|  |             .joinedload(RecipeModel.recipe_ingredient) | ||||||
|  |             .joinedload(RecipeIngredientModel.food), | ||||||
|  |         ] | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
|  | from sqlalchemy.orm import selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  |  | ||||||
|  | from ...db.models.recipe import RecipeModel, Tool | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeToolCreate(MealieModel): | class RecipeToolCreate(MealieModel): | ||||||
|     name: str |     name: str | ||||||
| @@ -21,12 +25,20 @@ class RecipeToolOut(RecipeToolCreate): | |||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeToolResponse(RecipeToolOut): | class RecipeToolResponse(RecipeToolOut): | ||||||
|     recipes: list["Recipe"] = [] |     recipes: list["RecipeSummary"] = [] | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(Tool.recipes).joinedload(RecipeModel.recipe_category), | ||||||
|  |             selectinload(Tool.recipes).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(Tool.recipes).joinedload(RecipeModel.tools), | ||||||
|  |         ] | ||||||
|  |  | ||||||
| from .recipe import Recipe  # noqa: E402 |  | ||||||
|  | from .recipe import RecipeSummary  # noqa: E402 | ||||||
|  |  | ||||||
| RecipeToolResponse.update_forward_refs() | RecipeToolResponse.update_forward_refs() | ||||||
|   | |||||||
| @@ -3,7 +3,10 @@ import enum | |||||||
|  |  | ||||||
| from pydantic import Field | from pydantic import Field | ||||||
| from pydantic.types import UUID4 | from pydantic.types import UUID4 | ||||||
|  | from sqlalchemy.orm import joinedload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
|  | from mealie.db.models.group import ReportModel | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -53,3 +56,7 @@ class ReportOut(ReportSummary): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(ReportModel.entries)] | ||||||
|   | |||||||
| @@ -5,7 +5,8 @@ from uuid import UUID | |||||||
|  |  | ||||||
| from pydantic import UUID4, Field, validator | from pydantic import UUID4, Field, validator | ||||||
| from pydantic.types import constr | from pydantic.types import constr | ||||||
| from pydantic.utils import GetterDict | from sqlalchemy.orm import joinedload, selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.core.config import get_app_dirs, get_app_settings | from mealie.core.config import get_app_dirs, get_app_settings | ||||||
| from mealie.db.models.users import User | from mealie.db.models.users import User | ||||||
| @@ -15,6 +16,9 @@ from mealie.schema.group.group_preferences import ReadGroupPreferences | |||||||
| from mealie.schema.recipe import RecipeSummary | from mealie.schema.recipe import RecipeSummary | ||||||
| from mealie.schema.response.pagination import PaginationBase | from mealie.schema.response.pagination import PaginationBase | ||||||
|  |  | ||||||
|  | from ...db.models.group import Group | ||||||
|  | from ...db.models.recipe import RecipeModel | ||||||
|  | from ..getter_dict import GroupGetterDict, UserGetterDict | ||||||
| from ..recipe import CategoryBase | from ..recipe import CategoryBase | ||||||
|  |  | ||||||
| DEFAULT_INTEGRATION_ID = "generic" | DEFAULT_INTEGRATION_ID = "generic" | ||||||
| @@ -78,19 +82,8 @@ class UserBase(MealieModel): | |||||||
|     can_organize: bool = False |     can_organize: bool = False | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         class _UserGetter(GetterDict): |  | ||||||
|             def get(self, key: Any, default: Any = None) -> Any: |  | ||||||
|                 # Transform extras into key-value dict |  | ||||||
|                 if key == "group": |  | ||||||
|                     value = super().get(key, default) |  | ||||||
|                     return value.group.name |  | ||||||
|  |  | ||||||
|                 # Keep all other fields as they are |  | ||||||
|                 else: |  | ||||||
|                     return super().get(key, default) |  | ||||||
|  |  | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|         getter_dict = _UserGetter |         getter_dict = GroupGetterDict | ||||||
|  |  | ||||||
|         schema_extra = { |         schema_extra = { | ||||||
|             "example": { |             "example": { | ||||||
| @@ -118,13 +111,11 @@ class UserOut(UserBase): | |||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |         getter_dict = UserGetterDict | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|         def getter_dict(cls, ormModel: User): |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|             return { |         return [joinedload(User.group), joinedload(User.favorite_recipes), joinedload(User.tokens)] | ||||||
|                 **GetterDict(ormModel), |  | ||||||
|                 "group": ormModel.group.name, |  | ||||||
|                 "favorite_recipes": [x.slug for x in ormModel.favorite_recipes], |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserPagination(PaginationBase): | class UserPagination(PaginationBase): | ||||||
| @@ -136,13 +127,16 @@ class UserFavorites(UserBase): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |         getter_dict = GroupGetterDict | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|         def getter_dict(cls, ormModel: User): |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|             return { |         return [ | ||||||
|                 **GetterDict(ormModel), |             joinedload(User.group), | ||||||
|                 "group": ormModel.group.name, |             selectinload(User.favorite_recipes).joinedload(RecipeModel.recipe_category), | ||||||
|             } |             selectinload(User.favorite_recipes).joinedload(RecipeModel.tags), | ||||||
|  |             selectinload(User.favorite_recipes).joinedload(RecipeModel.tools), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PrivateUser(UserOut): | class PrivateUser(UserOut): | ||||||
| @@ -175,6 +169,10 @@ class PrivateUser(UserOut): | |||||||
|     def directory(self) -> Path: |     def directory(self) -> Path: | ||||||
|         return PrivateUser.get_directory(self.id) |         return PrivateUser.get_directory(self.id) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [joinedload(User.group), selectinload(User.favorite_recipes), joinedload(User.tokens)] | ||||||
|  |  | ||||||
|  |  | ||||||
| class UpdateGroup(GroupBase): | class UpdateGroup(GroupBase): | ||||||
|     id: UUID4 |     id: UUID4 | ||||||
| @@ -211,6 +209,17 @@ class GroupInDB(UpdateGroup): | |||||||
|     def exports(self) -> Path: |     def exports(self) -> Path: | ||||||
|         return GroupInDB.get_export_directory(self.id) |         return GroupInDB.get_export_directory(self.id) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             joinedload(Group.categories), | ||||||
|  |             joinedload(Group.webhooks), | ||||||
|  |             joinedload(Group.preferences), | ||||||
|  |             selectinload(Group.users).joinedload(User.group), | ||||||
|  |             selectinload(Group.users).joinedload(User.favorite_recipes), | ||||||
|  |             selectinload(Group.users).joinedload(User.tokens), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupPagination(PaginationBase): | class GroupPagination(PaginationBase): | ||||||
|     items: list[GroupInDB] |     items: list[GroupInDB] | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
|  | from sqlalchemy.orm import selectinload | ||||||
|  | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
|  |  | ||||||
|  | from ...db.models.users import PasswordResetModel, User | ||||||
| from .user import PrivateUser | from .user import PrivateUser | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -33,3 +36,11 @@ class PrivatePasswordResetToken(SavePasswordResetToken): | |||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def loader_options(cls) -> list[LoaderOption]: | ||||||
|  |         return [ | ||||||
|  |             selectinload(PasswordResetModel.user).joinedload(User.group), | ||||||
|  |             selectinload(PasswordResetModel.user).joinedload(User.favorite_recipes), | ||||||
|  |             selectinload(PasswordResetModel.user).joinedload(User.tokens), | ||||||
|  |         ] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user