nucliadb-sdk 0.0.0.post2__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.
- nucliadb_sdk/__init__.py +24 -0
- nucliadb_sdk/client.py +420 -0
- nucliadb_sdk/entities.py +33 -0
- nucliadb_sdk/file.py +27 -0
- nucliadb_sdk/knowledgebox.py +368 -0
- nucliadb_sdk/labels.py +43 -0
- nucliadb_sdk/py.typed +0 -0
- nucliadb_sdk/resource.py +369 -0
- nucliadb_sdk/search.py +104 -0
- nucliadb_sdk/tests/__init__.py +18 -0
- nucliadb_sdk/tests/conftest.py +22 -0
- nucliadb_sdk/tests/fixtures.py +93 -0
- nucliadb_sdk/tests/test_create.py +29 -0
- nucliadb_sdk/tests/test_dict_resource.py +51 -0
- nucliadb_sdk/tests/test_search_resource.py +346 -0
- nucliadb_sdk/tests/test_set_resource.py +126 -0
- nucliadb_sdk/utils.py +133 -0
- nucliadb_sdk/vectors.py +51 -0
- nucliadb_sdk-0.0.0.post2.dist-info/METADATA +25 -0
- nucliadb_sdk-0.0.0.post2.dist-info/RECORD +23 -0
- nucliadb_sdk-0.0.0.post2.dist-info/WHEEL +5 -0
- nucliadb_sdk-0.0.0.post2.dist-info/top_level.txt +1 -0
- nucliadb_sdk-0.0.0.post2.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Copyright (C) 2021 Bosutech XXI S.L.
|
|
2
|
+
#
|
|
3
|
+
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
|
4
|
+
# For commercial licensing, contact us at info@nuclia.com.
|
|
5
|
+
#
|
|
6
|
+
# AGPL:
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Affero General Public License as
|
|
9
|
+
# published by the Free Software Foundation, either version 3 of the
|
|
10
|
+
# License, or (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Affero General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any, AsyncIterable, Dict, Iterable, List, Optional, Union
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from nucliadb_models.labels import KnowledgeBoxLabels
|
|
26
|
+
from nucliadb_models.labels import Label as NDBLabel
|
|
27
|
+
from nucliadb_models.resource import Resource
|
|
28
|
+
from nucliadb_models.search import (
|
|
29
|
+
KnowledgeboxSearchResults,
|
|
30
|
+
SearchOptions,
|
|
31
|
+
SearchRequest,
|
|
32
|
+
)
|
|
33
|
+
from nucliadb_models.vectors import VectorSet, VectorSets
|
|
34
|
+
from nucliadb_sdk import DEFAULT_LABELSET
|
|
35
|
+
from nucliadb_sdk.client import NucliaDBClient
|
|
36
|
+
from nucliadb_sdk.entities import Entities
|
|
37
|
+
from nucliadb_sdk.file import File
|
|
38
|
+
from nucliadb_sdk.labels import Label, Labels, LabelSet, LabelType
|
|
39
|
+
from nucliadb_sdk.resource import (
|
|
40
|
+
create_resource,
|
|
41
|
+
from_resource_to_payload,
|
|
42
|
+
update_resource,
|
|
43
|
+
)
|
|
44
|
+
from nucliadb_sdk.search import SearchResult
|
|
45
|
+
from nucliadb_sdk.vectors import Vectors, convert_vector
|
|
46
|
+
|
|
47
|
+
NUCLIA_CLOUD = os.environ.get("NUCLIA_CLOUD_URL", ".nuclia.cloud")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class KnowledgeBox:
|
|
51
|
+
vectorsets: Optional[VectorSets] = None
|
|
52
|
+
id: str
|
|
53
|
+
|
|
54
|
+
def __init__(self, client: NucliaDBClient):
|
|
55
|
+
self.client = client
|
|
56
|
+
self.id = self.client.reader_session.base_url.path.strip("/").split("/")[-1]
|
|
57
|
+
|
|
58
|
+
def __iter__(self) -> Iterable[Resource]:
|
|
59
|
+
for batch_resources in self.client.list_resources():
|
|
60
|
+
for resource in batch_resources:
|
|
61
|
+
yield resource
|
|
62
|
+
|
|
63
|
+
async def __aiter__(self) -> AsyncIterable[Resource]:
|
|
64
|
+
async for batch_resources in self.client.async_list_resources():
|
|
65
|
+
for resource in batch_resources:
|
|
66
|
+
yield resource
|
|
67
|
+
|
|
68
|
+
def __len__(self):
|
|
69
|
+
return self.client.length().resources
|
|
70
|
+
|
|
71
|
+
async def async_len(self) -> int:
|
|
72
|
+
return (await self.client.async_length()).resources
|
|
73
|
+
|
|
74
|
+
def __getitem__(self, key: str) -> Resource:
|
|
75
|
+
return self.client.get_resource(id=key)
|
|
76
|
+
|
|
77
|
+
def download(self, uri: str) -> bytes:
|
|
78
|
+
return self.client.download(uri=uri)
|
|
79
|
+
|
|
80
|
+
def get(self, key: str, default: Optional[Resource] = None) -> Resource:
|
|
81
|
+
try:
|
|
82
|
+
result = self.client.get_resource(id=key)
|
|
83
|
+
except KeyError:
|
|
84
|
+
result = default
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
async def async_get(self, key: str, default: Optional[Resource] = None) -> Resource:
|
|
88
|
+
try:
|
|
89
|
+
result = await self.client.async_get_resource(id=key)
|
|
90
|
+
except KeyError:
|
|
91
|
+
result = default
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
def __setitem__(self, key: str, item: Resource):
|
|
95
|
+
resource = self.get(key)
|
|
96
|
+
if resource is None:
|
|
97
|
+
item.slug = key
|
|
98
|
+
self.client.create_resource(
|
|
99
|
+
from_resource_to_payload(item, download=self.download)
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
self.client.update_resource(
|
|
103
|
+
resource.id,
|
|
104
|
+
from_resource_to_payload(item, download=self.download, update=True),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def __delitem__(self, key):
|
|
108
|
+
self.client.del_resource(id=key)
|
|
109
|
+
|
|
110
|
+
async def async_del(self, key):
|
|
111
|
+
await self.client.async_del_resource(id=key)
|
|
112
|
+
|
|
113
|
+
async def async_new_vectorset(self, key: str, dimension: int):
|
|
114
|
+
await self.client.async_set_vectorset(key, VectorSet(dimension=dimension))
|
|
115
|
+
|
|
116
|
+
def new_vectorset(self, key: str, dimension: int):
|
|
117
|
+
self.client.set_vectorset(key, VectorSet(dimension=dimension))
|
|
118
|
+
|
|
119
|
+
async def async_list_vectorset(self) -> VectorSets:
|
|
120
|
+
return self.client.async_get_vectorsets()
|
|
121
|
+
|
|
122
|
+
def list_vectorset(self) -> VectorSets:
|
|
123
|
+
return self.client.get_vectorsets()
|
|
124
|
+
|
|
125
|
+
async def async_del_vectorset(self, key: str):
|
|
126
|
+
await self.client.async_del_vectorset(key)
|
|
127
|
+
|
|
128
|
+
def del_vectorset(self, key: str):
|
|
129
|
+
self.client.del_vectorset(key)
|
|
130
|
+
|
|
131
|
+
async def async_upload(
|
|
132
|
+
self,
|
|
133
|
+
key: Optional[str] = None,
|
|
134
|
+
binary: Optional[File] = None,
|
|
135
|
+
text: Optional[str] = None,
|
|
136
|
+
labels: Optional[Labels] = None,
|
|
137
|
+
entities: Optional[Entities] = None,
|
|
138
|
+
vectors: Optional[Vectors] = None,
|
|
139
|
+
):
|
|
140
|
+
resource: Optional[Resource] = None
|
|
141
|
+
|
|
142
|
+
if key is None:
|
|
143
|
+
creating = True
|
|
144
|
+
else:
|
|
145
|
+
resource = await self.async_get(key)
|
|
146
|
+
if resource is None:
|
|
147
|
+
creating = True
|
|
148
|
+
else:
|
|
149
|
+
creating = False
|
|
150
|
+
|
|
151
|
+
if vectors is not None and self.vectorsets is None:
|
|
152
|
+
self.vectorsets = await self.client.async_get_vectorsets()
|
|
153
|
+
|
|
154
|
+
if creating:
|
|
155
|
+
create_payload = create_resource(
|
|
156
|
+
key=key,
|
|
157
|
+
text=text,
|
|
158
|
+
binary=binary,
|
|
159
|
+
labels=labels,
|
|
160
|
+
entities=entities,
|
|
161
|
+
vectors=vectors,
|
|
162
|
+
vectorsets=self.vectorsets,
|
|
163
|
+
)
|
|
164
|
+
resp = await self.client.async_create_resource(create_payload)
|
|
165
|
+
rid = resp.uuid
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
assert resource is not None
|
|
169
|
+
|
|
170
|
+
update_payload = update_resource(
|
|
171
|
+
text=text,
|
|
172
|
+
binary=binary,
|
|
173
|
+
labels=labels,
|
|
174
|
+
entities=entities,
|
|
175
|
+
vectors=vectors,
|
|
176
|
+
resource=resource,
|
|
177
|
+
vectorsets=self.vectorsets,
|
|
178
|
+
)
|
|
179
|
+
rid = resource.id
|
|
180
|
+
await self.client.async_update_resource(rid, update_payload)
|
|
181
|
+
return rid
|
|
182
|
+
|
|
183
|
+
def upload(
|
|
184
|
+
self,
|
|
185
|
+
key: Optional[str] = None,
|
|
186
|
+
binary: Optional[Union[File, str]] = None,
|
|
187
|
+
text: Optional[str] = None,
|
|
188
|
+
labels: Optional[Labels] = None,
|
|
189
|
+
entities: Optional[Entities] = None,
|
|
190
|
+
vectors: Optional[
|
|
191
|
+
Union[Vectors, Dict[str, Union[np.ndarray, List[float]]]]
|
|
192
|
+
] = None,
|
|
193
|
+
) -> str:
|
|
194
|
+
resource: Optional[Resource] = None
|
|
195
|
+
|
|
196
|
+
if key is None:
|
|
197
|
+
creating = True
|
|
198
|
+
else:
|
|
199
|
+
resource = self.get(key)
|
|
200
|
+
if resource is None:
|
|
201
|
+
creating = True
|
|
202
|
+
else:
|
|
203
|
+
creating = False
|
|
204
|
+
|
|
205
|
+
if vectors is not None and self.vectorsets is None:
|
|
206
|
+
self.vectorsets = self.client.get_vectorsets()
|
|
207
|
+
|
|
208
|
+
if creating:
|
|
209
|
+
create_payload = create_resource(
|
|
210
|
+
key=key,
|
|
211
|
+
text=text,
|
|
212
|
+
binary=binary,
|
|
213
|
+
labels=labels,
|
|
214
|
+
entities=entities,
|
|
215
|
+
vectors=vectors,
|
|
216
|
+
vectorsets=self.vectorsets,
|
|
217
|
+
)
|
|
218
|
+
resp = self.client.create_resource(create_payload)
|
|
219
|
+
rid = resp.uuid
|
|
220
|
+
|
|
221
|
+
else:
|
|
222
|
+
assert resource is not None
|
|
223
|
+
update_payload = update_resource(
|
|
224
|
+
text=text,
|
|
225
|
+
binary=binary,
|
|
226
|
+
labels=labels,
|
|
227
|
+
entities=entities,
|
|
228
|
+
vectors=vectors,
|
|
229
|
+
resource=resource,
|
|
230
|
+
vectorsets=self.vectorsets,
|
|
231
|
+
)
|
|
232
|
+
rid = resource.id
|
|
233
|
+
self.client.update_resource(rid, update_payload)
|
|
234
|
+
return rid
|
|
235
|
+
|
|
236
|
+
def process_uploaded_labels_from_search(
|
|
237
|
+
self, search_result: KnowledgeboxSearchResults
|
|
238
|
+
) -> Dict[str, LabelSet]:
|
|
239
|
+
response: Dict[str, LabelSet] = {}
|
|
240
|
+
if search_result.fulltext is None or search_result.fulltext.facets is None:
|
|
241
|
+
return response
|
|
242
|
+
for labelset, count in search_result.fulltext.facets.get("/l", {}).items():
|
|
243
|
+
real_labelset = labelset[3:] # removing /l/
|
|
244
|
+
response[real_labelset] = LabelSet(count=count)
|
|
245
|
+
|
|
246
|
+
for labelset, labelset_obj in response.items():
|
|
247
|
+
base_label = f"/l/{labelset}"
|
|
248
|
+
search_result = self.client.search(
|
|
249
|
+
SearchRequest(features=["document"], faceted=[base_label], page_size=0)
|
|
250
|
+
)
|
|
251
|
+
if search_result.fulltext is None or search_result.fulltext.facets is None:
|
|
252
|
+
raise Exception("Search error")
|
|
253
|
+
for label, count in search_result.fulltext.facets.get(
|
|
254
|
+
base_label, {}
|
|
255
|
+
).items():
|
|
256
|
+
labelset_obj.labels[label.replace(base_label + "/", "")] = count
|
|
257
|
+
return response
|
|
258
|
+
|
|
259
|
+
def get_uploaded_labels(self) -> Dict[str, LabelSet]:
|
|
260
|
+
# Search for fulltext with faceted for labelsets
|
|
261
|
+
search_result = self.client.search(
|
|
262
|
+
SearchRequest(features=["document"], faceted=["/l"], page_size=0)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return self.process_uploaded_labels_from_search(search_result)
|
|
266
|
+
|
|
267
|
+
async def async_get_uploaded_labels(self) -> Dict[str, LabelSet]:
|
|
268
|
+
# Search for fulltext with faceted for labelsets
|
|
269
|
+
search_result = await self.client.async_search(
|
|
270
|
+
SearchRequest(features=["document"], faceted=["/l"], page_size=0)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return self.process_uploaded_labels_from_search(search_result)
|
|
274
|
+
|
|
275
|
+
def set_labels(self, labelset: str, labels: List[str], labelset_type: LabelType):
|
|
276
|
+
resp = self.client.writer_session.post(
|
|
277
|
+
f"/labelset/{labelset}",
|
|
278
|
+
json={
|
|
279
|
+
"title": labelset,
|
|
280
|
+
"labels": [NDBLabel(title=label).dict() for label in labels],
|
|
281
|
+
"kind": [labelset_type.value],
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
assert resp.status_code == 200
|
|
285
|
+
|
|
286
|
+
def get_labels(self) -> KnowledgeBoxLabels:
|
|
287
|
+
resp = self.client.get_labels()
|
|
288
|
+
return resp
|
|
289
|
+
|
|
290
|
+
def set_entities(self, entity_group: str, entities: List[str]):
|
|
291
|
+
resp = self.client.writer_session.post(
|
|
292
|
+
f"/entitiesgroup/{entity_group}",
|
|
293
|
+
json={
|
|
294
|
+
"title": entity_group,
|
|
295
|
+
"entities": {entity: {"value": entity} for entity in entities},
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
assert resp.status_code == 200
|
|
299
|
+
|
|
300
|
+
def search(
|
|
301
|
+
self,
|
|
302
|
+
text: Optional[str] = None,
|
|
303
|
+
filter: Optional[List[Union[Label, str]]] = None,
|
|
304
|
+
vector: Optional[Union[np.ndarray, List[float]]] = None,
|
|
305
|
+
vectorset: Optional[str] = None,
|
|
306
|
+
min_score: Optional[float] = 0.0,
|
|
307
|
+
):
|
|
308
|
+
result = self.client.search(
|
|
309
|
+
self.build_search_request(text, filter, vector, vectorset, min_score)
|
|
310
|
+
)
|
|
311
|
+
return SearchResult(result, self.client)
|
|
312
|
+
|
|
313
|
+
async def async_search(
|
|
314
|
+
self,
|
|
315
|
+
text: Optional[str] = None,
|
|
316
|
+
filter: Optional[List[Union[Label, str]]] = None,
|
|
317
|
+
vector: Optional[Union[np.ndarray, List[float]]] = None,
|
|
318
|
+
vectorset: Optional[str] = None,
|
|
319
|
+
min_score: Optional[float] = 0.0,
|
|
320
|
+
):
|
|
321
|
+
result = await self.client.async_search(
|
|
322
|
+
self.build_search_request(text, filter, vector, vectorset, min_score)
|
|
323
|
+
)
|
|
324
|
+
return SearchResult(result, self.client)
|
|
325
|
+
|
|
326
|
+
def build_search_request(
|
|
327
|
+
self,
|
|
328
|
+
text: Optional[str] = None,
|
|
329
|
+
filter: Optional[List[Union[Label, str]]] = None,
|
|
330
|
+
vector: Optional[Union[np.ndarray, List[float]]] = None,
|
|
331
|
+
vectorset: Optional[str] = None,
|
|
332
|
+
min_score: Optional[float] = 0.0,
|
|
333
|
+
) -> SearchRequest:
|
|
334
|
+
args: Dict[str, Any] = {"features": []}
|
|
335
|
+
if filter is not None:
|
|
336
|
+
new_filter: List[Label] = []
|
|
337
|
+
for fil in filter:
|
|
338
|
+
if isinstance(fil, str):
|
|
339
|
+
if len(fil.split("/")) == 1:
|
|
340
|
+
lset = DEFAULT_LABELSET
|
|
341
|
+
lab = fil
|
|
342
|
+
else:
|
|
343
|
+
lset, lab = fil.split("/")
|
|
344
|
+
new_filter.append(Label(label=lab, labelset=lset))
|
|
345
|
+
else:
|
|
346
|
+
new_filter.append(fil)
|
|
347
|
+
filter_list = [f"/l/{label.labelset}/{label.label}" for label in new_filter]
|
|
348
|
+
args["filters"] = filter_list
|
|
349
|
+
|
|
350
|
+
if text is not None:
|
|
351
|
+
args["query"] = text
|
|
352
|
+
args["features"].append(SearchOptions.DOCUMENT)
|
|
353
|
+
args["features"].append(SearchOptions.PARAGRAPH)
|
|
354
|
+
|
|
355
|
+
if vector is not None and vectorset is not None:
|
|
356
|
+
vector = convert_vector(vector)
|
|
357
|
+
|
|
358
|
+
args["vector"] = vector
|
|
359
|
+
args["vectorset"] = vectorset
|
|
360
|
+
args["features"].append(SearchOptions.VECTOR)
|
|
361
|
+
|
|
362
|
+
if len(args["features"]) == 0:
|
|
363
|
+
args["features"].append(SearchOptions.DOCUMENT)
|
|
364
|
+
args["features"].append(SearchOptions.PARAGRAPH)
|
|
365
|
+
|
|
366
|
+
args["min_score"] = min_score
|
|
367
|
+
request = SearchRequest(**args)
|
|
368
|
+
return request
|
nucliadb_sdk/labels.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (C) 2021 Bosutech XXI S.L.
|
|
2
|
+
#
|
|
3
|
+
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
|
4
|
+
# For commercial licensing, contact us at info@nuclia.com.
|
|
5
|
+
#
|
|
6
|
+
# AGPL:
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Affero General Public License as
|
|
9
|
+
# published by the Free Software Foundation, either version 3 of the
|
|
10
|
+
# License, or (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Affero General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Dict, List, Optional, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LabelType(str, Enum):
|
|
26
|
+
PARAGRAPHS = "PARAGRAPHS"
|
|
27
|
+
RESOURCES = "RESOURCES"
|
|
28
|
+
SENTENCES = "SENTENCES"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Label:
|
|
33
|
+
label: str
|
|
34
|
+
labelset: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Labels = List[Union[Label, str]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LabelSet:
|
|
42
|
+
count: int
|
|
43
|
+
labels: Dict[str, int] = field(default_factory=dict)
|
nucliadb_sdk/py.typed
ADDED
|
File without changes
|