Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/response/query_search.py: 95%
37 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 re
3from sqlalchemy import Select
4from sqlalchemy.orm import Session
5from text_unidecode import unidecode
7from ...db.models._model_base import SqlAlchemyBase
8from .._mealie import MealieModel, SearchType
11class SearchFilter:
12 """
13 0. fuzzy search (postgres only) and tokenized search are performed separately
14 1. take search string and do a little pre-normalization
15 2. look for internal quoted strings and keep them together as "literal" parts of the search
16 3. remove special characters from each non-literal search string
17 """
19 punctuation = r"!\#$%&()*+,-./:;<=>?@[\\]^_`{|}~" # string.punctuation with ' & " removed
20 quoted_regex = re.compile(r"""(["'])(?:(?=(\\?))\2.)*?\1""")
21 remove_quotes_regex = re.compile(r"""['"](.*)['"]""")
23 @classmethod
24 def _normalize_search(cls, search: str, normalize_characters: bool) -> str:
25 search = search.translate(str.maketrans(cls.punctuation, " " * len(cls.punctuation)))
27 if normalize_characters:
28 search = unidecode(search).lower().strip()
29 else:
30 search = search.strip()
32 return search
34 @classmethod
35 def _build_search_list(cls, search: str) -> list[str]:
36 if cls.quoted_regex.search(search):
37 # all quoted strings
38 quoted_search_list = [match.group() for match in cls.quoted_regex.finditer(search)]
40 # remove outer quotes
41 quoted_search_list = [cls.remove_quotes_regex.sub("\\1", x) for x in quoted_search_list]
43 # punctuation->spaces for splitting, but only on unquoted strings
44 search = cls.quoted_regex.sub("", search) # remove all quoted strings, leaving just non-quoted
45 search = search.translate(str.maketrans(cls.punctuation, " " * len(cls.punctuation)))
47 # all unquoted strings
48 unquoted_search_list = search.split()
49 search_list = quoted_search_list + unquoted_search_list
50 else:
51 search_list = search.translate(str.maketrans(cls.punctuation, " " * len(cls.punctuation))).split()
53 # remove padding whitespace inside quotes
54 return [x.strip() for x in search_list]
56 def __init__(self, session: Session, search: str, normalize_characters: bool = False) -> None:
57 if session.get_bind().name != "postgresql" or self.quoted_regex.search(search.strip()): 57 ↛ 60line 57 didn't jump to line 60 because the condition on line 57 was always true
58 self.search_type = SearchType.tokenized
59 else:
60 self.search_type = SearchType.fuzzy
62 self.session = session
63 self.search = self._normalize_search(search, normalize_characters)
64 self.search_list = self._build_search_list(self.search)
66 def filter_query_by_search(self, query: Select, schema: type[MealieModel], model: type[SqlAlchemyBase]) -> Select:
67 return schema.filter_search_query(model, query, self.session, self.search_type, self.search, self.search_list)