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.
@@ -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