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
« 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#
13from __future__ import annotations
15from enum import StrEnum
16from typing import override
18from flipdare.app_globals import sanitize_search_input
19from flipdare.app_log import LOG
20from flipdare.constants import IS_DEBUG
22__all__ = ["SimpleFilter", "FilterType"]
24type FilterType = str | list[str]
27class SimpleFilter[T: StrEnum]:
28 __slots__ = ("_filters", "_obj_type", "_prepare_called", "_relationship")
30 def __init__(
31 self,
32 filters: dict[T, FilterType] | None = None,
33 ) -> None:
34 self._filters = filters
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)
43 return self._build_filter(base_filter)
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
51 return {key.value: value for key, value in filter_by.items()}
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
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
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)
81 filters.append(f"{key}:={values}")
83 return " && ".join(filters) if filters else None
85 @override
86 def __str__(self) -> str:
87 return f"BaseFilter(filters={self._filters})"
89 @override
90 def __repr__(self) -> str:
91 return self.__str__()
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
99 @override
100 def __hash__(self) -> int:
101 return hash(frozenset(self.filter_by_dict.items()) if self.filter_by_dict else None)