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
nucliadb_sdk/__init__.py
ADDED
|
@@ -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}")
|
nucliadb_sdk/entities.py
ADDED
|
@@ -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"
|