Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/response/pagination.py: 97%
67 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
1import enum
2from typing import Annotated, Any
3from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
5from humps import camelize
6from pydantic import UUID4, BaseModel, Field, field_validator
7from pydantic_core.core_schema import ValidationInfo
9from mealie.schema._mealie import MealieModel
12class OrderDirection(str, enum.Enum):
13 asc = "asc"
14 desc = "desc"
17class OrderByNullPosition(str, enum.Enum):
18 first = "first"
19 last = "last"
22class RecipeSearchQuery(MealieModel):
23 cookbook: UUID4 | str | None = None
24 require_all_categories: bool = False
25 require_all_tags: bool = False
26 require_all_tools: bool = False
27 require_all_foods: bool = False
28 search: str | None = None
29 _search_seed: str | None = None
32class RequestQuery(MealieModel):
33 order_by: str | None = None
34 order_by_null_position: OrderByNullPosition | None = None
35 order_direction: OrderDirection = OrderDirection.desc
36 query_filter: str | None = None
37 pagination_seed: Annotated[str | None, Field(validate_default=True)] = None
39 @field_validator("pagination_seed", mode="before")
40 def validate_randseed(cls, pagination_seed, info: ValidationInfo):
41 if info.data.get("order_by") == "random" and not pagination_seed: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 raise ValueError("paginationSeed is required when orderBy is random")
43 return pagination_seed
46class PaginationQuery(RequestQuery):
47 page: int = 1
48 per_page: int = 50
51class PaginationBase[DataT: BaseModel](BaseModel):
52 page: int = 1
53 per_page: int = 10
54 total: int = 0
55 total_pages: int = 0
56 items: list[DataT]
57 next: str | None = None
58 previous: str | None = None
60 def _set_next(self, route: str, query_params: dict[str, Any]) -> None:
61 if self.page >= self.total_pages:
62 self.next = None
63 return
65 # combine params with base route
66 query_params["page"] = self.page + 1
67 self.next = PaginationBase.merge_query_parameters(route, query_params)
69 def _set_prev(self, route: str, query_params: dict[str, Any]) -> None:
70 if self.page <= 1:
71 self.previous = None
72 return
74 # combine params with base route
75 query_params["page"] = self.page - 1
76 self.previous = PaginationBase.merge_query_parameters(route, query_params)
78 def set_pagination_guides(self, route: str, query_params: dict[str, Any] | None) -> None:
79 valid_dict: dict[str, Any] = camelize(query_params) if query_params else {}
81 # sanitize user input
82 self.page = max(self.page, 1)
83 self._set_next(route, valid_dict)
84 self._set_prev(route, valid_dict)
86 @staticmethod
87 def merge_query_parameters(url: str, params: dict[str, Any]):
88 scheme, netloc, path, query_string, fragment = urlsplit(url)
90 query_params = parse_qs(query_string)
91 query_params.update(params)
92 new_query_string = urlencode(query_params, doseq=True)
94 return urlunsplit((scheme, netloc, path, new_query_string, fragment))