Coverage for flogin/search_handler.py: 82%
78 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 22:51 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 22:51 +0000
1from __future__ import annotations
3import logging
4import re
5from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
7from ._types.search_handlers import (
8 PluginT,
9 SearchHandlerCallbackReturns,
10 SearchHandlerCondition,
11)
12from .conditions import KeywordCondition, PlainTextCondition, RegexCondition
13from .jsonrpc import ErrorResponse
14from .utils import MISSING, copy_doc, decorator
16if TYPE_CHECKING:
17 from collections.abc import Callable, Iterable
19 from .query import Query
21 ErrorHandlerT = TypeVar(
22 "ErrorHandlerT",
23 bound=Callable[[Query, Exception], SearchHandlerCallbackReturns],
24 )
26log = logging.getLogger(__name__)
28__all__ = ("SearchHandler",)
31class SearchHandler(Generic[PluginT]):
32 r"""This represents a search handler.
34 When creating this on your own, the :func:`~flogin.plugin.Plugin.register_search_handler` method can be used to register it.
36 See the :ref:`search handler section <search_handlers>` for more information about using search handlers.
38 There is a provided decorator to easily create search handlers: :func:`~flogin.plugin.Plugin.search`
40 The keywords in the constructor can also be passed into the subclassed init, like so: ::
42 class MyHandler(SearchHandler, text="text"):
43 ...
45 # is equal to
47 class MyHandler(SearchHandler):
48 def __init__(self):
49 super().__init__(text="text")
51 Parameters
52 ----------
53 condition: Optional[:ref:`condition <condition_example>`]
54 The condition to determine which queries this handler should run on. If given, this should be the only argument given.
55 text: Optional[:class:`str`]
56 A kwarg to quickly add a :class:`~flogin.conditions.PlainTextCondition`. If given, this should be the only argument given.
57 pattern: Optional[:class:`re.Pattern` | :class:`str`]
58 A kwarg to quickly add a :class:`~flogin.conditions.RegexCondition`. If given, this should be the only argument given.
59 keyword: Optional[:class:`str`]
60 A kwarg to quickly set the condition to a :class:`~flogin.conditions.KeywordCondition` condition with the ``keyword`` kwarg being the only allowed keyword.
61 allowed_keywords: Optional[Iterable[:class:`str`]]
62 A kwarg to quickly set the condition to a :class:`~flogin.conditions.KeywordCondition` condition with the kwarg being the list of allowed keywords.
63 disallowed_keywords: Optional[Iterable[:class:`str`]]
64 A kwarg to quickly set the condition to a :class:`~flogin.conditions.KeywordCondition` condition with the kwarg being the list of disallowed keywords.
66 Attributes
67 ------------
68 plugin: :class:`~flogin.plugin.Plugin` | None
69 Your plugin instance. This is filled before :func:`~flogin.search_handler.SearchHandler.callback` is triggered.
70 """
72 @overload
73 def __init__(self, condition: SearchHandlerCondition) -> None: ...
75 @overload
76 def __init__(self, *, text: str) -> None: ...
78 @overload
79 def __init__(
80 self,
81 *,
82 pattern: re.Pattern[str] | str = MISSING,
83 ) -> None: ...
85 @overload
86 def __init__(
87 self,
88 *,
89 keyword: str = MISSING,
90 ) -> None: ...
92 @overload
93 def __init__(
94 self,
95 *,
96 allowed_keywords: Iterable[str] = MISSING,
97 ) -> None: ...
99 @overload
100 def __init__(
101 self,
102 *,
103 disallowed_keywords: Iterable[str] = MISSING,
104 ) -> None: ...
106 def __init__(
107 self,
108 condition: SearchHandlerCondition | None = None,
109 *,
110 text: str = MISSING,
111 pattern: re.Pattern[str] | str = MISSING,
112 keyword: str = MISSING,
113 allowed_keywords: Iterable[str] = MISSING,
114 disallowed_keywords: Iterable[str] = MISSING,
115 ) -> None:
116 if condition is None:
117 condition = self._builtin_condition_kwarg_to_obj(
118 text=text,
119 pattern=pattern,
120 keyword=keyword,
121 allowed_keywords=allowed_keywords,
122 disallowed_keywords=disallowed_keywords,
123 )
124 if condition:
125 setattr(self, "condition", condition)
127 self.plugin: PluginT | None = None
129 @overload
130 def __init_subclass__(cls: type[SearchHandler], *, text: str) -> None: ...
132 @overload
133 def __init_subclass__(
134 cls: type[SearchHandler],
135 *,
136 pattern: re.Pattern[str] | str = MISSING,
137 ) -> None: ...
139 @overload
140 def __init_subclass__(
141 cls: type[SearchHandler],
142 *,
143 keyword: str = MISSING,
144 ) -> None: ...
146 @overload
147 def __init_subclass__(
148 cls: type[SearchHandler],
149 *,
150 allowed_keywords: Iterable[str] = MISSING,
151 ) -> None: ...
153 @overload
154 def __init_subclass__(
155 cls: type[SearchHandler],
156 *,
157 disallowed_keywords: Iterable[str] = MISSING,
158 ) -> None: ...
160 def __init_subclass__(
161 cls: type[SearchHandler],
162 *,
163 text: str = MISSING,
164 pattern: re.Pattern[str] | str = MISSING,
165 keyword: str = MISSING,
166 allowed_keywords: Iterable[str] = MISSING,
167 disallowed_keywords: Iterable[str] = MISSING,
168 ) -> None:
169 con = cls._builtin_condition_kwarg_to_obj(
170 text=text,
171 pattern=pattern,
172 keyword=keyword,
173 allowed_keywords=allowed_keywords,
174 disallowed_keywords=disallowed_keywords,
175 )
176 if con is not None:
177 setattr(cls, "condition", con)
179 @classmethod
180 def _builtin_condition_kwarg_to_obj(
181 cls: type[SearchHandler],
182 *,
183 text: str = MISSING,
184 pattern: re.Pattern[str] | str = MISSING,
185 keyword: str = MISSING,
186 allowed_keywords: Iterable[str] = MISSING,
187 disallowed_keywords: Iterable[str] = MISSING,
188 ) -> SearchHandlerCondition | None:
189 if text is not MISSING:
190 return PlainTextCondition(text)
191 if pattern is not MISSING:
192 if isinstance(pattern, str):
193 pattern = re.compile(pattern)
194 return RegexCondition(pattern)
195 if keyword is not MISSING:
196 return KeywordCondition(allowed_keywords=[keyword])
197 if allowed_keywords is not MISSING:
198 return KeywordCondition(allowed_keywords=allowed_keywords)
199 if disallowed_keywords is not MISSING:
200 return KeywordCondition(disallowed_keywords=disallowed_keywords)
202 def condition(self, query: Query) -> bool:
203 r"""A function that determines whether or not to fire off this search handler for a given query
205 Parameters
206 ----------
207 query: :class:`~flogin.query.Query`
208 The query object for the query request
210 Returns
211 --------
212 :class:`bool`
213 Whether or not to fire off this handler for the given query.
214 """
216 return True
218 def callback(self, query: Query) -> SearchHandlerCallbackReturns:
219 r"""|coro|
221 Override this function to add the search handler behavior you want for the set condition.
223 This method can return/yield almost anything, and flogin will convert it into a list of :class:`~flogin.jsonrpc.results.Result` objects before sending it to flow.
225 Returns
226 -------
227 list[:class:`~flogin.jsonrpc.results.Result`] | :class:`~flogin.jsonrpc.results.Result` | str | Any
228 A list of results, an results, or something that can be converted into a list of results.
230 Yields
231 ------
232 :class:`~flogin.jsonrpc.results.Result` | str | Any
233 A result object or something that can be converted into a result object.
234 """
235 ...
237 def on_error(self, query: Query, error: Exception) -> SearchHandlerCallbackReturns:
238 r"""|coro|
240 Override this function to add an error response behavior to this handler's callback.
242 If the error was handled:
243 You can return/yield almost anything, and flogin will convert it into a list of :class:`~flogin.jsonrpc.results.Result` objects before sending it to flow.
245 If the error was not handled:
246 Return a :class:`~flogin.jsonrpc.responses.ErrorResponse` object
248 Parameters
249 ----------
250 query: :class:`~flogin.query.Query`
251 The query that was being handled when the error occured.
252 error: :class:`Exception`
253 The error that occured
255 Returns
256 -------
257 :class:`~flogin.jsonrpc.responses.ErrorResponse` | list[:class:`~flogin.jsonrpc.results.Result`] | :class:`~flogin.jsonrpc.results.Result` | str | Any
258 A list of results, an results, or something that can be converted into a list of results.
260 Yields
261 ------
262 :class:`~flogin.jsonrpc.results.Result` | str | Any
263 A result object or something that can be converted into a result object.
264 """
265 ...
267 if not TYPE_CHECKING:
269 @copy_doc(callback)
270 async def callback(self, query: Query) -> Any:
271 raise RuntimeError("Callback was not overriden")
273 @copy_doc(on_error)
274 async def on_error(self, query: Query, error: Exception) -> Any:
275 log.exception(
276 "Ignoring exception in search handler callback (%r)",
277 self,
278 exc_info=error,
279 )
280 return ErrorResponse.internal_error(error)
282 @property
283 def name(self) -> str:
284 """:class:`str`: The name of the search handler's callback"""
285 return self.callback.__name__
287 @decorator
288 def error(self, func: ErrorHandlerT) -> ErrorHandlerT:
289 """A decorator that registers a error handler for this search handler.
291 For more information see :class:`~flogin.search_handler.SearchHandler.on_error`
293 Example
294 ---------
296 .. code-block:: python3
298 @plugin.search()
299 async def my_hander(query):
300 ..
302 @my_handler.error
303 async def my_error_handler(query, error):
304 ...
306 """
308 setattr(self, "on_error", func)
309 return func