Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/response/query_search.py: 95%

37 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-25 15:48 +0000

1import re 1b

2 

3from sqlalchemy import Select 1b

4from sqlalchemy.orm import Session 1b

5from text_unidecode import unidecode 1b

6 

7from ...db.models._model_base import SqlAlchemyBase 1b

8from .._mealie import MealieModel, SearchType 1b

9 

10 

11class SearchFilter: 1b

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 """ 

18 

19 punctuation = r"!\#$%&()*+,-./:;<=>?@[\\]^_`{|}~" # string.punctuation with ' & " removed 1b

20 quoted_regex = re.compile(r"""(["'])(?:(?=(\\?))\2.)*?\1""") 1b

21 remove_quotes_regex = re.compile(r"""['"](.*)['"]""") 1b

22 

23 @classmethod 1b

24 def _normalize_search(cls, search: str, normalize_characters: bool) -> str: 1b

25 search = search.translate(str.maketrans(cls.punctuation, " " * len(cls.punctuation))) 1cdefghijklmnqopa

26 

27 if normalize_characters: 1cdefghijklmnqopa

28 search = unidecode(search).lower().strip() 1cdefghijklmnopa

29 else: 

30 search = search.strip() 1cdefghijklmnqopa

31 

32 return search 1cdefghijklmnqopa

33 

34 @classmethod 1b

35 def _build_search_list(cls, search: str) -> list[str]: 1b

36 if cls.quoted_regex.search(search): 1cdefghijklmnqopa

37 # all quoted strings 

38 quoted_search_list = [match.group() for match in cls.quoted_regex.finditer(search)] 

39 

40 # remove outer quotes 

41 quoted_search_list = [cls.remove_quotes_regex.sub("\\1", x) for x in quoted_search_list] 

42 

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))) 

46 

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() 1cdefghijklmnqopa

52 

53 # remove padding whitespace inside quotes 

54 return [x.strip() for x in search_list] 1cdefghijklmnqopa

55 

56 def __init__(self, session: Session, search: str, normalize_characters: bool = False) -> None: 1b

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 true1cdefghijklmnqopa

58 self.search_type = SearchType.tokenized 1cdefghijklmnqopa

59 else: 

60 self.search_type = SearchType.fuzzy 

61 

62 self.session = session 1cdefghijklmnqopa

63 self.search = self._normalize_search(search, normalize_characters) 1cdefghijklmnqopa

64 self.search_list = self._build_search_list(self.search) 1cdefghijklmnqopa

65 

66 def filter_query_by_search(self, query: Select, schema: type[MealieModel], model: type[SqlAlchemyBase]) -> Select: 1b

67 return schema.filter_search_query(model, query, self.session, self.search_type, self.search, self.search_list) 1cdefghijklmnqopa