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:32 +0000

1from __future__ import annotations 1a

2 

3from datetime import datetime 1a

4from uuid import UUID 1a

5 

6from pydantic import UUID4, ConfigDict, field_validator, model_validator 1a

7from sqlalchemy.orm import joinedload, selectinload 1a

8from sqlalchemy.orm.interfaces import LoaderOption 1a

9 

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

30 

31 

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

36 

37 recipe_scale: NoneFloat = 1 1a

38 """the number of times this recipe has been added""" 1a

39 

40 recipe_note: str | None = None 1a

41 """the original note from the recipe""" 1a

42 

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 

47 

48 

49class ShoppingListItemRecipeRefUpdate(ShoppingListItemRecipeRefCreate): 1a

50 id: UUID4 1a

51 shopping_list_item_id: UUID4 1a

52 

53 

54class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRefUpdate): 1a

55 model_config = ConfigDict(from_attributes=True) 1a

56 

57 

58class ShoppingListItemBase(RecipeIngredientBase): 1a

59 shopping_list_id: UUID4 1a

60 checked: bool = False 1a

61 position: int = 0 1a

62 

63 quantity: float = 1 1a

64 

65 food_id: UUID4 | None = None 1a

66 label_id: UUID4 | None = None 1a

67 unit_id: UUID4 | None = None 1a

68 

69 extras: dict | None = {} 1a

70 

71 @field_validator("extras", mode="before") 1a

72 def convert_extras_to_dict(cls, v): 1a

73 if isinstance(v, dict): 

74 return v 

75 

76 return {x.key_name: x.value for x in v} if v else {} 

77 

78 

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

83 

84 @field_validator("id", mode="before") 1a

85 def validate_id(cls, v): 1a

86 v = v or None 

87 if not v or isinstance(v, UUID): 

88 return v 

89 

90 try: 

91 return UUID(v) 

92 except Exception: 

93 return None 

94 

95 

96class ShoppingListItemUpdate(ShoppingListItemBase): 1a

97 recipe_references: list[ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate] = [] 1a

98 

99 

100class ShoppingListItemUpdateBulk(ShoppingListItemUpdate): 1a

101 """Only used for bulk update operations where the shopping list item id isn't already supplied""" 

102 

103 id: UUID4 1a

104 

105 

106class ShoppingListItemOut(ShoppingListItemBase): 1a

107 id: UUID4 1a

108 group_id: UUID4 1a

109 household_id: UUID4 1a

110 

111 food: IngredientFood | None = None 1a

112 label: MultiPurposeLabelSummary | None = None 1a

113 unit: IngredientUnit | None = None 1a

114 

115 recipe_references: list[ShoppingListItemRecipeRefOut] = [] 1a

116 

117 created_at: datetime | None = None 1a

118 updated_at: datetime | None = UpdatedAtField(None) 1a

119 

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 

126 

127 return self 

128 

129 model_config = ConfigDict(from_attributes=True) 1a

130 

131 @classmethod 1a

132 def loader_options(cls) -> list[LoaderOption]: 1a

133 return [ 1l

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 ] 

144 

145 

146class ShoppingListItemsCollectionOut(MealieModel): 1a

147 """Container for bulk shopping list item changes""" 

148 

149 created_items: list[ShoppingListItemOut] = [] 1a

150 updated_items: list[ShoppingListItemOut] = [] 1a

151 deleted_items: list[ShoppingListItemOut] = [] 1a

152 

153 

154class ShoppingListMultiPurposeLabelCreate(MealieModel): 1a

155 shopping_list_id: UUID4 1a

156 label_id: UUID4 1a

157 position: int = 0 1a

158 

159 

160class ShoppingListMultiPurposeLabelUpdate(ShoppingListMultiPurposeLabelCreate): 1a

161 id: UUID4 1a

162 

163 

164class ShoppingListMultiPurposeLabelOut(ShoppingListMultiPurposeLabelUpdate): 1a

165 label: MultiPurposeLabelSummary 1a

166 model_config = ConfigDict(from_attributes=True) 1a

167 

168 @classmethod 1a

169 def loader_options(cls) -> list[LoaderOption]: 1a

170 return [joinedload(ShoppingListMultiPurposeLabel.label)] 

171 

172 

173class ShoppingListItemPagination(PaginationBase): 1a

174 items: list[ShoppingListItemOut] 1a

175 

176 

177class ShoppingListCreate(MealieModel): 1a

178 name: str | None = None 1a

179 extras: dict | None = {} 1a

180 

181 created_at: datetime | None = None 1a

182 updated_at: datetime | None = UpdatedAtField(None) 1a

183 

184 @field_validator("extras", mode="before") 1a

185 def convert_extras_to_dict(cls, v): 1a

186 if isinstance(v, dict): 1cdefghijkb

187 return v 1cdefghijkb

188 

189 return {x.key_name: x.value for x in v} if v else {} 1cdefghijkb

190 

191 

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

198 

199 recipe: RecipeSummary 1a

200 model_config = ConfigDict(from_attributes=True) 1a

201 

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 ] 

209 

210 

211class ShoppingListSave(ShoppingListCreate): 1a

212 group_id: UUID4 1a

213 user_id: UUID4 1a

214 

215 

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

222 

223 @classmethod 1a

224 def loader_options(cls) -> list[LoaderOption]: 1a

225 return [ 1l

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 ] 

239 

240 

241class ShoppingListPagination(PaginationBase): 1a

242 items: list[ShoppingListSummary] 1a

243 

244 

245class ShoppingListUpdate(ShoppingListSave): 1a

246 id: UUID4 1a

247 list_items: list[ShoppingListItemOut] = [] 1a

248 

249 

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

255 

256 @field_validator("recipe_references", "label_settings", mode="before") 1a

257 def default_none_to_empty_list(cls, v): 1a

258 return v or [] 1cdefghijkb

259 

260 @classmethod 1a

261 def loader_options(cls) -> list[LoaderOption]: 1a

262 return [ 1cdefghijkb

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 ] 

286 

287 

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

292 

293 

294class ShoppingListAddRecipeParamsBulk(ShoppingListAddRecipeParams): 1a

295 recipe_id: UUID4 1a

296 

297 

298class ShoppingListRemoveRecipeParams(MealieModel): 1a

299 recipe_decrement_quantity: float = 1 1a