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,24 @@
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 logging
21
+
22
+ logger = logging.getLogger("nucliadb_sdk")
23
+
24
+ DEFAULT_LABELSET = "default"
nucliadb_sdk/client.py ADDED
@@ -0,0 +1,420 @@
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 base64
21
+ from enum import Enum
22
+ from typing import Optional
23
+
24
+ import httpx
25
+ import requests
26
+
27
+ from nucliadb_models.entities import KnowledgeBoxEntities
28
+ from nucliadb_models.labels import KnowledgeBoxLabels
29
+ from nucliadb_models.resource import Resource
30
+ from nucliadb_models.search import (
31
+ KnowledgeboxCounters,
32
+ KnowledgeboxSearchResults,
33
+ SearchRequest,
34
+ )
35
+ from nucliadb_models.vectors import VectorSet, VectorSets
36
+ from nucliadb_models.writer import (
37
+ CreateResourcePayload,
38
+ ResourceCreated,
39
+ UpdateResourcePayload,
40
+ )
41
+
42
+ RESOURCE_PATH = "/resource/{rid}"
43
+ RESOURCE_PATH_BY_SLUG = "/slug/{slug}"
44
+ SEARCH_PATH = "/search"
45
+ CREATE_RESOURCE_PATH = "/resources"
46
+ CREATE_VECTORSET = "/vectorset/{vectorset}"
47
+ VECTORSETS = "/vectorsets"
48
+ COUNTER = "/counters"
49
+ SEARCH_URL = "/search"
50
+ LABELS_URL = "/labelsets"
51
+ ENTITIES_URL = "/entitiesgroups"
52
+ DOWNLOAD_URL = "/{uri}"
53
+ TUS_UPLOAD_URL = "/resource/{rid}/file/{field}/tusupload"
54
+
55
+
56
+ class HTTPError(Exception):
57
+ pass
58
+
59
+
60
+ class Environment(str, Enum):
61
+ CLOUD = "CLOUD"
62
+ OSS = "OSS"
63
+
64
+
65
+ class NucliaDBClient:
66
+ api_key: Optional[str]
67
+ environment: Environment
68
+ session: httpx.Client
69
+ url: Optional[str]
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ environment: Environment = Environment.CLOUD,
75
+ url: Optional[str] = None,
76
+ api_key: Optional[str] = None,
77
+ writer_host: Optional[str] = None,
78
+ reader_host: Optional[str] = None,
79
+ search_host: Optional[str] = None,
80
+ train_host: Optional[str] = None,
81
+ ):
82
+ self.api_key = api_key
83
+ self.environment = environment
84
+ self.url = url
85
+
86
+ internal_hosts_set = all((writer_host, reader_host, search_host, train_host))
87
+ url_set = bool(url)
88
+
89
+ if not (url_set or internal_hosts_set):
90
+ raise AttributeError("Either url or nucliadb services hosts must be set")
91
+
92
+ if url_set:
93
+ self.url = url
94
+ elif internal_hosts_set:
95
+ self.url = writer_host
96
+
97
+ if environment == Environment.CLOUD and api_key is not None:
98
+ reader_headers = {
99
+ "X-STF-SERVICEACCOUNT": f"Bearer {api_key}",
100
+ }
101
+ writer_headers = {
102
+ "X-STF-SERVICEACCOUNT": f"Bearer {api_key}",
103
+ }
104
+ elif environment == Environment.CLOUD and api_key is None:
105
+ raise AttributeError("On Cloud you need to provide API Key")
106
+ else:
107
+ reader_headers = {
108
+ "X-NUCLIADB-ROLES": f"READER",
109
+ }
110
+ writer_headers = {
111
+ "X-NUCLIADB-ROLES": f"WRITER",
112
+ }
113
+
114
+ self.reader_session = httpx.Client(
115
+ headers=reader_headers, base_url=reader_host or url # type: ignore
116
+ )
117
+ self.async_reader_session = httpx.AsyncClient(
118
+ headers=reader_headers, base_url=reader_host or url # type: ignore
119
+ )
120
+ self.stream_session = requests.Session()
121
+ self.stream_session.headers.update(reader_headers)
122
+ self.writer_session = httpx.Client(
123
+ headers=writer_headers, base_url=writer_host or url # type: ignore
124
+ )
125
+ self.async_writer_session = httpx.AsyncClient(
126
+ headers=writer_headers, base_url=writer_host or url # type: ignore
127
+ )
128
+ self.search_session = httpx.Client(
129
+ headers=reader_headers, base_url=search_host or url # type: ignore
130
+ )
131
+ self.async_search_session = httpx.AsyncClient(
132
+ headers=reader_headers, base_url=search_host or url # type: ignore
133
+ )
134
+ self.train_session = httpx.Client(
135
+ headers=reader_headers, base_url=train_host or url # type: ignore
136
+ )
137
+
138
+ def get_resource(self, id: str):
139
+ url = RESOURCE_PATH.format(rid=id)
140
+ params = {
141
+ "show": ["values", "relations", "origin", "basic"],
142
+ "extracted": ["vectors", "text", "metadata", "link", "file"],
143
+ }
144
+ response = self.reader_session.get(
145
+ url,
146
+ params=params,
147
+ )
148
+ if response.status_code == 200:
149
+ return Resource.parse_raw(response.content)
150
+ elif response.status_code == 404:
151
+ url = RESOURCE_PATH_BY_SLUG.format(slug=id)
152
+ response = self.reader_session.get(url, params=params)
153
+ if response.status_code == 200:
154
+ return Resource.parse_raw(response.content)
155
+ else:
156
+ raise KeyError(f"No key {id}")
157
+ else:
158
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
159
+
160
+ async def async_get_resource(self, id: str):
161
+ url = RESOURCE_PATH.format(rid=id)
162
+ params = {
163
+ "show": ["values", "relations", "origin", "basic"],
164
+ "extracted": ["vectors", "text", "metadata", "link", "file"],
165
+ }
166
+ response = await self.async_reader_session.get(url, params=params)
167
+ if response.status_code == 200:
168
+ return Resource.parse_raw(response.content)
169
+ elif response.status_code == 404:
170
+ url = RESOURCE_PATH_BY_SLUG.format(slug=id)
171
+ response = await self.async_reader_session.get(url, params=params)
172
+ if response.status_code == 200:
173
+ return Resource.parse_raw(response.content)
174
+ else:
175
+ raise KeyError(f"No key {id}")
176
+ else:
177
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
178
+
179
+ def del_resource(self, id: str):
180
+ url = RESOURCE_PATH.format(rid=id)
181
+ response = self.writer_session.delete(url)
182
+ if response.status_code == 204:
183
+ return
184
+ elif response.status_code == 404:
185
+ url = RESOURCE_PATH_BY_SLUG.format(slug=id)
186
+ response = self.writer_session.delete(url)
187
+ if response.status_code == 204:
188
+ return
189
+ elif response.status_code == 404:
190
+ raise KeyError(f"No key {id}")
191
+ else:
192
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
193
+ else:
194
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
195
+
196
+ async def async_del_resource(self, id: str):
197
+ url = RESOURCE_PATH.format(rid=id)
198
+ response = await self.async_writer_session.delete(url)
199
+ if response.status_code == 204:
200
+ return
201
+ elif response.status_code == 404:
202
+ url = RESOURCE_PATH_BY_SLUG.format(slug=id)
203
+ response = await self.async_writer_session.delete(url)
204
+ if response.status_code == 204:
205
+ return
206
+ elif response.status_code == 404:
207
+ raise KeyError(f"No key {id}")
208
+ else:
209
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
210
+ else:
211
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
212
+
213
+ def list_resources(self):
214
+ url = CREATE_RESOURCE_PATH
215
+ response = self.reader_session.get(url)
216
+ if response.status_code == 200:
217
+ return Resource.parse_raw(response.content)
218
+ elif response.status_code == 404:
219
+ raise KeyError(f"No key {id}")
220
+ else:
221
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
222
+
223
+ async def async_list_resources(self):
224
+ url = CREATE_RESOURCE_PATH
225
+ response = await self.async_reader_session.get(url)
226
+ if response.status_code == 200:
227
+ return Resource.parse_raw(response.content)
228
+ elif response.status_code == 404:
229
+ return KeyError(f"No key {id}")
230
+ else:
231
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
232
+
233
+ def set_vectorset(self, vectorset: str, payload: VectorSet):
234
+ url = CREATE_VECTORSET.format(vectorset=vectorset)
235
+ response = self.writer_session.post(url, content=payload.json())
236
+ if response.status_code == 200:
237
+ return
238
+ else:
239
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
240
+
241
+ async def async_set_vectorset(self, vectorset: str, payload: VectorSet):
242
+ url = CREATE_VECTORSET.format(vectorset=vectorset)
243
+ response = await self.async_writer_session.post(url, content=payload.json())
244
+ if response.status_code == 200:
245
+ return
246
+ else:
247
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
248
+
249
+ def del_vectorset(self, vectorset: str):
250
+ url = CREATE_VECTORSET.format(vectorset=vectorset)
251
+ response = self.writer_session.delete(url)
252
+ if response.status_code == 200:
253
+ return
254
+ else:
255
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
256
+
257
+ async def async_del_vectorset(self, vectorset: str):
258
+ url = CREATE_VECTORSET.format(vectorset=vectorset)
259
+ response = await self.async_writer_session.delete(url)
260
+ if response.status_code == 200:
261
+ return
262
+ else:
263
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
264
+
265
+ def get_vectorsets(self):
266
+ url = VECTORSETS
267
+ response = self.reader_session.get(url)
268
+ if response.status_code == 200:
269
+ return VectorSets.parse_raw(response.content)
270
+ else:
271
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
272
+
273
+ async def async_get_vectorsets(self):
274
+ url = VECTORSETS
275
+ response = await self.async_reader_session.get(url)
276
+ if response.status_code == 200:
277
+ return VectorSets.parse_raw(response.content)
278
+ else:
279
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
280
+
281
+ def create_resource(self, payload: CreateResourcePayload) -> ResourceCreated:
282
+ url = CREATE_RESOURCE_PATH
283
+ response: httpx.Response = self.writer_session.post(url, content=payload.json())
284
+ if response.status_code == 201:
285
+ return ResourceCreated.parse_raw(response.content)
286
+ else:
287
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
288
+
289
+ async def async_create_resource(
290
+ self, payload: CreateResourcePayload
291
+ ) -> ResourceCreated:
292
+ url = CREATE_RESOURCE_PATH
293
+ response: httpx.Response = await self.async_writer_session.post(
294
+ url, content=payload.json()
295
+ )
296
+ if response.status_code == 201:
297
+ return ResourceCreated.parse_raw(response.content)
298
+ else:
299
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
300
+
301
+ def update_resource(self, id: str, payload: UpdateResourcePayload):
302
+ url = RESOURCE_PATH.format(rid=id)
303
+ response: httpx.Response = self.writer_session.post(url, content=payload.json())
304
+ if response.status_code == 200:
305
+ return
306
+ else:
307
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
308
+
309
+ async def async_update_resource(self, id: str, payload: UpdateResourcePayload):
310
+ url = RESOURCE_PATH.format(rid=id)
311
+ response: httpx.Response = await self.async_writer_session.post(
312
+ url, content=payload.json()
313
+ )
314
+ if response.status_code == 200:
315
+ return
316
+ else:
317
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
318
+
319
+ def length(self) -> KnowledgeboxCounters:
320
+ url = COUNTER
321
+ response: httpx.Response = self.search_session.get(url)
322
+ if response.status_code == 200:
323
+ return KnowledgeboxCounters.parse_raw(response.content)
324
+ else:
325
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
326
+
327
+ async def async_length(self) -> KnowledgeboxCounters:
328
+ url = COUNTER
329
+ response: httpx.Response = await self.async_search_session.get(url)
330
+ if response.status_code == 200:
331
+ return KnowledgeboxCounters.parse_raw(response.content)
332
+ else:
333
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
334
+
335
+ def get_entities(self) -> KnowledgeBoxEntities:
336
+ url = ENTITIES_URL
337
+ response: httpx.Response = self.reader_session.get(url)
338
+ if response.status_code == 200:
339
+ return KnowledgeBoxEntities.parse_raw(response.content)
340
+ else:
341
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
342
+
343
+ def get_labels(self) -> KnowledgeBoxLabels:
344
+ url = LABELS_URL
345
+ response: httpx.Response = self.reader_session.get(url)
346
+ if response.status_code == 200:
347
+ return KnowledgeBoxLabels.parse_raw(response.content)
348
+ else:
349
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
350
+
351
+ def search(self, request: SearchRequest):
352
+ url = SEARCH_URL
353
+ response: httpx.Response = self.search_session.post(url, content=request.json())
354
+ if response.status_code == 200:
355
+ return KnowledgeboxSearchResults.parse_raw(response.content)
356
+ else:
357
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
358
+
359
+ async def async_search(self, request: SearchRequest):
360
+ url = SEARCH_URL
361
+ response: httpx.Response = await self.async_search_session.post(
362
+ url, content=request.json()
363
+ )
364
+ if response.status_code == 200:
365
+ return KnowledgeboxSearchResults.parse_raw(response.content)
366
+ else:
367
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
368
+
369
+ def download(self, uri: str) -> bytes:
370
+ # uri has format
371
+ # /kb/2a00d5b4-cfcc-48eb-85ac-d70bfd38b26d/resource/41d02aac4ade48098b23e38141807738/file/file/download/field
372
+ # we need to remove the kb url
373
+
374
+ uri_parts = uri.split("/")
375
+ if len(uri_parts) < 9:
376
+ raise AttributeError("Not a valid download uri")
377
+
378
+ new_uri = "/".join(uri_parts[3:])
379
+ url = DOWNLOAD_URL.format(uri=new_uri)
380
+ response: httpx.Response = self.reader_session.get(url)
381
+ if response.status_code == 200:
382
+ return response.content
383
+ else:
384
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
385
+
386
+ def start_tus_upload(
387
+ self,
388
+ rid: str,
389
+ field: str,
390
+ size: int,
391
+ filename: str,
392
+ content_type: str = "application/octet-stream",
393
+ ):
394
+ url = TUS_UPLOAD_URL.format(rid=rid, field=field)
395
+ encoded_filename = base64.b64encode(filename.encode()).decode()
396
+ headers = {
397
+ "upload-length": str(size),
398
+ "tus-resumable": "1.0.0",
399
+ "upload-metadata": f"filename {encoded_filename}",
400
+ "content-type": content_type,
401
+ }
402
+ response: httpx.Response = self.writer_session.post(url, headers=headers)
403
+ if response.status_code == 201:
404
+ return response.headers.get("Location")
405
+ else:
406
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
407
+
408
+ def patch_tus_upload(self, upload_url: str, data: bytes, offset: int) -> int:
409
+ headers = {
410
+ "upload-offset": str(offset),
411
+ }
412
+ # upload url has all path, we should remove /kb/kbid/
413
+ upload_url = "/" + "/".join(upload_url.split("/")[3:])
414
+ response: httpx.Response = self.writer_session.patch(
415
+ upload_url, headers=headers, content=data
416
+ )
417
+ if response.status_code == 200:
418
+ return int(response.headers.get("Upload-Offset"))
419
+ else:
420
+ raise HTTPError(f"Status code {response.status_code}: {response.text}")
@@ -0,0 +1,33 @@
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
21
+ from typing import List, Tuple
22
+
23
+ EntityPosition = Tuple[int, int]
24
+
25
+
26
+ @dataclass
27
+ class Entity:
28
+ type: str
29
+ value: str
30
+ positions: List[EntityPosition]
31
+
32
+
33
+ Entities = List[Entity]
nucliadb_sdk/file.py ADDED
@@ -0,0 +1,27 @@
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
21
+
22
+
23
+ @dataclass
24
+ class File:
25
+ data: bytes
26
+ filename: str
27
+ content_type: str = "application/octet-stream"