Coverage for functions \ flipdare \ search \ core \ query_builder.py: 97%
99 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 abc import ABC, abstractmethod
16from typing import Any, Self, override
18from flipdare.app_log import LOG
19from flipdare.constants import IS_DEBUG
20from flipdare.generated.schema.search.friend_document_schema import FriendDocumentKey
21from flipdare.generated.schema.search.general_document_schema import GeneralDocumentKey
22from flipdare.generated.schema.search.search_request_schema import SearchRequestSchema
23from flipdare.generated.shared.search.search_collections import SearchCollections
24from flipdare.generated.shared.search.search_obj_type import SearchObjType
25from flipdare.generated.shared.search.search_sort_type import SearchSortType
26from flipdare.search.core.filter._complex_filter import Relationship
27from flipdare.search.core.filter.friend_filter import ComplexFriendFilter
28from flipdare.search.core.filter.general_filter import ComplexGeneralFilter
29from flipdare.search.core.query.friend_query import FriendQuery
30from flipdare.search.core.query.general_query import GeneralQuery
31from flipdare.search.core.query_by import FriendQueryBy, GeneralQueryBy
32from flipdare.search.core.query_options import QueryOptions
34__all__ = ["QueryBuilder", "GeneralQueryBuilder", "FriendQueryBuilder", "QueryBuilderFactory"]
36type QueryBuilderType = GeneralQuery | FriendQuery
39class QueryBuilderFactory:
40 @staticmethod
41 def create(
42 request: SearchRequestSchema,
43 uid: str | None = None,
44 ) -> QueryBuilder[QueryBuilderType]:
45 collection = request["collection"]
46 match collection:
47 case SearchCollections.GENERAL:
48 return GeneralQueryBuilder.from_request(request, uid)
49 case SearchCollections.FRIEND:
50 return FriendQueryBuilder.from_request(request, uid)
53class QueryBuilder[T: GeneralQuery | FriendQuery](ABC):
54 query_str: str
55 sort_type: SearchSortType
56 page: int
57 collection: SearchCollections
58 relationship: Relationship | None
59 obj_types: list[SearchObjType] | None
60 auto_complete: bool
62 def __init__(
63 self,
64 collection: SearchCollections,
65 query_str: str,
66 page: int,
67 sort_type: SearchSortType,
68 relationship: Relationship | None = None,
69 obj_types: list[SearchObjType] | None = None,
70 auto_complete: bool = False,
71 ) -> None:
72 self.query_str = query_str
73 self.page = page
74 self.sort_type = sort_type
75 self.collection = collection
77 self.relationship = relationship
78 self.auto_complete = auto_complete
80 self.obj_types = obj_types
82 @classmethod
83 def from_request(cls, request: SearchRequestSchema, uid: str | None = None) -> Self:
84 relationship: Relationship | None = None
85 relation_type = request.get("relation_type")
86 if uid is not None and relation_type is not None:
87 relationship = Relationship(uid=uid, relation_type=relation_type)
89 return cls(
90 collection=request["collection"],
91 query_str=request["query"],
92 page=request["page_num"],
93 sort_type=request["sort_type"],
94 relationship=relationship,
95 obj_types=request.get("obj_types"),
96 auto_complete=request.get("auto_complete", False),
97 )
99 @abstractmethod
100 def build(self) -> T: ...
103class GeneralQueryBuilder(QueryBuilder[GeneralQuery]):
104 def __init__(
105 self,
106 **kwargs: Any,
107 ) -> None:
108 kwargs.pop("collection", None) # Ensure collection is not passed to super
110 super().__init__(collection=SearchCollections.GENERAL, **kwargs)
112 @override
113 def build(self) -> GeneralQuery:
114 obj_types = self.obj_types
115 relationship = self.relationship
116 auto_complete = self.auto_complete
117 query = self.query_str
118 sort_type = self.sort_type
119 page = self.page
121 if IS_DEBUG:
122 msg = f"Building relationship={relationship} obj_types={obj_types}, auto_complete={auto_complete}"
123 LOG().debug(msg)
125 filter_by: ComplexGeneralFilter | None = None
126 if relationship is None:
127 filter_by = ComplexGeneralFilter(obj_types=obj_types)
128 else:
129 relation_type = relationship.relation_type
130 uid = relationship.uid
131 if relation_type.is_mine:
132 # we dont pass the relationship,
133 # we just filter for the user's own UID since that's what "mine" means.
134 filter_by = ComplexGeneralFilter(
135 obj_types=obj_types,
136 filters={GeneralDocumentKey.UID: uid},
137 )
138 else:
139 filter_by = ComplexGeneralFilter(
140 obj_types=obj_types,
141 relationship=relationship,
142 )
144 options = QueryOptions.general() if not auto_complete else QueryOptions.auto()
145 query_by = GeneralQueryBy([GeneralDocumentKey.KEYWORDS, GeneralDocumentKey.TAGS])
146 return GeneralQuery.query(
147 query_str=query,
148 query_by=query_by,
149 filter_by=filter_by,
150 query_options=options,
151 sort_type=sort_type,
152 page=page,
153 )
156class FriendQueryBuilder(QueryBuilder[FriendQuery]):
157 def __init__(
158 self,
159 uid: str,
160 **kwargs: Any,
161 ) -> None:
162 kwargs.pop("collection", None) # Ensure collection is not passed to super
164 super().__init__(collection=SearchCollections.FRIEND, **kwargs)
165 # friend requires a uid even without a relationship,
166 # since we need to know whose friends to search.
167 self._uid = uid
169 @override
170 def build(self) -> FriendQuery:
171 relationship = self.relationship
172 auto_complete = self.auto_complete
173 query = self.query_str
174 sort_type = self.sort_type
175 page = self.page
177 if relationship is None or relationship.relation_type.is_mine:
178 filter_by = ComplexFriendFilter(uid=self._uid)
179 else: # relationship.relation_type.is_friends
180 filter_by = ComplexFriendFilter(
181 uid=self._uid,
182 filters={FriendDocumentKey.FRIEND_UID: self._uid},
183 exclude_uid=True,
184 )
186 if IS_DEBUG:
187 msg = f"Building query for relationship={relationship} auto_complete={auto_complete}"
188 LOG().debug(msg)
190 options = QueryOptions.friend() if not auto_complete else QueryOptions.auto()
191 query_by = FriendQueryBy([FriendDocumentKey.KEYWORDS, FriendDocumentKey.FRIEND_KEYWORDS])
192 return FriendQuery.query(
193 query_str=query,
194 query_by=query_by,
195 filter_by=filter_by,
196 query_options=options,
197 sort_type=sort_type,
198 page=page,
199 )