Coverage for functions \ flipdare \ search \ core \ filter \ _simple_filter.py: 82%

62 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13from __future__ import annotations 

14 

15from enum import StrEnum 

16from typing import override 

17 

18from flipdare.app_globals import sanitize_search_input 

19from flipdare.app_log import LOG 

20from flipdare.constants import IS_DEBUG 

21 

22__all__ = ["SimpleFilter", "FilterType"] 

23 

24type FilterType = str | list[str] 

25 

26 

27class SimpleFilter[T: StrEnum]: 

28 __slots__ = ("_filters", "_obj_type", "_prepare_called", "_relationship") 

29 

30 def __init__( 

31 self, 

32 filters: dict[T, FilterType] | None = None, 

33 ) -> None: 

34 self._filters = filters 

35 

36 @property 

37 def filters(self) -> str | None: 

38 base_filter = self.filter_by_dict 

39 if IS_DEBUG: 

40 msg = f"Building Filter with:\n\tBase Filter: {base_filter}\n" 

41 LOG().debug(msg) 

42 

43 return self._build_filter(base_filter) 

44 

45 @property 

46 def filter_by_dict(self) -> dict[str, FilterType] | None: 

47 filter_by = self._filters 

48 if filter_by is None: 

49 return None 

50 

51 return {key.value: value for key, value in filter_by.items()} 

52 

53 def add_filter(self, key: T, value: FilterType) -> None: 

54 match self._filters: 

55 case None: 

56 self._filters = {key: value} 

57 case dict(): 

58 self._filters[key] = value 

59 

60 @staticmethod 

61 def _combine_filters(filter1: str | None, filter2: str | None) -> str | None: 

62 """Combine two filter strings with AND logic.""" 

63 if filter1 and filter2: 

64 return f"{filter1} && {filter2}" 

65 return filter1 or filter2 

66 

67 @staticmethod 

68 def _build_filter(filter_by: dict[str, FilterType] | None) -> str | None: 

69 """Convert filter to Typesense filter string.""" 

70 if filter_by is None: 

71 return None 

72 # Handle dict - iterate over items and format as key:="value" 

73 filters = [] 

74 for key, value in filter_by.items(): 

75 values: str | None = None 

76 if isinstance(value, list): 

77 values = "[" + ", ".join(sanitize_search_input(v) for v in value) + "]" 

78 else: 

79 values = sanitize_search_input(value) 

80 

81 filters.append(f"{key}:={values}") 

82 

83 return " && ".join(filters) if filters else None 

84 

85 @override 

86 def __str__(self) -> str: 

87 return f"BaseFilter(filters={self._filters})" 

88 

89 @override 

90 def __repr__(self) -> str: 

91 return self.__str__() 

92 

93 @override 

94 def __eq__(self, other: object) -> bool: 

95 if not isinstance(other, SimpleFilter): 

96 return NotImplemented 

97 return self.filter_by_dict == other.filter_by_dict 

98 

99 @override 

100 def __hash__(self) -> int: 

101 return hash(frozenset(self.filter_by_dict.items()) if self.filter_by_dict else None)