torque-http 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torque_http/__init__.py +121 -0
- torque_http/client.py +1471 -0
- torque_http/exceptions.py +96 -0
- torque_http/models.py +302 -0
- torque_http/py.typed +0 -0
- torque_http/tcp.py +575 -0
- torque_http-0.5.0.dist-info/METADATA +132 -0
- torque_http-0.5.0.dist-info/RECORD +10 -0
- torque_http-0.5.0.dist-info/WHEEL +4 -0
- torque_http-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Torque client exceptions — typed error hierarchy.
|
|
2
|
+
|
|
3
|
+
Maps HTTP status codes to specific exception types for precise
|
|
4
|
+
error handling. Zero external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TorqueError(Exception):
|
|
11
|
+
"""Base exception for all Torque client errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TorqueApiError(TorqueError):
|
|
15
|
+
"""Raised when the Torque server returns a non-2xx response.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
status_code: HTTP status code from the server.
|
|
19
|
+
message: Human-readable error message.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str, *, status_code: int) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.message = message
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ObjectNotFound(TorqueApiError):
|
|
29
|
+
"""Raised when the requested resource does not exist (HTTP 404)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ObjectAlreadyExists(TorqueApiError):
|
|
33
|
+
"""Raised when the resource already exists (HTTP 409)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RequestUnauthorized(TorqueApiError):
|
|
37
|
+
"""Raised when authentication fails (HTTP 401)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RequestForbidden(TorqueApiError):
|
|
41
|
+
"""Raised when the API key lacks required permissions (HTTP 403)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RequestMalformed(TorqueApiError):
|
|
45
|
+
"""Raised when the request is invalid (HTTP 400)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ServerError(TorqueApiError):
|
|
49
|
+
"""Raised when the server encounters an internal error (HTTP 500+)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ServiceUnavailable(TorqueApiError):
|
|
53
|
+
"""Raised when the server is temporarily unavailable (HTTP 503)."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RequestTimeout(TorqueApiError):
|
|
57
|
+
"""Raised when the request times out."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, message: str = "Request timed out") -> None:
|
|
60
|
+
super().__init__(message, status_code=408)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Keep backward compatibility: TorqueApiException is an alias
|
|
64
|
+
TorqueApiException = TorqueApiError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Status code -> exception mapping ──────────────────────────────────
|
|
68
|
+
|
|
69
|
+
_STATUS_EXCEPTION_MAP: dict[int, type[TorqueApiError]] = {
|
|
70
|
+
400: RequestMalformed,
|
|
71
|
+
401: RequestUnauthorized,
|
|
72
|
+
403: RequestForbidden,
|
|
73
|
+
404: ObjectNotFound,
|
|
74
|
+
408: RequestTimeout,
|
|
75
|
+
409: ObjectAlreadyExists,
|
|
76
|
+
503: ServiceUnavailable,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def raise_for_status(status_code: int, message: str) -> None:
|
|
81
|
+
"""Raise the appropriate typed exception for the given HTTP status code.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
status_code: HTTP status code from the server response.
|
|
85
|
+
message: Error message from the server or a default.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
TorqueApiError: A subclass matching the status code, or the base
|
|
89
|
+
class for unrecognized codes.
|
|
90
|
+
"""
|
|
91
|
+
exc_cls = _STATUS_EXCEPTION_MAP.get(status_code)
|
|
92
|
+
if exc_cls is not None:
|
|
93
|
+
raise exc_cls(message, status_code=status_code)
|
|
94
|
+
if status_code >= 500:
|
|
95
|
+
raise ServerError(message, status_code=status_code)
|
|
96
|
+
raise TorqueApiError(message, status_code=status_code)
|
torque_http/models.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Torque response models — dataclasses, zero dependencies.
|
|
2
|
+
|
|
3
|
+
Includes typed search parameters for Typesense v30.1 API compatibility.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, TypedDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ── Collections ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FieldDefinition:
|
|
16
|
+
"""Schema field definition."""
|
|
17
|
+
name: str
|
|
18
|
+
type: str
|
|
19
|
+
facet: bool = False
|
|
20
|
+
index: bool = True
|
|
21
|
+
sort: bool = False
|
|
22
|
+
optional: bool = False
|
|
23
|
+
stem: bool = False
|
|
24
|
+
locale: str | None = None
|
|
25
|
+
num_dim: int | None = None
|
|
26
|
+
vec_dist: str | None = None
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict[str, Any]:
|
|
29
|
+
d: dict[str, Any] = {"name": self.name, "type": self.type}
|
|
30
|
+
if self.facet: d["facet"] = True
|
|
31
|
+
if not self.index: d["index"] = False
|
|
32
|
+
if self.sort: d["sort"] = True
|
|
33
|
+
if self.optional: d["optional"] = True
|
|
34
|
+
if self.stem: d["stem"] = True
|
|
35
|
+
if self.locale: d["locale"] = self.locale
|
|
36
|
+
if self.num_dim: d["num_dim"] = self.num_dim
|
|
37
|
+
if self.vec_dist: d["vec_dist"] = self.vec_dist
|
|
38
|
+
return d
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CollectionSchema:
|
|
43
|
+
"""Schema for creating a collection."""
|
|
44
|
+
name: str
|
|
45
|
+
fields: list[FieldDefinition]
|
|
46
|
+
default_sorting_field: str | None = None
|
|
47
|
+
enable_nested_fields: bool = False
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, Any]:
|
|
50
|
+
d: dict[str, Any] = {
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"fields": [f.to_dict() for f in self.fields],
|
|
53
|
+
}
|
|
54
|
+
if self.default_sorting_field:
|
|
55
|
+
d["default_sorting_field"] = self.default_sorting_field
|
|
56
|
+
if self.enable_nested_fields:
|
|
57
|
+
d["enable_nested_fields"] = True
|
|
58
|
+
return d
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CollectionResponse:
|
|
63
|
+
"""Collection metadata returned by the server."""
|
|
64
|
+
name: str
|
|
65
|
+
num_documents: int = 0
|
|
66
|
+
fields: list[dict[str, Any]] = field(default_factory=list)
|
|
67
|
+
default_sorting_field: str | None = None
|
|
68
|
+
enable_nested_fields: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Documents ────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ImportResult:
|
|
75
|
+
"""Result of a single document in a batch import."""
|
|
76
|
+
success: bool
|
|
77
|
+
error: str | None = None
|
|
78
|
+
id: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class DeleteResult:
|
|
83
|
+
"""Result of a document or batch deletion."""
|
|
84
|
+
num_deleted: int = 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class UpdateResult:
|
|
89
|
+
"""Result of a batch update by filter."""
|
|
90
|
+
num_updated: int = 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── Search ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Hit:
|
|
97
|
+
"""Single search result hit."""
|
|
98
|
+
document: dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
text_match: int = 0
|
|
100
|
+
highlights: list[dict[str, Any]] = field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class FacetCount:
|
|
105
|
+
"""Facet value with count."""
|
|
106
|
+
value: str = ""
|
|
107
|
+
count: int = 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class FacetResult:
|
|
112
|
+
"""Facet field with value counts."""
|
|
113
|
+
field_name: str = ""
|
|
114
|
+
counts: list[FacetCount] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class SearchResult:
|
|
119
|
+
"""Search response from the server."""
|
|
120
|
+
found: int = 0
|
|
121
|
+
out_of: int = 0
|
|
122
|
+
page: int = 1
|
|
123
|
+
per_page: int = 10
|
|
124
|
+
hits: list[Hit] = field(default_factory=list)
|
|
125
|
+
facet_counts: list[FacetResult] = field(default_factory=list)
|
|
126
|
+
search_time_ms: int = 0
|
|
127
|
+
search_time_us: int = 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class GroupedHit:
|
|
132
|
+
"""Group of search results."""
|
|
133
|
+
group_key: list[str] = field(default_factory=list)
|
|
134
|
+
hits: list[Hit] = field(default_factory=list)
|
|
135
|
+
found: int = 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class GroupedSearchResult:
|
|
140
|
+
"""Grouped search response."""
|
|
141
|
+
found: int = 0
|
|
142
|
+
out_of: int = 0
|
|
143
|
+
grouped_hits: list[GroupedHit] = field(default_factory=list)
|
|
144
|
+
search_time_us: int = 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SearchParameters(TypedDict, total=False):
|
|
148
|
+
"""Typed search parameters for Typesense v30.1 API.
|
|
149
|
+
|
|
150
|
+
All fields are optional. Pass as keyword arguments to ``search()``
|
|
151
|
+
or construct a dict and unpack it::
|
|
152
|
+
|
|
153
|
+
params = SearchParameters(q="laptop", query_by="title,description")
|
|
154
|
+
results = client.search("products", **params)
|
|
155
|
+
|
|
156
|
+
See https://typesense.org/docs/30.1/api/search.html for full details.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
# ── Query ─────────────────────────────────────────────────────────
|
|
160
|
+
q: str
|
|
161
|
+
query_by: str
|
|
162
|
+
query_by_weights: str
|
|
163
|
+
prefix: bool | str
|
|
164
|
+
num_typos: int | str
|
|
165
|
+
|
|
166
|
+
# ── Filtering & sorting ───────────────────────────────────────────
|
|
167
|
+
filter_by: str
|
|
168
|
+
sort_by: str
|
|
169
|
+
|
|
170
|
+
# ── Faceting ──────────────────────────────────────────────────────
|
|
171
|
+
facet_by: str
|
|
172
|
+
max_facet_values: int
|
|
173
|
+
facet_query: str
|
|
174
|
+
|
|
175
|
+
# ── Grouping ──────────────────────────────────────────────────────
|
|
176
|
+
group_by: str
|
|
177
|
+
group_limit: int
|
|
178
|
+
|
|
179
|
+
# ── Pagination ────────────────────────────────────────────────────
|
|
180
|
+
per_page: int
|
|
181
|
+
page: int
|
|
182
|
+
offset: int
|
|
183
|
+
limit: int
|
|
184
|
+
|
|
185
|
+
# ── Field selection ───────────────────────────────────────────────
|
|
186
|
+
include_fields: str
|
|
187
|
+
exclude_fields: str
|
|
188
|
+
|
|
189
|
+
# ── Highlighting ──────────────────────────────────────────────────
|
|
190
|
+
highlight_fields: str
|
|
191
|
+
highlight_full_fields: str
|
|
192
|
+
highlight_start_tag: str
|
|
193
|
+
highlight_end_tag: str
|
|
194
|
+
snippet_threshold: int
|
|
195
|
+
|
|
196
|
+
# ── Pinning / hiding ──────────────────────────────────────────────
|
|
197
|
+
pinned_hits: str
|
|
198
|
+
hidden_hits: str
|
|
199
|
+
enable_overrides: bool
|
|
200
|
+
|
|
201
|
+
# ── Token dropping ────────────────────────────────────────────────
|
|
202
|
+
drop_tokens_threshold: int
|
|
203
|
+
typo_tokens_threshold: int
|
|
204
|
+
|
|
205
|
+
# ── Performance ───────────────────────────────────────────────────
|
|
206
|
+
exhaustive_search: bool
|
|
207
|
+
search_cutoff_ms: int
|
|
208
|
+
|
|
209
|
+
# ── Preset ────────────────────────────────────────────────────────
|
|
210
|
+
preset: str
|
|
211
|
+
|
|
212
|
+
# ── Stopwords ─────────────────────────────────────────────────────
|
|
213
|
+
stopwords: str
|
|
214
|
+
|
|
215
|
+
# ── Vector search ─────────────────────────────────────────────────
|
|
216
|
+
vector_query: str
|
|
217
|
+
|
|
218
|
+
# ── Decay sorting ─────────────────────────────────────────────────
|
|
219
|
+
text_match_type: str
|
|
220
|
+
|
|
221
|
+
# ── Priority (Torque extension) ───────────────────────────────────
|
|
222
|
+
prioritize_exact_match: bool
|
|
223
|
+
prioritize_token_position: bool
|
|
224
|
+
prioritize_num_matching_fields: bool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── Aliases ──────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class CollectionAlias:
|
|
231
|
+
"""Collection alias."""
|
|
232
|
+
name: str = ""
|
|
233
|
+
collection_name: str = ""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ── API Keys ─────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class ApiKey:
|
|
240
|
+
"""API key."""
|
|
241
|
+
id: int = 0
|
|
242
|
+
description: str = ""
|
|
243
|
+
actions: list[str] = field(default_factory=list)
|
|
244
|
+
collections: list[str] = field(default_factory=list)
|
|
245
|
+
value: str | None = None
|
|
246
|
+
expires_at: int | None = None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ── Synonyms ─────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class SynonymItem:
|
|
253
|
+
"""Synonym rule (one-way or multi-way)."""
|
|
254
|
+
id: str = ""
|
|
255
|
+
synonyms: list[str] = field(default_factory=list)
|
|
256
|
+
root: str | None = None
|
|
257
|
+
locale: str | None = None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass
|
|
261
|
+
class SynonymSet:
|
|
262
|
+
"""Named synonym set containing items."""
|
|
263
|
+
name: str = ""
|
|
264
|
+
items: list[SynonymItem] = field(default_factory=list)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Presets ──────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
@dataclass
|
|
270
|
+
class Preset:
|
|
271
|
+
"""Saved search preset."""
|
|
272
|
+
id: str = ""
|
|
273
|
+
value: dict[str, Any] = field(default_factory=dict)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ── Stopwords ────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
@dataclass
|
|
279
|
+
class StopwordSet:
|
|
280
|
+
"""Named stopword set."""
|
|
281
|
+
id: str = ""
|
|
282
|
+
stopwords: list[str] = field(default_factory=list)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── Health ───────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class HealthResponse:
|
|
289
|
+
"""Server health response."""
|
|
290
|
+
ok: bool = False
|
|
291
|
+
version: str = ""
|
|
292
|
+
license: dict[str, Any] = field(default_factory=dict)
|
|
293
|
+
update: dict[str, Any] | None = None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ── Admin ────────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class AdminStats:
|
|
300
|
+
"""Server admin statistics."""
|
|
301
|
+
system: dict[str, Any] = field(default_factory=dict)
|
|
302
|
+
collections: list[dict[str, Any]] = field(default_factory=list)
|
torque_http/py.typed
ADDED
|
File without changes
|