eigenlake 0.2.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.
- eigenlake/__init__.py +46 -0
- eigenlake/client.py +532 -0
- eigenlake/errors.py +29 -0
- eigenlake/py.typed +1 -0
- eigenlake/schema.py +209 -0
- eigenlake/transport.py +104 -0
- eigenlake-0.2.0.dist-info/METADATA +98 -0
- eigenlake-0.2.0.dist-info/RECORD +9 -0
- eigenlake-0.2.0.dist-info/WHEEL +4 -0
eigenlake/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .client import EigenLakeClient
|
|
4
|
+
from . import schema
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def connect(
|
|
10
|
+
*,
|
|
11
|
+
url: str,
|
|
12
|
+
api_key: str | None = None,
|
|
13
|
+
timeout: float = 20.0,
|
|
14
|
+
retries: int = 2,
|
|
15
|
+
) -> EigenLakeClient:
|
|
16
|
+
return EigenLakeClient(
|
|
17
|
+
url=url,
|
|
18
|
+
api_key=api_key,
|
|
19
|
+
timeout=timeout,
|
|
20
|
+
retries=retries,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def connect_local(
|
|
25
|
+
*,
|
|
26
|
+
host: str = "http://localhost",
|
|
27
|
+
port: int = 8000,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
timeout: float = 20.0,
|
|
30
|
+
retries: int = 2,
|
|
31
|
+
) -> EigenLakeClient:
|
|
32
|
+
return EigenLakeClient(
|
|
33
|
+
url=f"{host.rstrip('/')}:{int(port)}",
|
|
34
|
+
api_key=api_key,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
retries=retries,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"EigenLakeClient",
|
|
42
|
+
"__version__",
|
|
43
|
+
"connect",
|
|
44
|
+
"connect_local",
|
|
45
|
+
"schema",
|
|
46
|
+
]
|
eigenlake/client.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Iterable, Iterator, List, Literal
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .transport import Transport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _q(value: str) -> str:
|
|
12
|
+
return quote(str(value), safe="")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _index_path(namespace: str, index: str) -> str:
|
|
16
|
+
return f"/v1/indexes/{_q(namespace)}/{_q(index)}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FailedRecord:
|
|
21
|
+
id: str
|
|
22
|
+
error: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AddManyResult(list[str]):
|
|
26
|
+
def __init__(self, ids: Iterable[str], *, failed_records: List[FailedRecord] | None = None):
|
|
27
|
+
super().__init__(ids)
|
|
28
|
+
self.failed_records: List[FailedRecord] = failed_records or []
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def number_errors(self) -> int:
|
|
32
|
+
return len(self.failed_records)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IndexRecords:
|
|
36
|
+
def __init__(self, handle: "IndexHandle"):
|
|
37
|
+
self._h = handle
|
|
38
|
+
|
|
39
|
+
def add(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
properties: Dict[str, Any],
|
|
43
|
+
vector: List[float],
|
|
44
|
+
id: str | None = None,
|
|
45
|
+
on_duplicate: Literal["error", "replace", "skip"] = "error",
|
|
46
|
+
batch_size: int = 500,
|
|
47
|
+
max_workers: int = 1,
|
|
48
|
+
) -> str:
|
|
49
|
+
payload = {
|
|
50
|
+
"properties": properties,
|
|
51
|
+
"vector": vector,
|
|
52
|
+
"uuid": id,
|
|
53
|
+
"on_duplicate": on_duplicate,
|
|
54
|
+
"batch_size": batch_size,
|
|
55
|
+
"max_workers": max_workers,
|
|
56
|
+
}
|
|
57
|
+
resp = self._h._t.post(f"{self._h._path}/records/insert", json=payload).json()
|
|
58
|
+
return str(resp["uuid"])
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
item = dict(record)
|
|
63
|
+
if "id" in item:
|
|
64
|
+
item["uuid"] = item.pop("id")
|
|
65
|
+
return item
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _normalize_vector_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
69
|
+
normalized = dict(item)
|
|
70
|
+
if "id" in normalized:
|
|
71
|
+
normalized["uuid"] = normalized.pop("id")
|
|
72
|
+
return normalized
|
|
73
|
+
|
|
74
|
+
def add_many(
|
|
75
|
+
self,
|
|
76
|
+
records: Iterable[dict[str, Any]],
|
|
77
|
+
*,
|
|
78
|
+
on_duplicate: Literal["error", "replace", "skip"] = "error",
|
|
79
|
+
on_error: Literal["raise", "continue"] = "raise",
|
|
80
|
+
batch_size: int = 500,
|
|
81
|
+
max_workers: int = 1,
|
|
82
|
+
) -> AddManyResult:
|
|
83
|
+
payload = {
|
|
84
|
+
"objects": [self._normalize_record(record) for record in records],
|
|
85
|
+
"on_duplicate": on_duplicate,
|
|
86
|
+
"on_error": on_error,
|
|
87
|
+
"batch_size": batch_size,
|
|
88
|
+
"max_workers": max_workers,
|
|
89
|
+
}
|
|
90
|
+
resp = self._h._t.post(f"{self._h._path}/records/insert-many", json=payload).json()
|
|
91
|
+
failed = [
|
|
92
|
+
FailedRecord(id=str(item.get("uuid") or ""), error=str(item.get("error") or ""))
|
|
93
|
+
for item in (resp.get("failed_objects") or [])
|
|
94
|
+
]
|
|
95
|
+
return AddManyResult(resp.get("uuids") or [], failed_records=failed)
|
|
96
|
+
|
|
97
|
+
def add_vectors(
|
|
98
|
+
self,
|
|
99
|
+
vectors: Iterable[dict[str, Any]],
|
|
100
|
+
*,
|
|
101
|
+
batch_size: int = 500,
|
|
102
|
+
max_workers: int = 1,
|
|
103
|
+
) -> None:
|
|
104
|
+
payload = {
|
|
105
|
+
"vectors": [self._normalize_vector_item(item) for item in vectors],
|
|
106
|
+
"batch_size": batch_size,
|
|
107
|
+
"max_workers": max_workers,
|
|
108
|
+
}
|
|
109
|
+
self._h._t.post(f"{self._h._path}/records/insert-vectors", json=payload)
|
|
110
|
+
|
|
111
|
+
def get(self, id: str, *, return_data: bool = True, return_metadata: bool = True) -> dict[str, Any] | None:
|
|
112
|
+
payload = {
|
|
113
|
+
"uuid": id,
|
|
114
|
+
"return_data": return_data,
|
|
115
|
+
"return_metadata": return_metadata,
|
|
116
|
+
}
|
|
117
|
+
resp = self._h._t.post(f"{self._h._path}/records/get-by-id", json=payload).json()
|
|
118
|
+
return resp.get("object")
|
|
119
|
+
|
|
120
|
+
def exists(self, id: str) -> bool:
|
|
121
|
+
resp = self._h._t.get(f"{self._h._path}/records/exists/{_q(id)}").json()
|
|
122
|
+
return bool(resp.get("exists", False))
|
|
123
|
+
|
|
124
|
+
def remove(self, id: str, *, batch_size: int = 500) -> None:
|
|
125
|
+
self._h._t.delete(f"{self._h._path}/records/{_q(id)}", params={"batch_size": batch_size})
|
|
126
|
+
|
|
127
|
+
def remove_many(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
filter: Dict[str, Any],
|
|
131
|
+
limit: int | None = None,
|
|
132
|
+
delete_sql_rows: bool = False,
|
|
133
|
+
on_missing: Literal["skip", "error"] = "skip",
|
|
134
|
+
batch_size: int = 500,
|
|
135
|
+
background: bool = True,
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
payload = {
|
|
138
|
+
"where": filter,
|
|
139
|
+
"limit": limit,
|
|
140
|
+
"delete_patent_rows": delete_sql_rows,
|
|
141
|
+
"on_missing_keys": on_missing,
|
|
142
|
+
"batch_size": batch_size,
|
|
143
|
+
"background": background,
|
|
144
|
+
}
|
|
145
|
+
return self._h._t.post(f"{self._h._path}/records/delete-many", json=payload).json()
|
|
146
|
+
|
|
147
|
+
def remove_job(self, job_id: int) -> Dict[str, Any]:
|
|
148
|
+
return self._h._t.get(f"{self._h._path}/records/delete-jobs/{int(job_id)}").json()
|
|
149
|
+
|
|
150
|
+
def update(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
id: str,
|
|
154
|
+
properties: Dict[str, Any] | None = None,
|
|
155
|
+
vector: List[float] | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
payload = {
|
|
158
|
+
"properties": properties,
|
|
159
|
+
"vector": vector,
|
|
160
|
+
}
|
|
161
|
+
self._h._t.patch(f"{self._h._path}/records/{_q(id)}", json=payload)
|
|
162
|
+
|
|
163
|
+
def replace(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
id: str,
|
|
167
|
+
properties: Dict[str, Any],
|
|
168
|
+
vector: List[float] | None = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
payload = {
|
|
171
|
+
"properties": properties,
|
|
172
|
+
"vector": vector,
|
|
173
|
+
}
|
|
174
|
+
self._h._t.put(f"{self._h._path}/records/{_q(id)}", json=payload)
|
|
175
|
+
|
|
176
|
+
def list(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
filter: Dict[str, Any],
|
|
180
|
+
limit: int = 100,
|
|
181
|
+
after: str | None = None,
|
|
182
|
+
with_vector: bool = False,
|
|
183
|
+
with_properties: bool = True,
|
|
184
|
+
on_missing: Literal["skip", "error"] = "skip",
|
|
185
|
+
) -> Dict[str, Any]:
|
|
186
|
+
payload = {
|
|
187
|
+
"where": filter,
|
|
188
|
+
"limit": limit,
|
|
189
|
+
"after": after,
|
|
190
|
+
"include_vector": with_vector,
|
|
191
|
+
"include_properties": with_properties,
|
|
192
|
+
"on_missing_keys": on_missing,
|
|
193
|
+
}
|
|
194
|
+
return self._h._t.post(f"{self._h._path}/records/get-by-filter", json=payload).json()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class IndexSearch:
|
|
198
|
+
def __init__(self, handle: "IndexHandle"):
|
|
199
|
+
self._h = handle
|
|
200
|
+
|
|
201
|
+
def nearest(self, *, vector: List[float], limit: int = 10, filter: Dict[str, Any] | None = None):
|
|
202
|
+
payload = {
|
|
203
|
+
"vector": vector,
|
|
204
|
+
"top_k": limit,
|
|
205
|
+
"filter": filter,
|
|
206
|
+
}
|
|
207
|
+
return self._h._t.post(f"{self._h._path}/search/near-vector", json=payload).json()
|
|
208
|
+
|
|
209
|
+
def cluster(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
filter: Dict[str, Any] | None = None,
|
|
213
|
+
limit: int = 1000,
|
|
214
|
+
algorithm: Literal["kmeans"] = "kmeans",
|
|
215
|
+
num_clusters: int | None = None,
|
|
216
|
+
distance_metric: Literal["cosine", "euclidean"] = "cosine",
|
|
217
|
+
text_fields: List[str] | None = None,
|
|
218
|
+
representatives_per_cluster: int = 3,
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
payload = {
|
|
221
|
+
"filter": filter or {},
|
|
222
|
+
"limit": limit,
|
|
223
|
+
"algorithm": algorithm,
|
|
224
|
+
"num_clusters": num_clusters,
|
|
225
|
+
"distance_metric": distance_metric,
|
|
226
|
+
"text_fields": text_fields or [],
|
|
227
|
+
"representatives_per_cluster": representatives_per_cluster,
|
|
228
|
+
}
|
|
229
|
+
return self._h._t.post(f"{self._h._path}/search/cluster", json=payload).json()
|
|
230
|
+
|
|
231
|
+
def get(self, id: str, *, with_vector: bool = False) -> Dict[str, Any]:
|
|
232
|
+
params = {"include_vector": bool(with_vector)}
|
|
233
|
+
return self._h._t.get(f"{self._h._path}/search/object/{_q(id)}", params=params).json()
|
|
234
|
+
|
|
235
|
+
def list(
|
|
236
|
+
self,
|
|
237
|
+
*,
|
|
238
|
+
limit: int = 100,
|
|
239
|
+
offset: int = 0,
|
|
240
|
+
with_vector: bool = False,
|
|
241
|
+
with_properties: bool = True,
|
|
242
|
+
newest_first: bool = True,
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
params = {
|
|
245
|
+
"limit": limit,
|
|
246
|
+
"offset": offset,
|
|
247
|
+
"include_vector": with_vector,
|
|
248
|
+
"include_properties": with_properties,
|
|
249
|
+
"newest_first": newest_first,
|
|
250
|
+
}
|
|
251
|
+
return self._h._t.get(f"{self._h._path}/search/objects", params=params).json()
|
|
252
|
+
|
|
253
|
+
def iterate(
|
|
254
|
+
self,
|
|
255
|
+
*,
|
|
256
|
+
page_size: int = 500,
|
|
257
|
+
with_vector: bool = False,
|
|
258
|
+
with_properties: bool = True,
|
|
259
|
+
newest_first: bool = True,
|
|
260
|
+
) -> Iterator[dict[str, Any]]:
|
|
261
|
+
offset = 0
|
|
262
|
+
while True:
|
|
263
|
+
page = self.list(
|
|
264
|
+
limit=page_size,
|
|
265
|
+
offset=offset,
|
|
266
|
+
with_vector=with_vector,
|
|
267
|
+
with_properties=with_properties,
|
|
268
|
+
newest_first=newest_first,
|
|
269
|
+
)
|
|
270
|
+
objects = page.get("objects") or []
|
|
271
|
+
if not objects:
|
|
272
|
+
break
|
|
273
|
+
for obj in objects:
|
|
274
|
+
yield obj
|
|
275
|
+
offset = int(page.get("next_offset") or 0)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class IndexSettings:
|
|
279
|
+
def __init__(self, handle: "IndexHandle"):
|
|
280
|
+
self._h = handle
|
|
281
|
+
|
|
282
|
+
def _read(self) -> dict[str, Any]:
|
|
283
|
+
return self._h._t.get(f"{self._h._path}/settings").json()
|
|
284
|
+
|
|
285
|
+
def dimensions(self) -> int:
|
|
286
|
+
return int(self._read().get("dims", 0))
|
|
287
|
+
|
|
288
|
+
def schema(self) -> Dict[str, Any]:
|
|
289
|
+
return dict(self._read().get("schema") or {})
|
|
290
|
+
|
|
291
|
+
def shards(self) -> Dict[str, Any]:
|
|
292
|
+
return dict(self._read().get("shards") or {})
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class IndexManage:
|
|
296
|
+
def __init__(self, handle: "IndexHandle"):
|
|
297
|
+
self._h = handle
|
|
298
|
+
|
|
299
|
+
def delete(self, *, ensure_remote: bool = True, drop_keys_table: bool = True):
|
|
300
|
+
params = {
|
|
301
|
+
"ensure_remote": ensure_remote,
|
|
302
|
+
"drop_keys_table": drop_keys_table,
|
|
303
|
+
}
|
|
304
|
+
return self._h._t.delete(self._h._path, params=params).json()
|
|
305
|
+
|
|
306
|
+
def remove_by_filter(
|
|
307
|
+
self,
|
|
308
|
+
*,
|
|
309
|
+
filter: Dict[str, Any],
|
|
310
|
+
limit_ids: int | None = None,
|
|
311
|
+
delete_sql_rows: bool = False,
|
|
312
|
+
on_missing: Literal["skip", "error"] = "skip",
|
|
313
|
+
batch_size: int = 500,
|
|
314
|
+
background: bool = True,
|
|
315
|
+
) -> Dict[str, Any]:
|
|
316
|
+
payload = {
|
|
317
|
+
"where": filter,
|
|
318
|
+
"limit_object_ids": limit_ids,
|
|
319
|
+
"delete_sql_metadata_rows": delete_sql_rows,
|
|
320
|
+
"on_missing_keys": on_missing,
|
|
321
|
+
"batch_size": batch_size,
|
|
322
|
+
"background": background,
|
|
323
|
+
}
|
|
324
|
+
return self._h._t.post(f"{self._h._path}/manage/delete-by-filter", json=payload).json()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class IndexBatch:
|
|
328
|
+
def __init__(self, handle: "IndexHandle"):
|
|
329
|
+
self._h = handle
|
|
330
|
+
self.failed_records: List[FailedRecord] = []
|
|
331
|
+
|
|
332
|
+
def with_size(
|
|
333
|
+
self,
|
|
334
|
+
*,
|
|
335
|
+
batch_size: int = 200,
|
|
336
|
+
max_workers: int = 1,
|
|
337
|
+
on_error: Literal["raise", "continue"] = "continue",
|
|
338
|
+
):
|
|
339
|
+
return _SizedBatchWriter(
|
|
340
|
+
manager=self,
|
|
341
|
+
batch_size=int(batch_size),
|
|
342
|
+
max_workers=int(max_workers),
|
|
343
|
+
on_error=on_error,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class _SizedBatchWriter:
|
|
348
|
+
def __init__(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
manager: IndexBatch,
|
|
352
|
+
batch_size: int,
|
|
353
|
+
max_workers: int,
|
|
354
|
+
on_error: Literal["raise", "continue"],
|
|
355
|
+
):
|
|
356
|
+
self._m = manager
|
|
357
|
+
self._batch_size = max(1, batch_size)
|
|
358
|
+
self._max_workers = max(1, max_workers)
|
|
359
|
+
self._on_error = on_error
|
|
360
|
+
|
|
361
|
+
self._buffer: List[dict[str, Any]] = []
|
|
362
|
+
self.failed_records: List[FailedRecord] = []
|
|
363
|
+
self.number_errors: int = 0
|
|
364
|
+
|
|
365
|
+
def __enter__(self):
|
|
366
|
+
return self
|
|
367
|
+
|
|
368
|
+
def __exit__(self, exc_type, exc, tb):
|
|
369
|
+
self.flush()
|
|
370
|
+
self._m.failed_records = list(self.failed_records)
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
def add(
|
|
374
|
+
self,
|
|
375
|
+
*,
|
|
376
|
+
properties: Dict[str, Any],
|
|
377
|
+
vector: List[float],
|
|
378
|
+
id: str | None = None,
|
|
379
|
+
) -> str:
|
|
380
|
+
out_id = str(id) if id is not None else str(uuid4())
|
|
381
|
+
self._buffer.append(
|
|
382
|
+
{
|
|
383
|
+
"properties": properties,
|
|
384
|
+
"vector": vector,
|
|
385
|
+
"id": out_id,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
if len(self._buffer) >= self._batch_size:
|
|
389
|
+
self.flush()
|
|
390
|
+
return out_id
|
|
391
|
+
|
|
392
|
+
def flush(self) -> None:
|
|
393
|
+
if not self._buffer:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
payload = list(self._buffer)
|
|
397
|
+
self._buffer = []
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
result = self._m._h.records.add_many(
|
|
401
|
+
payload,
|
|
402
|
+
on_duplicate="error",
|
|
403
|
+
on_error="continue",
|
|
404
|
+
batch_size=self._batch_size,
|
|
405
|
+
max_workers=self._max_workers,
|
|
406
|
+
)
|
|
407
|
+
self.number_errors += result.number_errors
|
|
408
|
+
self.failed_records.extend(result.failed_records)
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
self.number_errors += len(payload)
|
|
411
|
+
self.failed_records.extend(
|
|
412
|
+
[FailedRecord(id=str(item.get("id") or ""), error=str(exc)) for item in payload]
|
|
413
|
+
)
|
|
414
|
+
if self._on_error == "raise":
|
|
415
|
+
raise
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class IndexAgent:
|
|
419
|
+
def __init__(self, handle: "IndexHandle"):
|
|
420
|
+
self._h = handle
|
|
421
|
+
|
|
422
|
+
def query(
|
|
423
|
+
self,
|
|
424
|
+
query: str,
|
|
425
|
+
*,
|
|
426
|
+
filter: Dict[str, Any] | None = None,
|
|
427
|
+
limit: int = 1000,
|
|
428
|
+
mode: Literal["auto", "cluster", "filter"] = "auto",
|
|
429
|
+
num_clusters: int | None = None,
|
|
430
|
+
text_fields: List[str] | None = None,
|
|
431
|
+
time_field: str = "created_at",
|
|
432
|
+
failure_field: str = "status",
|
|
433
|
+
failure_values: List[str] | None = None,
|
|
434
|
+
recent_days: int = 7,
|
|
435
|
+
) -> Dict[str, Any]:
|
|
436
|
+
payload = {
|
|
437
|
+
"query": query,
|
|
438
|
+
"filter": filter or {},
|
|
439
|
+
"limit": limit,
|
|
440
|
+
"mode": mode,
|
|
441
|
+
"num_clusters": num_clusters,
|
|
442
|
+
"text_fields": text_fields or [],
|
|
443
|
+
"time_field": time_field,
|
|
444
|
+
"failure_field": failure_field,
|
|
445
|
+
"failure_values": failure_values or ["failure", "failed", "error"],
|
|
446
|
+
"recent_days": recent_days,
|
|
447
|
+
}
|
|
448
|
+
return self._h._t.post(f"{self._h._path}/agent/query", json=payload).json()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class IndexHandle:
|
|
452
|
+
def __init__(self, transport: Transport, namespace: str, index: str):
|
|
453
|
+
self._t = transport
|
|
454
|
+
self._path = _index_path(namespace, index)
|
|
455
|
+
|
|
456
|
+
self.records = IndexRecords(self)
|
|
457
|
+
self.search = IndexSearch(self)
|
|
458
|
+
self.settings = IndexSettings(self)
|
|
459
|
+
self.manage = IndexManage(self)
|
|
460
|
+
self.batch = IndexBatch(self)
|
|
461
|
+
self.agent = IndexAgent(self)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class IndexesNamespace:
|
|
465
|
+
def __init__(self, transport: Transport):
|
|
466
|
+
self._t = transport
|
|
467
|
+
|
|
468
|
+
def create_or_get(
|
|
469
|
+
self,
|
|
470
|
+
*,
|
|
471
|
+
namespace: str,
|
|
472
|
+
index: str,
|
|
473
|
+
dimensions: int,
|
|
474
|
+
schema: Dict[str, Any] | None = None,
|
|
475
|
+
index_options: Dict[str, Any] | None = None,
|
|
476
|
+
shard_count: int = 1,
|
|
477
|
+
record_id_property: str = "document_id",
|
|
478
|
+
) -> IndexHandle:
|
|
479
|
+
shard_count = max(1, int(shard_count))
|
|
480
|
+
payload = {
|
|
481
|
+
"namespace": namespace,
|
|
482
|
+
"index": index,
|
|
483
|
+
"dimensions": int(dimensions),
|
|
484
|
+
"schema": schema,
|
|
485
|
+
"index_options": index_options,
|
|
486
|
+
"shard_count": shard_count,
|
|
487
|
+
"record_id_property": record_id_property,
|
|
488
|
+
}
|
|
489
|
+
self._t.post("/v1/indexes/get-or-create", json=payload)
|
|
490
|
+
return IndexHandle(self._t, namespace, index)
|
|
491
|
+
|
|
492
|
+
def open(self, *, namespace: str, index: str) -> IndexHandle:
|
|
493
|
+
self._t.get(_index_path(namespace, index))
|
|
494
|
+
return IndexHandle(self._t, namespace, index)
|
|
495
|
+
|
|
496
|
+
def ref(self, *, namespace: str, index: str) -> IndexHandle:
|
|
497
|
+
return IndexHandle(self._t, namespace, index)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class EigenLakeClient:
|
|
501
|
+
def __init__(
|
|
502
|
+
self,
|
|
503
|
+
*,
|
|
504
|
+
url: str,
|
|
505
|
+
api_key: str | None = None,
|
|
506
|
+
timeout: float = 20.0,
|
|
507
|
+
retries: int = 2,
|
|
508
|
+
):
|
|
509
|
+
self._transport = Transport(
|
|
510
|
+
base_url=url,
|
|
511
|
+
api_key=api_key,
|
|
512
|
+
timeout=timeout,
|
|
513
|
+
retries=retries,
|
|
514
|
+
)
|
|
515
|
+
self.indexes = IndexesNamespace(self._transport)
|
|
516
|
+
|
|
517
|
+
def ready(self) -> bool:
|
|
518
|
+
try:
|
|
519
|
+
payload = self._transport.get("/v1/health/ready").json()
|
|
520
|
+
return bool(payload.get("ready"))
|
|
521
|
+
except Exception:
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
def close(self) -> None:
|
|
525
|
+
self._transport.close()
|
|
526
|
+
|
|
527
|
+
def __enter__(self) -> "EigenLakeClient":
|
|
528
|
+
return self
|
|
529
|
+
|
|
530
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
531
|
+
self.close()
|
|
532
|
+
return False
|
eigenlake/errors.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EigenlakeError(Exception):
|
|
5
|
+
"""Base error for the Eigenlake SDK."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(EigenlakeError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotFoundError(EigenlakeError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConflictError(EigenlakeError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ValidationError(EigenlakeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIError(EigenlakeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NetworkError(EigenlakeError):
|
|
29
|
+
pass
|
eigenlake/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
eigenlake/schema.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Field:
|
|
9
|
+
field_type: str
|
|
10
|
+
required: bool = False
|
|
11
|
+
filterable: bool = True
|
|
12
|
+
format: str | None = None
|
|
13
|
+
description: str | None = None
|
|
14
|
+
enum: list[Any] | None = None
|
|
15
|
+
minimum: int | float | None = None
|
|
16
|
+
maximum: int | float | None = None
|
|
17
|
+
min_length: int | None = None
|
|
18
|
+
max_length: int | None = None
|
|
19
|
+
pattern: str | None = None
|
|
20
|
+
items: dict[str, Any] | None = None
|
|
21
|
+
min_items: int | None = None
|
|
22
|
+
max_items: int | None = None
|
|
23
|
+
unique_items: bool = False
|
|
24
|
+
|
|
25
|
+
def to_json_schema(self) -> dict[str, Any]:
|
|
26
|
+
out: dict[str, Any] = {"type": self.field_type}
|
|
27
|
+
if self.format is not None:
|
|
28
|
+
out["format"] = self.format
|
|
29
|
+
if self.description is not None:
|
|
30
|
+
out["description"] = self.description
|
|
31
|
+
if self.enum is not None:
|
|
32
|
+
out["enum"] = list(self.enum)
|
|
33
|
+
if self.minimum is not None:
|
|
34
|
+
out["minimum"] = self.minimum
|
|
35
|
+
if self.maximum is not None:
|
|
36
|
+
out["maximum"] = self.maximum
|
|
37
|
+
if self.min_length is not None:
|
|
38
|
+
out["minLength"] = self.min_length
|
|
39
|
+
if self.max_length is not None:
|
|
40
|
+
out["maxLength"] = self.max_length
|
|
41
|
+
if self.pattern is not None:
|
|
42
|
+
out["pattern"] = self.pattern
|
|
43
|
+
if self.items is not None:
|
|
44
|
+
out["items"] = dict(self.items)
|
|
45
|
+
if self.min_items is not None:
|
|
46
|
+
out["minItems"] = self.min_items
|
|
47
|
+
if self.max_items is not None:
|
|
48
|
+
out["maxItems"] = self.max_items
|
|
49
|
+
if self.unique_items:
|
|
50
|
+
out["uniqueItems"] = True
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def string(
|
|
55
|
+
*,
|
|
56
|
+
required: bool = False,
|
|
57
|
+
filterable: bool = True,
|
|
58
|
+
format: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
enum: list[str] | None = None,
|
|
61
|
+
min_length: int | None = None,
|
|
62
|
+
max_length: int | None = None,
|
|
63
|
+
pattern: str | None = None,
|
|
64
|
+
) -> Field:
|
|
65
|
+
return Field(
|
|
66
|
+
field_type="string",
|
|
67
|
+
required=required,
|
|
68
|
+
filterable=filterable,
|
|
69
|
+
format=format,
|
|
70
|
+
description=description,
|
|
71
|
+
enum=list(enum) if enum is not None else None,
|
|
72
|
+
min_length=min_length,
|
|
73
|
+
max_length=max_length,
|
|
74
|
+
pattern=pattern,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def integer(
|
|
79
|
+
*,
|
|
80
|
+
required: bool = False,
|
|
81
|
+
filterable: bool = True,
|
|
82
|
+
description: str | None = None,
|
|
83
|
+
minimum: int | None = None,
|
|
84
|
+
maximum: int | None = None,
|
|
85
|
+
) -> Field:
|
|
86
|
+
return Field(
|
|
87
|
+
field_type="integer",
|
|
88
|
+
required=required,
|
|
89
|
+
filterable=filterable,
|
|
90
|
+
description=description,
|
|
91
|
+
minimum=minimum,
|
|
92
|
+
maximum=maximum,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def number(
|
|
97
|
+
*,
|
|
98
|
+
required: bool = False,
|
|
99
|
+
filterable: bool = True,
|
|
100
|
+
description: str | None = None,
|
|
101
|
+
minimum: int | float | None = None,
|
|
102
|
+
maximum: int | float | None = None,
|
|
103
|
+
) -> Field:
|
|
104
|
+
return Field(
|
|
105
|
+
field_type="number",
|
|
106
|
+
required=required,
|
|
107
|
+
filterable=filterable,
|
|
108
|
+
description=description,
|
|
109
|
+
minimum=minimum,
|
|
110
|
+
maximum=maximum,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def boolean(
|
|
115
|
+
*,
|
|
116
|
+
required: bool = False,
|
|
117
|
+
filterable: bool = True,
|
|
118
|
+
description: str | None = None,
|
|
119
|
+
) -> Field:
|
|
120
|
+
return Field(
|
|
121
|
+
field_type="boolean",
|
|
122
|
+
required=required,
|
|
123
|
+
filterable=filterable,
|
|
124
|
+
description=description,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def array(
|
|
129
|
+
item: Field | dict[str, Any],
|
|
130
|
+
*,
|
|
131
|
+
required: bool = False,
|
|
132
|
+
filterable: bool = True,
|
|
133
|
+
description: str | None = None,
|
|
134
|
+
min_items: int | None = None,
|
|
135
|
+
max_items: int | None = None,
|
|
136
|
+
unique_items: bool = False,
|
|
137
|
+
) -> Field:
|
|
138
|
+
if isinstance(item, Field):
|
|
139
|
+
item_schema = item.to_json_schema()
|
|
140
|
+
else:
|
|
141
|
+
item_schema = dict(item)
|
|
142
|
+
return Field(
|
|
143
|
+
field_type="array",
|
|
144
|
+
required=required,
|
|
145
|
+
filterable=filterable,
|
|
146
|
+
description=description,
|
|
147
|
+
items=item_schema,
|
|
148
|
+
min_items=min_items,
|
|
149
|
+
max_items=max_items,
|
|
150
|
+
unique_items=unique_items,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def datetime(
|
|
155
|
+
*,
|
|
156
|
+
required: bool = False,
|
|
157
|
+
filterable: bool = True,
|
|
158
|
+
description: str | None = None,
|
|
159
|
+
) -> Field:
|
|
160
|
+
return string(
|
|
161
|
+
required=required,
|
|
162
|
+
filterable=filterable,
|
|
163
|
+
format="date-time",
|
|
164
|
+
description=description,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def date(
|
|
169
|
+
*,
|
|
170
|
+
required: bool = False,
|
|
171
|
+
filterable: bool = True,
|
|
172
|
+
description: str | None = None,
|
|
173
|
+
) -> Field:
|
|
174
|
+
return string(
|
|
175
|
+
required=required,
|
|
176
|
+
filterable=filterable,
|
|
177
|
+
format="date",
|
|
178
|
+
description=description,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class SchemaBuilder:
|
|
183
|
+
def __init__(self, *, additional_properties: bool = False):
|
|
184
|
+
self._additional_properties = bool(additional_properties)
|
|
185
|
+
self._fields: dict[str, Field] = {}
|
|
186
|
+
|
|
187
|
+
def add(self, name: str, field: Field) -> "SchemaBuilder":
|
|
188
|
+
self._fields[str(name)] = field
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def build(self) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
192
|
+
properties = {name: field.to_json_schema() for name, field in self._fields.items()}
|
|
193
|
+
required = [name for name, field in self._fields.items() if field.required]
|
|
194
|
+
non_filterable = [name for name, field in self._fields.items() if not field.filterable]
|
|
195
|
+
|
|
196
|
+
schema: dict[str, Any] = {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"additionalProperties": self._additional_properties,
|
|
199
|
+
"properties": properties,
|
|
200
|
+
}
|
|
201
|
+
if required:
|
|
202
|
+
schema["required"] = required
|
|
203
|
+
|
|
204
|
+
index_options: dict[str, Any] = {
|
|
205
|
+
"metadataConfiguration": {
|
|
206
|
+
"nonFilterableMetadataKeys": non_filterable,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return schema, index_options
|
eigenlake/transport.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .errors import APIError, AuthenticationError, ConflictError, NetworkError, NotFoundError, ValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Transport:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
base_url: str,
|
|
16
|
+
api_key: str | None,
|
|
17
|
+
timeout: float = 20.0,
|
|
18
|
+
retries: int = 2,
|
|
19
|
+
):
|
|
20
|
+
normalized = base_url.rstrip("/")
|
|
21
|
+
self._retries = max(0, int(retries))
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=normalized,
|
|
24
|
+
timeout=float(timeout),
|
|
25
|
+
headers=self._auth_headers(api_key),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _auth_headers(api_key: str | None) -> dict[str, str]:
|
|
30
|
+
token = (api_key or "").strip()
|
|
31
|
+
if not token:
|
|
32
|
+
return {}
|
|
33
|
+
return {"X-API-Key": token}
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _detail(resp: httpx.Response) -> str:
|
|
37
|
+
try:
|
|
38
|
+
payload = resp.json()
|
|
39
|
+
if isinstance(payload, dict) and payload.get("detail") is not None:
|
|
40
|
+
return str(payload["detail"])
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
text = (resp.text or "").strip()
|
|
44
|
+
return text or f"HTTP {resp.status_code}"
|
|
45
|
+
|
|
46
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
47
|
+
if 200 <= resp.status_code < 300:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
detail = self._detail(resp)
|
|
51
|
+
code = int(resp.status_code)
|
|
52
|
+
|
|
53
|
+
if code in (401, 403):
|
|
54
|
+
raise AuthenticationError(detail)
|
|
55
|
+
if code == 404:
|
|
56
|
+
raise NotFoundError(detail)
|
|
57
|
+
if code == 409:
|
|
58
|
+
raise ConflictError(detail)
|
|
59
|
+
if code in (400, 422):
|
|
60
|
+
raise ValidationError(detail)
|
|
61
|
+
raise APIError(detail)
|
|
62
|
+
|
|
63
|
+
def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
64
|
+
path = path if path.startswith("/") else f"/{path}"
|
|
65
|
+
|
|
66
|
+
last_exc: Exception | None = None
|
|
67
|
+
for attempt in range(self._retries + 1):
|
|
68
|
+
try:
|
|
69
|
+
resp = self._client.request(method.upper(), path, **kwargs)
|
|
70
|
+
except httpx.RequestError as exc:
|
|
71
|
+
last_exc = exc
|
|
72
|
+
if attempt >= self._retries:
|
|
73
|
+
raise NetworkError(str(exc)) from exc
|
|
74
|
+
time.sleep(0.2 * (attempt + 1))
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if resp.status_code >= 500 and attempt < self._retries:
|
|
78
|
+
time.sleep(0.2 * (attempt + 1))
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
self._raise_for_status(resp)
|
|
82
|
+
return resp
|
|
83
|
+
|
|
84
|
+
if last_exc is not None:
|
|
85
|
+
raise NetworkError(str(last_exc)) from last_exc
|
|
86
|
+
raise NetworkError("Request failed")
|
|
87
|
+
|
|
88
|
+
def get(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
89
|
+
return self.request("GET", path, **kwargs)
|
|
90
|
+
|
|
91
|
+
def post(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
92
|
+
return self.request("POST", path, **kwargs)
|
|
93
|
+
|
|
94
|
+
def delete(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
95
|
+
return self.request("DELETE", path, **kwargs)
|
|
96
|
+
|
|
97
|
+
def patch(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
98
|
+
return self.request("PATCH", path, **kwargs)
|
|
99
|
+
|
|
100
|
+
def put(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
101
|
+
return self.request("PUT", path, **kwargs)
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
self._client.close()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eigenlake
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for EigenLake Cloud
|
|
5
|
+
Project-URL: Homepage, https://eigenlake.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.eigenlake.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/EigenLake-Org/eigenlake-client
|
|
8
|
+
Project-URL: Issues, https://github.com/EigenLake-Org/eigenlake-client/issues
|
|
9
|
+
Author: EigenLake
|
|
10
|
+
Keywords: agent,clustering,eigenlake,embeddings,vector-search
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocstrings[python]>=0.25.0; extra == 'docs'
|
|
26
|
+
Requires-Dist: pymdown-extensions>=10.0.0; extra == 'docs'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# EigenLake Python Client
|
|
30
|
+
|
|
31
|
+
Python SDK for EigenLake Cloud.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install eigenlake
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import eigenlake
|
|
43
|
+
from eigenlake import schema as s
|
|
44
|
+
|
|
45
|
+
with eigenlake.connect(
|
|
46
|
+
url="https://api.eigenlake.dev",
|
|
47
|
+
api_key="<sk_sbx_your_api_key_here>",
|
|
48
|
+
) as client:
|
|
49
|
+
schema, index_options = (
|
|
50
|
+
s.SchemaBuilder(additional_properties=False)
|
|
51
|
+
.add("document_id", s.string(required=True, filterable=True))
|
|
52
|
+
.add("text", s.string(filterable=False))
|
|
53
|
+
.add("created_at", s.datetime(filterable=True))
|
|
54
|
+
.build()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
idx = client.indexes.create_or_get(
|
|
58
|
+
namespace="demo-namespace",
|
|
59
|
+
index="demo-index",
|
|
60
|
+
dimensions=128,
|
|
61
|
+
schema=schema,
|
|
62
|
+
index_options=index_options,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
record_id = idx.records.add(
|
|
66
|
+
properties={"document_id": "doc-1", "text": "hello"},
|
|
67
|
+
vector=[0.1] * 128,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = idx.search.nearest(
|
|
71
|
+
vector=[0.1] * 128,
|
|
72
|
+
limit=3,
|
|
73
|
+
)
|
|
74
|
+
print(record_id, result)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Agent Query
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import eigenlake
|
|
81
|
+
|
|
82
|
+
with eigenlake.connect(url="https://api.eigenlake.dev", api_key="<sk_sbx_your_api_key_here>") as client:
|
|
83
|
+
idx = client.indexes.open(namespace="demo-automotive", index="automotive-fault-clustering")
|
|
84
|
+
result = idx.agent.query("show me recent battery failures")
|
|
85
|
+
print(result["filter"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Docs
|
|
89
|
+
|
|
90
|
+
- Documentation source: `docs/`
|
|
91
|
+
- Docs site config: `mkdocs.yml`
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install -e ".[docs]"
|
|
95
|
+
mkdocs serve
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Docs will be available at `http://127.0.0.1:8000`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
eigenlake/__init__.py,sha256=jWzMPa7Pr0NoMfktENY7D1NSzkOqgj95OpDufdKFh1g,834
|
|
2
|
+
eigenlake/client.py,sha256=fEfDuUedWZUlWBZc7bjCGPBs7I5Ic7QEZW6qJEGuF-k,16316
|
|
3
|
+
eigenlake/errors.py,sha256=4edjrFl99gGVij9iO3bTF015GlyUfv6HraLzVRH3D-s,404
|
|
4
|
+
eigenlake/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
eigenlake/schema.py,sha256=TyH--q4YbNHaSkmIgEglayt04-9XfkUDATn52plTe0k,5652
|
|
6
|
+
eigenlake/transport.py,sha256=f2pxpsaCjXA8hfKCS7PeWAsmf0vRUz74TJmCoFatQAw,3281
|
|
7
|
+
eigenlake-0.2.0.dist-info/METADATA,sha256=VnIb1oq56Arz-YPaVaS9EZZs4A3CHynWn38I406wbks,2757
|
|
8
|
+
eigenlake-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
eigenlake-0.2.0.dist-info/RECORD,,
|