Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/cookbook/cookbook.py: 96%
68 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 typing import Annotated 1a
3import sqlalchemy as sa 1a
4from pydantic import UUID4, ConfigDict, Field, ValidationInfo, field_validator 1a
5from slugify import slugify 1a
6from sqlalchemy.orm import joinedload 1a
7from sqlalchemy.orm.interfaces import LoaderOption 1a
9from mealie.core.root_logger import get_logger 1a
10from mealie.db.models.household.cookbook import CookBook 1a
11from mealie.db.models.recipe import RecipeModel 1a
12from mealie.schema._mealie import MealieModel 1a
13from mealie.schema.response.pagination import PaginationBase 1a
14from mealie.schema.response.query_filter import QueryFilterBuilder, QueryFilterJSON 1a
16logger = get_logger() 1a
19class CookbookHousehold(MealieModel): 1a
20 id: UUID4 1a
21 name: str 1a
22 model_config = ConfigDict(from_attributes=True) 1a
25class CreateCookBook(MealieModel): 1a
26 name: str 1a
27 description: str = "" 1a
28 slug: Annotated[str | None, Field(validate_default=True)] = None 1a
29 position: int = 1 1a
30 public: Annotated[bool, Field(validate_default=True)] = False 1a
31 query_filter_string: str = "" 1a
33 @field_validator("public", mode="before") 1a
34 def validate_public(public: bool | None) -> bool: 1a
35 return False if public is None else public 1qocdefghinjkplmb
37 @field_validator("name") 1a
38 def validate_name(name: str) -> str: 1a
39 name = name.strip() 1qocdefghinjkplmb
41 # we calculate the slug later leveraging the database,
42 # but we still need to validate the name can be slugified
43 possible_slug = slugify(name) 1qocdefghinjkplmb
44 if not (name and possible_slug): 1qocdefghinjkplmb
45 raise ValueError("Name cannot be empty") 1qocdefghijklmb
47 return name 1qocdefghinjkplmb
49 @field_validator("query_filter_string") 1a
50 def validate_query_filter_string(value: str) -> str: 1a
51 # The query filter builder does additional validations while building the
52 # database query, so we make sure constructing the query is successful
53 builder = QueryFilterBuilder(value) 1qocdefghinjklmb
55 try: 1qocdefghinjklmb
56 builder.filter_query(sa.select(RecipeModel), RecipeModel) 1qocdefghinjklmb
57 except Exception as e:
58 raise ValueError("Invalid query filter string") from e
60 return value 1qocdefghinjklmb
63class SaveCookBook(CreateCookBook): 1a
64 group_id: UUID4 1a
65 household_id: UUID4 1a
68class UpdateCookBook(SaveCookBook): 1a
69 id: UUID4 1a
72class ReadCookBook(UpdateCookBook): 1a
73 query_filter: Annotated[QueryFilterJSON, Field(validate_default=True)] = None # type: ignore 1a
74 household: CookbookHousehold | None = None 1a
76 model_config = ConfigDict(from_attributes=True) 1a
78 @field_validator("query_filter_string") 1a
79 def validate_query_filter_string(value: str) -> str: 1a
80 # Skip validation since we are not updating the query filter string
81 return value 1cdefghinjkplmb
83 @field_validator("query_filter", mode="before") 1a
84 def validate_query_filter(cls, _, info: ValidationInfo) -> QueryFilterJSON: 1a
85 try: 1cdefghinjkplmb
86 query_filter_string: str = info.data.get("query_filter_string") or "" 1cdefghinjkplmb
87 builder = QueryFilterBuilder(query_filter_string) 1cdefghinjkplmb
88 return builder.as_json_model() 1cdefghinjkplmb
89 except Exception:
90 logger.exception(f"Invalid query filter string: {query_filter_string}")
91 return QueryFilterJSON()
93 @classmethod 1a
94 def loader_options(cls) -> list[LoaderOption]: 1a
95 return [joinedload(CookBook.household)] 1rocdefghinjkplmb
98class CookBookPagination(PaginationBase): 1a
99 items: list[ReadCookBook] 1a