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.
Files changed (40) hide show
  1. narsil_client/__init__.py +36 -0
  2. narsil_client/_async_client.py +314 -0
  3. narsil_client/_async_operations/__init__.py +25 -0
  4. narsil_client/_async_operations/cluster.py +357 -0
  5. narsil_client/_async_operations/documents.py +496 -0
  6. narsil_client/_async_operations/indexes.py +358 -0
  7. narsil_client/_async_operations/maintenance.py +628 -0
  8. narsil_client/_async_operations/query_suggestions.py +388 -0
  9. narsil_client/_async_operations/search.py +142 -0
  10. narsil_client/_async_operations/server.py +232 -0
  11. narsil_client/_async_operations/sync.py +134 -0
  12. narsil_client/_async_operations/tasks.py +140 -0
  13. narsil_client/_async_transport.py +168 -0
  14. narsil_client/_client.py +317 -0
  15. narsil_client/_errors.py +95 -0
  16. narsil_client/_exchange.py +123 -0
  17. narsil_client/_failover.py +116 -0
  18. narsil_client/_failures.py +93 -0
  19. narsil_client/_helpers.py +76 -0
  20. narsil_client/_model.py +33 -0
  21. narsil_client/_operations/__init__.py +25 -0
  22. narsil_client/_operations/cluster.py +357 -0
  23. narsil_client/_operations/documents.py +496 -0
  24. narsil_client/_operations/indexes.py +358 -0
  25. narsil_client/_operations/maintenance.py +628 -0
  26. narsil_client/_operations/query_suggestions.py +388 -0
  27. narsil_client/_operations/search.py +142 -0
  28. narsil_client/_operations/server.py +232 -0
  29. narsil_client/_operations/sync.py +134 -0
  30. narsil_client/_operations/tasks.py +140 -0
  31. narsil_client/_streams.py +104 -0
  32. narsil_client/_sync_transport.py +144 -0
  33. narsil_client/_transport_core.py +177 -0
  34. narsil_client/_watchdog.py +142 -0
  35. narsil_client/models/__init__.py +2576 -0
  36. narsil_client/py.typed +0 -0
  37. narsil_client-0.1.0.dist-info/METADATA +55 -0
  38. narsil_client-0.1.0.dist-info/RECORD +40 -0
  39. narsil_client-0.1.0.dist-info/WHEEL +4 -0
  40. 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."""