Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/household/group_shopping_list.py: 95%
159 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
1from __future__ import annotations 1a
3from datetime import datetime 1a
4from uuid import UUID 1a
6from pydantic import UUID4, ConfigDict, field_validator, model_validator 1a
7from sqlalchemy.orm import joinedload, selectinload 1a
8from sqlalchemy.orm.interfaces import LoaderOption 1a
10from mealie.db.models.household import ( 1a
11 ShoppingList,
12 ShoppingListItem,
13 ShoppingListMultiPurposeLabel,
14 ShoppingListRecipeReference,
15)
16from mealie.db.models.recipe import IngredientFoodModel, RecipeModel 1a
17from mealie.db.models.users.users import User 1a
18from mealie.schema._mealie import MealieModel 1a
19from mealie.schema._mealie.mealie_model import UpdatedAtField 1a
20from mealie.schema._mealie.types import NoneFloat 1a
21from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary 1a
22from mealie.schema.recipe.recipe import RecipeSummary 1a
23from mealie.schema.recipe.recipe_ingredient import ( 1a
24 IngredientFood,
25 IngredientUnit,
26 RecipeIngredient,
27 RecipeIngredientBase,
28)
29from mealie.schema.response.pagination import PaginationBase 1a
32class ShoppingListItemRecipeRefCreate(MealieModel): 1a
33 recipe_id: UUID4 1a
34 recipe_quantity: float = 0 1a
35 """the quantity of this item in a single recipe (scale == 1)""" 1a
37 recipe_scale: NoneFloat = 1 1a
38 """the number of times this recipe has been added""" 1a
40 recipe_note: str | None = None 1a
41 """the original note from the recipe""" 1a
43 @field_validator("recipe_quantity", mode="before") 1a
44 @classmethod 1a
45 def default_none_to_zero(cls, v): 1a
46 return 0 if v is None else v 1cqb
49class ShoppingListItemRecipeRefUpdate(ShoppingListItemRecipeRefCreate): 1a
50 id: UUID4 1a
51 shopping_list_item_id: UUID4 1a
54class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRefUpdate): 1a
55 model_config = ConfigDict(from_attributes=True) 1a
58class ShoppingListItemBase(RecipeIngredientBase): 1a
59 shopping_list_id: UUID4 1a
60 checked: bool = False 1a
61 position: int = 0 1a
63 quantity: float = 1 1a
65 food_id: UUID4 | None = None 1a
66 label_id: UUID4 | None = None 1a
67 unit_id: UUID4 | None = None 1a
69 extras: dict | None = {} 1a
71 @field_validator("extras", mode="before") 1a
72 def convert_extras_to_dict(cls, v): 1a
73 if isinstance(v, dict): 1cqb
74 return v 1cqb
76 return {x.key_name: x.value for x in v} if v else {} 1cqb
79class ShoppingListItemCreate(ShoppingListItemBase): 1a
80 id: UUID4 | None = None 1a
81 """The unique id of the item to create. If not supplied, one will be generated.""" 1a
82 recipe_references: list[ShoppingListItemRecipeRefCreate] = [] 1a
84 @field_validator("id", mode="before") 1a
85 def validate_id(cls, v): 1a
86 v = v or None 1cb
87 if not v or isinstance(v, UUID): 1cb
88 return v 1cb
90 try: 1cb
91 return UUID(v) 1cb
92 except Exception: 1cb
93 return None 1cb
96class ShoppingListItemUpdate(ShoppingListItemBase): 1a
97 recipe_references: list[ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate] = [] 1a
100class ShoppingListItemUpdateBulk(ShoppingListItemUpdate): 1a
101 """Only used for bulk update operations where the shopping list item id isn't already supplied"""
103 id: UUID4 1a
106class ShoppingListItemOut(ShoppingListItemBase): 1a
107 id: UUID4 1a
108 group_id: UUID4 1a
109 household_id: UUID4 1a
111 food: IngredientFood | None = None 1a
112 label: MultiPurposeLabelSummary | None = None 1a
113 unit: IngredientUnit | None = None 1a
115 recipe_references: list[ShoppingListItemRecipeRefOut] = [] 1a
117 created_at: datetime | None = None 1a
118 updated_at: datetime | None = UpdatedAtField(None) 1a
120 @model_validator(mode="after") 1a
121 def populate_missing_label(self): 1a
122 # if we're missing a label, but the food has a label, use that as the label
123 if (not self.label) and (self.food and self.food.label):
124 self.label = self.food.label
125 self.label_id = self.label.id
127 return self
129 model_config = ConfigDict(from_attributes=True) 1a
131 @classmethod 1a
132 def loader_options(cls) -> list[LoaderOption]: 1a
133 return [
134 selectinload(ShoppingListItem.extras),
135 selectinload(ShoppingListItem.food).joinedload(IngredientFoodModel.extras),
136 selectinload(ShoppingListItem.food).joinedload(IngredientFoodModel.label),
137 joinedload(ShoppingListItem.label),
138 joinedload(ShoppingListItem.unit),
139 selectinload(ShoppingListItem.recipe_references),
140 joinedload(ShoppingListItem.shopping_list)
141 .joinedload(ShoppingList.user)
142 .load_only(User.household_id, User.group_id),
143 ]
146class ShoppingListItemsCollectionOut(MealieModel): 1a
147 """Container for bulk shopping list item changes"""
149 created_items: list[ShoppingListItemOut] = [] 1a
150 updated_items: list[ShoppingListItemOut] = [] 1a
151 deleted_items: list[ShoppingListItemOut] = [] 1a
154class ShoppingListMultiPurposeLabelCreate(MealieModel): 1a
155 shopping_list_id: UUID4 1a
156 label_id: UUID4 1a
157 position: int = 0 1a
160class ShoppingListMultiPurposeLabelUpdate(ShoppingListMultiPurposeLabelCreate): 1a
161 id: UUID4 1a
164class ShoppingListMultiPurposeLabelOut(ShoppingListMultiPurposeLabelUpdate): 1a
165 label: MultiPurposeLabelSummary 1a
166 model_config = ConfigDict(from_attributes=True) 1a
168 @classmethod 1a
169 def loader_options(cls) -> list[LoaderOption]: 1a
170 return [joinedload(ShoppingListMultiPurposeLabel.label)]
173class ShoppingListItemPagination(PaginationBase): 1a
174 items: list[ShoppingListItemOut] 1a
177class ShoppingListCreate(MealieModel): 1a
178 name: str | None = None 1a
179 extras: dict | None = {} 1a
181 created_at: datetime | None = None 1a
182 updated_at: datetime | None = UpdatedAtField(None) 1a
184 @field_validator("extras", mode="before") 1a
185 def convert_extras_to_dict(cls, v): 1a
186 if isinstance(v, dict): 1defghijrklmnopb
187 return v 1defghijklmnopb
189 return {x.key_name: x.value for x in v} if v else {} 1defghijrklmnopb
192class ShoppingListRecipeRefOut(MealieModel): 1a
193 id: UUID4 1a
194 shopping_list_id: UUID4 1a
195 recipe_id: UUID4 1a
196 recipe_quantity: float 1a
197 """the number of times this recipe has been added""" 1a
199 recipe: RecipeSummary 1a
200 model_config = ConfigDict(from_attributes=True) 1a
202 @classmethod 1a
203 def loader_options(cls) -> list[LoaderOption]: 1a
204 return [
205 selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.recipe_category),
206 selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.tags),
207 selectinload(ShoppingListRecipeReference.recipe).joinedload(RecipeModel.tools),
208 ]
211class ShoppingListSave(ShoppingListCreate): 1a
212 group_id: UUID4 1a
213 user_id: UUID4 1a
216class ShoppingListSummary(ShoppingListSave): 1a
217 id: UUID4 1a
218 household_id: UUID4 1a
219 recipe_references: list[ShoppingListRecipeRefOut] 1a
220 label_settings: list[ShoppingListMultiPurposeLabelOut] 1a
221 model_config = ConfigDict(from_attributes=True) 1a
223 @classmethod 1a
224 def loader_options(cls) -> list[LoaderOption]: 1a
225 return [
226 selectinload(ShoppingList.extras),
227 selectinload(ShoppingList.recipe_references)
228 .joinedload(ShoppingListRecipeReference.recipe)
229 .joinedload(RecipeModel.recipe_category),
230 selectinload(ShoppingList.recipe_references)
231 .joinedload(ShoppingListRecipeReference.recipe)
232 .joinedload(RecipeModel.tags),
233 selectinload(ShoppingList.recipe_references)
234 .joinedload(ShoppingListRecipeReference.recipe)
235 .joinedload(RecipeModel.tools),
236 selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label),
237 joinedload(ShoppingList.user).load_only(User.household_id, User.group_id),
238 ]
241class ShoppingListPagination(PaginationBase): 1a
242 items: list[ShoppingListSummary] 1a
245class ShoppingListUpdate(ShoppingListSave): 1a
246 id: UUID4 1a
247 list_items: list[ShoppingListItemOut] = [] 1a
250class ShoppingListOut(ShoppingListUpdate): 1a
251 household_id: UUID4 1a
252 recipe_references: list[ShoppingListRecipeRefOut] = [] 1a
253 label_settings: list[ShoppingListMultiPurposeLabelOut] = [] 1a
254 model_config = ConfigDict(from_attributes=True) 1a
256 @field_validator("recipe_references", "label_settings", mode="before") 1a
257 def default_none_to_empty_list(cls, v): 1a
258 return v or [] 1defghijrklmnopb
260 @classmethod 1a
261 def loader_options(cls) -> list[LoaderOption]: 1a
262 return [ 1defghijrklmnopb
263 selectinload(ShoppingList.extras),
264 selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.extras),
265 selectinload(ShoppingList.list_items)
266 .joinedload(ShoppingListItem.food)
267 .joinedload(IngredientFoodModel.extras),
268 selectinload(ShoppingList.list_items)
269 .joinedload(ShoppingListItem.food)
270 .joinedload(IngredientFoodModel.label),
271 selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.label),
272 selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.unit),
273 selectinload(ShoppingList.list_items).joinedload(ShoppingListItem.recipe_references),
274 selectinload(ShoppingList.recipe_references)
275 .joinedload(ShoppingListRecipeReference.recipe)
276 .joinedload(RecipeModel.recipe_category),
277 selectinload(ShoppingList.recipe_references)
278 .joinedload(ShoppingListRecipeReference.recipe)
279 .joinedload(RecipeModel.tags),
280 selectinload(ShoppingList.recipe_references)
281 .joinedload(ShoppingListRecipeReference.recipe)
282 .joinedload(RecipeModel.tools),
283 selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label),
284 joinedload(ShoppingList.user).load_only(User.household_id, User.group_id),
285 ]
288class ShoppingListAddRecipeParams(MealieModel): 1a
289 recipe_increment_quantity: float = 1 1a
290 recipe_ingredients: list[RecipeIngredient] | None = None 1a
291 """optionally override which ingredients are added from the recipe""" 1a
294class ShoppingListAddRecipeParamsBulk(ShoppingListAddRecipeParams): 1a
295 recipe_id: UUID4 1a
298class ShoppingListRemoveRecipeParams(MealieModel): 1a
299 recipe_decrement_quantity: float = 1 1a