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