Coverage for documents/search/_errors.py: 44%
28 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from __future__ import annotations
3from typing import TYPE_CHECKING
5if TYPE_CHECKING: 5 ↛ 6line 5 didn't jump to line 6 because the condition on line 5 was never true
6 from collections.abc import Sequence
9class SearchQueryError(ValueError):
10 """
11 Base for user-fixable search query errors.
13 Carries a message safe to surface to the user (no internal details). The
14 view layer catches this and returns an HTTP 400, so any future subclass
15 gets the same treatment.
16 """
19class InvalidDateQuery(SearchQueryError):
20 """Raised when a date field value or range bound cannot be parsed."""
22 def __init__(self, field: str | None, value: str | None) -> None:
23 self.field = field
24 self.value = value
25 super().__init__(f"Invalid date value {value!r} for field {field!r}.")
28class InvalidNumberQuery(SearchQueryError):
29 """Raised when a numeric field value or range bound cannot be parsed."""
31 def __init__(self, field: str | None, value: str | None) -> None:
32 self.field = field
33 self.value = value
34 super().__init__(f"Invalid numeric value {value!r} for field {field!r}.")
37class QueryTooLongError(SearchQueryError):
38 """Raised when a query string exceeds the maximum allowed length.
40 whoosh-compat's fieldname tagger is O(n^2) in plain word characters, so an
41 unbounded query is a CPU-exhaustion vector against a single request
42 handler. This is a hard boundary, not a validation nicety.
43 """
45 def __init__(self, length: int, limit: int) -> None:
46 self.length = length
47 self.limit = limit
48 super().__init__(
49 f"The search query is too long ({length} characters). "
50 f"The maximum allowed length is {limit} characters.",
51 )
54class MultipleSearchQueryErrors(SearchQueryError):
55 """Aggregates every user-fixable error from one parse, not just the first."""
57 def __init__(self, errors: Sequence[SearchQueryError]) -> None:
58 self.errors = tuple(errors)
59 super().__init__("; ".join(str(e) for e in self.errors))
62def search_query_error_messages(e: SearchQueryError) -> list[str]:
63 """The user-facing message list for a SearchQueryError.
65 Every offending value's message, not just the first, so the user can
66 fix them all in one round-trip. Shared by every view that maps
67 SearchQueryError to an HTTP 400.
68 """
69 if isinstance(e, MultipleSearchQueryErrors):
70 return [str(sub) for sub in e.errors]
71 return [str(e)]