narsil-client 0.1.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.
- narsil_client/__init__.py +36 -0
- narsil_client/_async_client.py +314 -0
- narsil_client/_async_operations/__init__.py +25 -0
- narsil_client/_async_operations/cluster.py +357 -0
- narsil_client/_async_operations/documents.py +496 -0
- narsil_client/_async_operations/indexes.py +358 -0
- narsil_client/_async_operations/maintenance.py +628 -0
- narsil_client/_async_operations/query_suggestions.py +388 -0
- narsil_client/_async_operations/search.py +142 -0
- narsil_client/_async_operations/server.py +232 -0
- narsil_client/_async_operations/sync.py +134 -0
- narsil_client/_async_operations/tasks.py +140 -0
- narsil_client/_async_transport.py +168 -0
- narsil_client/_client.py +317 -0
- narsil_client/_errors.py +95 -0
- narsil_client/_exchange.py +123 -0
- narsil_client/_failover.py +116 -0
- narsil_client/_failures.py +93 -0
- narsil_client/_helpers.py +76 -0
- narsil_client/_model.py +33 -0
- narsil_client/_operations/__init__.py +25 -0
- narsil_client/_operations/cluster.py +357 -0
- narsil_client/_operations/documents.py +496 -0
- narsil_client/_operations/indexes.py +358 -0
- narsil_client/_operations/maintenance.py +628 -0
- narsil_client/_operations/query_suggestions.py +388 -0
- narsil_client/_operations/search.py +142 -0
- narsil_client/_operations/server.py +232 -0
- narsil_client/_operations/sync.py +134 -0
- narsil_client/_operations/tasks.py +140 -0
- narsil_client/_streams.py +104 -0
- narsil_client/_sync_transport.py +144 -0
- narsil_client/_transport_core.py +177 -0
- narsil_client/_watchdog.py +142 -0
- narsil_client/models/__init__.py +2576 -0
- narsil_client/py.typed +0 -0
- narsil_client-0.1.0.dist-info/METADATA +55 -0
- narsil_client-0.1.0.dist-info/RECORD +40 -0
- narsil_client-0.1.0.dist-info/WHEEL +4 -0
- narsil_client-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from narsil_client._async_client import AsyncNarsilClient
|
|
2
|
+
from narsil_client._client import NarsilClient
|
|
3
|
+
from narsil_client._errors import ClientErrorCodes, NarsilError
|
|
4
|
+
from narsil_client._exchange import AsyncUpload, Upload
|
|
5
|
+
from narsil_client._helpers import (
|
|
6
|
+
ASYNC_IMPORT_CAPABILITY,
|
|
7
|
+
BM25_TUNING_CAPABILITY,
|
|
8
|
+
CHANGE_ANALYSIS_CAPABILITY,
|
|
9
|
+
DOCUMENT_EXPORT_CAPABILITY,
|
|
10
|
+
INDEX_LIFECYCLE_CAPABILITY,
|
|
11
|
+
REBUILD_ANALYSIS_CAPABILITY,
|
|
12
|
+
SYNONYMS_CAPABILITY,
|
|
13
|
+
TASK_CANCEL_CAPABILITY,
|
|
14
|
+
TASK_FILTER_CAPABILITY,
|
|
15
|
+
)
|
|
16
|
+
from narsil_client._streams import AsyncResponseStream, ResponseStream
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ASYNC_IMPORT_CAPABILITY",
|
|
20
|
+
"BM25_TUNING_CAPABILITY",
|
|
21
|
+
"CHANGE_ANALYSIS_CAPABILITY",
|
|
22
|
+
"DOCUMENT_EXPORT_CAPABILITY",
|
|
23
|
+
"INDEX_LIFECYCLE_CAPABILITY",
|
|
24
|
+
"REBUILD_ANALYSIS_CAPABILITY",
|
|
25
|
+
"SYNONYMS_CAPABILITY",
|
|
26
|
+
"TASK_CANCEL_CAPABILITY",
|
|
27
|
+
"TASK_FILTER_CAPABILITY",
|
|
28
|
+
"AsyncNarsilClient",
|
|
29
|
+
"AsyncResponseStream",
|
|
30
|
+
"AsyncUpload",
|
|
31
|
+
"ClientErrorCodes",
|
|
32
|
+
"NarsilClient",
|
|
33
|
+
"NarsilError",
|
|
34
|
+
"ResponseStream",
|
|
35
|
+
"Upload",
|
|
36
|
+
]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from narsil_client._async_operations import AsyncOperations
|
|
13
|
+
from narsil_client._async_transport import AsyncTransport
|
|
14
|
+
from narsil_client._errors import DOC_NOT_FOUND, NOT_FOUND, TASK_NOT_FOUND, NarsilError
|
|
15
|
+
from narsil_client._exchange import Exchange, ndjson, path_segment
|
|
16
|
+
from narsil_client._helpers import (
|
|
17
|
+
DEFAULT_TASK_POLL_INTERVAL_MS,
|
|
18
|
+
batch_body,
|
|
19
|
+
expect_import_result,
|
|
20
|
+
expect_task,
|
|
21
|
+
is_terminal,
|
|
22
|
+
poll_settings,
|
|
23
|
+
progress_key,
|
|
24
|
+
task_gone,
|
|
25
|
+
task_still_running,
|
|
26
|
+
)
|
|
27
|
+
from narsil_client._transport_core import build_settings
|
|
28
|
+
from narsil_client.models import AnyDocument, BatchResponse, ImportResult, TaskRecord
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncNarsilClient(AsyncOperations):
|
|
32
|
+
"""The async form of :class:`NarsilClient`, with the same methods, settings, and failover rules.
|
|
33
|
+
|
|
34
|
+
Every method is a coroutine. The client works under asyncio or trio, and it stops each try at exactly
|
|
35
|
+
`timeout_ms`. Close it with :meth:`close_connections` or an `async with` block.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
url: The server answers at this http or https address. Pass a list, such as one address for
|
|
39
|
+
every node in a cluster, so that the client moves on when one fails.
|
|
40
|
+
api_key: The client sends this as `authorization: Bearer <key>`, which a server reads in its
|
|
41
|
+
`onRequest` hook.
|
|
42
|
+
headers: The client sends these headers with every request.
|
|
43
|
+
timeout_ms: The client waits this many milliseconds for each try, and 30,000 unless you say
|
|
44
|
+
otherwise, so 0 waits for as long as the server takes. The methods that carry a corpus or a
|
|
45
|
+
whole index wait for as long as the server takes unless you set this.
|
|
46
|
+
max_response_bytes: The client stops reading an answer past this many bytes and raises
|
|
47
|
+
`CLIENT_INVALID_RESPONSE`. It reads whatever the server sends unless you set this.
|
|
48
|
+
url_skip_ms: The client skips an address for this many milliseconds after it refuses a
|
|
49
|
+
connection, 1,000 unless you say otherwise, and twice as long after each further refusal.
|
|
50
|
+
max_url_skip_ms: The client never skips an address for longer than this, 30,000 unless you say
|
|
51
|
+
otherwise.
|
|
52
|
+
http_client: The client sends through this `httpx.AsyncClient` in place of the one that it builds,
|
|
53
|
+
which is how you add a proxy or custom certificates. The client leaves a client that you pass
|
|
54
|
+
open when it closes.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
NarsilError: `CONFIG_INVALID` when an address is not an http or https URL, the list is empty, or a
|
|
58
|
+
setting is not a whole number, 0 or more.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
url: str | list[str] | tuple[str, ...],
|
|
64
|
+
*,
|
|
65
|
+
api_key: str | None = None,
|
|
66
|
+
headers: Mapping[str, str] | None = None,
|
|
67
|
+
timeout_ms: int | None = None,
|
|
68
|
+
max_response_bytes: int | None = None,
|
|
69
|
+
url_skip_ms: int | None = None,
|
|
70
|
+
max_url_skip_ms: int | None = None,
|
|
71
|
+
http_client: httpx.AsyncClient | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
settings = build_settings(url, api_key, headers, timeout_ms, max_response_bytes)
|
|
74
|
+
self._transport = AsyncTransport(settings, url_skip_ms, max_url_skip_ms, http_client)
|
|
75
|
+
self._capabilities: frozenset[str] | None = None
|
|
76
|
+
|
|
77
|
+
async def close_connections(self) -> None:
|
|
78
|
+
"""Closes the connections that the client holds open, after which the client sends nothing more."""
|
|
79
|
+
await self._transport.aclose()
|
|
80
|
+
|
|
81
|
+
async def __aenter__(self) -> AsyncNarsilClient:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def __aexit__(
|
|
85
|
+
self,
|
|
86
|
+
exc_type: type[BaseException] | None,
|
|
87
|
+
exc: BaseException | None,
|
|
88
|
+
traceback: TracebackType | None,
|
|
89
|
+
) -> None:
|
|
90
|
+
await self.close_connections()
|
|
91
|
+
|
|
92
|
+
async def get(
|
|
93
|
+
self,
|
|
94
|
+
name: str,
|
|
95
|
+
id: str,
|
|
96
|
+
*,
|
|
97
|
+
timeout_ms: int | None = None,
|
|
98
|
+
headers: Mapping[str, str] | None = None,
|
|
99
|
+
max_response_bytes: int | None = None,
|
|
100
|
+
) -> AnyDocument | None:
|
|
101
|
+
"""Returns one document, or None when the index holds no document under that id.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
name: The name of the index.
|
|
105
|
+
id: The id of the document. The client encodes it, so an id holding a slash, a space, or an
|
|
106
|
+
accent reaches the document stored under that id.
|
|
107
|
+
timeout_ms: The client waits this many milliseconds for each try of this request.
|
|
108
|
+
headers: The client adds these headers to this request.
|
|
109
|
+
max_response_bytes: The client stops reading the answer past this many bytes.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
NarsilError: Every failure other than a missing document, such as `INDEX_NOT_FOUND`.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
answer = await super().get(
|
|
116
|
+
name, id, timeout_ms=timeout_ms, headers=headers, max_response_bytes=max_response_bytes
|
|
117
|
+
)
|
|
118
|
+
except NarsilError as error:
|
|
119
|
+
if error.code == DOC_NOT_FOUND:
|
|
120
|
+
return None
|
|
121
|
+
raise
|
|
122
|
+
return answer.document
|
|
123
|
+
|
|
124
|
+
async def get_task(
|
|
125
|
+
self,
|
|
126
|
+
id: str,
|
|
127
|
+
*,
|
|
128
|
+
timeout_ms: int | None = None,
|
|
129
|
+
headers: Mapping[str, str] | None = None,
|
|
130
|
+
max_response_bytes: int | None = None,
|
|
131
|
+
) -> TaskRecord | None:
|
|
132
|
+
"""Returns one task record, or None once the server no longer holds it.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
id: The id of the task, from the record that started it.
|
|
136
|
+
timeout_ms: The client waits this many milliseconds for each try of this request.
|
|
137
|
+
headers: The client adds these headers to this request.
|
|
138
|
+
max_response_bytes: The client stops reading the answer past this many bytes.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
return await super().get_task(
|
|
142
|
+
id, timeout_ms=timeout_ms, headers=headers, max_response_bytes=max_response_bytes
|
|
143
|
+
)
|
|
144
|
+
except NarsilError as error:
|
|
145
|
+
if error.code == TASK_NOT_FOUND:
|
|
146
|
+
return None
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
async def is_alive(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
timeout_ms: int | None = None,
|
|
153
|
+
headers: Mapping[str, str] | None = None,
|
|
154
|
+
max_response_bytes: int | None = None,
|
|
155
|
+
) -> bool:
|
|
156
|
+
"""Returns True when the server process answers `GET /livez` with 200, and False for any other answer.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
NarsilError: `CLIENT_CONNECTION_FAILED` or `CLIENT_REQUEST_TIMEOUT` when no server answers.
|
|
160
|
+
"""
|
|
161
|
+
exchange = Exchange(
|
|
162
|
+
"GET", "/livez", timeout_ms=timeout_ms, headers=headers, max_response_bytes=max_response_bytes
|
|
163
|
+
)
|
|
164
|
+
return await self._transport.probe(exchange) == 200
|
|
165
|
+
|
|
166
|
+
async def is_ready(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
timeout_ms: int | None = None,
|
|
170
|
+
headers: Mapping[str, str] | None = None,
|
|
171
|
+
max_response_bytes: int | None = None,
|
|
172
|
+
) -> bool:
|
|
173
|
+
"""Returns True when the server answers `GET /readyz` with 200, and False when it answers 503.
|
|
174
|
+
|
|
175
|
+
Readiness turns false while a server starts up, and again once it starts draining.
|
|
176
|
+
"""
|
|
177
|
+
exchange = Exchange(
|
|
178
|
+
"GET", "/readyz", timeout_ms=timeout_ms, headers=headers, max_response_bytes=max_response_bytes
|
|
179
|
+
)
|
|
180
|
+
return await self._transport.probe(exchange) == 200
|
|
181
|
+
|
|
182
|
+
async def supports(self, capability: str) -> bool:
|
|
183
|
+
"""Returns True when the server announces this capability at `GET /capabilities`.
|
|
184
|
+
|
|
185
|
+
The client reads the list once and keeps it. A server that predates the endpoint answers 404, and
|
|
186
|
+
the client then reports that it supports nothing.
|
|
187
|
+
"""
|
|
188
|
+
if self._capabilities is None:
|
|
189
|
+
try:
|
|
190
|
+
announced = frozenset((await self.capabilities()).capabilities)
|
|
191
|
+
except NarsilError as error:
|
|
192
|
+
if error.code != NOT_FOUND:
|
|
193
|
+
raise
|
|
194
|
+
announced = frozenset()
|
|
195
|
+
self._capabilities = announced
|
|
196
|
+
return capability in self._capabilities
|
|
197
|
+
|
|
198
|
+
async def wait_for_task(
|
|
199
|
+
self,
|
|
200
|
+
task_id: str,
|
|
201
|
+
*,
|
|
202
|
+
poll_interval_ms: int = DEFAULT_TASK_POLL_INTERVAL_MS,
|
|
203
|
+
wait_timeout_ms: int = 0,
|
|
204
|
+
on_progress: Callable[[TaskRecord], None] | None = None,
|
|
205
|
+
timeout_ms: int | None = None,
|
|
206
|
+
headers: Mapping[str, str] | None = None,
|
|
207
|
+
) -> TaskRecord:
|
|
208
|
+
"""Polls a task until it reaches `succeeded`, `failed`, or `cancelled`, then returns that record.
|
|
209
|
+
|
|
210
|
+
A failed task comes back with its `error` set and raises nothing, because a part-finished import
|
|
211
|
+
still reports what it indexed, so read the status of the record you get back. Cancelling the
|
|
212
|
+
coroutine stops the wait, and the task keeps running on the server.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
task_id: The id of the task, from the record that started it.
|
|
216
|
+
poll_interval_ms: The client waits this many milliseconds between polls, 250 unless you say
|
|
217
|
+
otherwise.
|
|
218
|
+
wait_timeout_ms: The wait raises `CLIENT_TASK_TIMEOUT` after this long while the task keeps
|
|
219
|
+
running. It waits for as long as the task takes unless you set this.
|
|
220
|
+
on_progress: The client calls this each time the record changes, and never twice for the same
|
|
221
|
+
figures.
|
|
222
|
+
timeout_ms: The client waits this many milliseconds for each poll.
|
|
223
|
+
headers: The client adds these headers to each poll.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
NarsilError: `TASK_NOT_FOUND` when the server drops the record before the task ends, and
|
|
227
|
+
`CLIENT_TASK_TIMEOUT` when the wait passes `wait_timeout_ms`.
|
|
228
|
+
"""
|
|
229
|
+
interval, limit = poll_settings(poll_interval_ms, wait_timeout_ms)
|
|
230
|
+
started = time.monotonic()
|
|
231
|
+
reported: str | None = None
|
|
232
|
+
while True:
|
|
233
|
+
record = await self.get_task(task_id, timeout_ms=timeout_ms, headers=headers)
|
|
234
|
+
if record is None:
|
|
235
|
+
raise task_gone(task_id)
|
|
236
|
+
key = progress_key(record)
|
|
237
|
+
if key != reported:
|
|
238
|
+
reported = key
|
|
239
|
+
if on_progress is not None:
|
|
240
|
+
on_progress(record)
|
|
241
|
+
if is_terminal(record):
|
|
242
|
+
return record
|
|
243
|
+
if limit > 0 and time.monotonic() - started >= limit:
|
|
244
|
+
raise task_still_running(task_id, wait_timeout_ms, record)
|
|
245
|
+
await anyio.sleep(interval)
|
|
246
|
+
|
|
247
|
+
async def import_documents(
|
|
248
|
+
self,
|
|
249
|
+
name: str,
|
|
250
|
+
documents: Iterable[Mapping[str, Any] | BaseModel] | str | bytes,
|
|
251
|
+
*,
|
|
252
|
+
timeout_ms: int | None = None,
|
|
253
|
+
headers: Mapping[str, str] | None = None,
|
|
254
|
+
) -> ImportResult:
|
|
255
|
+
"""Loads documents in one request and returns once the whole load has finished.
|
|
256
|
+
|
|
257
|
+
The client encodes a list of documents as NDJSON, and it sends a string or bytes as they are. A
|
|
258
|
+
NumPy array in a document reaches the server as a list of numbers.
|
|
259
|
+
"""
|
|
260
|
+
answer = await self.import_ndjson(name, ndjson(documents), timeout_ms=timeout_ms, headers=headers)
|
|
261
|
+
return expect_import_result(answer, f"/indexes/{path_segment(name)}/documents/_import")
|
|
262
|
+
|
|
263
|
+
async def start_import(
|
|
264
|
+
self,
|
|
265
|
+
name: str,
|
|
266
|
+
documents: Iterable[Mapping[str, Any] | BaseModel] | str | bytes,
|
|
267
|
+
*,
|
|
268
|
+
timeout_ms: int | None = None,
|
|
269
|
+
headers: Mapping[str, str] | None = None,
|
|
270
|
+
) -> TaskRecord:
|
|
271
|
+
"""Uploads documents and returns the task that loads them, which :meth:`wait_for_task` follows."""
|
|
272
|
+
answer = await self.import_ndjson(
|
|
273
|
+
name, ndjson(documents), async_="true", timeout_ms=timeout_ms, headers=headers
|
|
274
|
+
)
|
|
275
|
+
return expect_task(answer, f"/indexes/{path_segment(name)}/documents/_import")
|
|
276
|
+
|
|
277
|
+
async def insert_batch(
|
|
278
|
+
self,
|
|
279
|
+
name: str,
|
|
280
|
+
documents: Iterable[Mapping[str, Any] | BaseModel],
|
|
281
|
+
options: BaseModel | Mapping[str, Any] | None = None,
|
|
282
|
+
*,
|
|
283
|
+
timeout_ms: int | None = None,
|
|
284
|
+
headers: Mapping[str, str] | None = None,
|
|
285
|
+
) -> BatchResponse:
|
|
286
|
+
"""Inserts many documents in one request, and returns the ids it wrote and each refusal."""
|
|
287
|
+
body = batch_body("insert", "documents", documents, options)
|
|
288
|
+
return await self.write_batch(name, body, timeout_ms=timeout_ms, headers=headers)
|
|
289
|
+
|
|
290
|
+
async def update_batch(
|
|
291
|
+
self,
|
|
292
|
+
name: str,
|
|
293
|
+
updates: Iterable[Mapping[str, Any] | BaseModel],
|
|
294
|
+
options: BaseModel | Mapping[str, Any] | None = None,
|
|
295
|
+
*,
|
|
296
|
+
timeout_ms: int | None = None,
|
|
297
|
+
headers: Mapping[str, str] | None = None,
|
|
298
|
+
) -> BatchResponse:
|
|
299
|
+
"""Replaces many documents in one request. Each update holds a `docId` and a `document`."""
|
|
300
|
+
body = batch_body("update", "updates", updates, options)
|
|
301
|
+
return await self.write_batch(name, body, timeout_ms=timeout_ms, headers=headers)
|
|
302
|
+
|
|
303
|
+
async def remove_batch(
|
|
304
|
+
self,
|
|
305
|
+
name: str,
|
|
306
|
+
doc_ids: Iterable[str],
|
|
307
|
+
options: BaseModel | Mapping[str, Any] | None = None,
|
|
308
|
+
*,
|
|
309
|
+
timeout_ms: int | None = None,
|
|
310
|
+
headers: Mapping[str, str] | None = None,
|
|
311
|
+
) -> BatchResponse:
|
|
312
|
+
"""Removes many documents in one request, and returns the ids it removed and each refusal."""
|
|
313
|
+
body = batch_body("delete", "docIds", doc_ids, options)
|
|
314
|
+
return await self.write_batch(name, body, timeout_ms=timeout_ms, headers=headers)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Generated from packages/ts/openapi.json by packages/python/generator. Do not edit."""
|
|
2
|
+
|
|
3
|
+
from narsil_client._async_operations.cluster import ClusterOperations
|
|
4
|
+
from narsil_client._async_operations.documents import DocumentsOperations
|
|
5
|
+
from narsil_client._async_operations.indexes import IndexesOperations
|
|
6
|
+
from narsil_client._async_operations.maintenance import MaintenanceOperations
|
|
7
|
+
from narsil_client._async_operations.query_suggestions import QuerySuggestionsOperations
|
|
8
|
+
from narsil_client._async_operations.search import SearchOperations
|
|
9
|
+
from narsil_client._async_operations.server import ServerOperations
|
|
10
|
+
from narsil_client._async_operations.sync import SyncOperations
|
|
11
|
+
from narsil_client._async_operations.tasks import TasksOperations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncOperations(
|
|
15
|
+
ServerOperations,
|
|
16
|
+
ClusterOperations,
|
|
17
|
+
TasksOperations,
|
|
18
|
+
IndexesOperations,
|
|
19
|
+
DocumentsOperations,
|
|
20
|
+
SearchOperations,
|
|
21
|
+
MaintenanceOperations,
|
|
22
|
+
SyncOperations,
|
|
23
|
+
QuerySuggestionsOperations,
|
|
24
|
+
):
|
|
25
|
+
"""These methods call every route of a Narsil server, one method for each route."""
|