pyPreservica 2.7.2__py3-none-any.whl → 3.3.4__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.
- pyPreservica/__init__.py +18 -6
- pyPreservica/adminAPI.py +29 -22
- pyPreservica/authorityAPI.py +6 -7
- pyPreservica/common.py +116 -19
- pyPreservica/contentAPI.py +179 -8
- pyPreservica/entityAPI.py +730 -214
- pyPreservica/mdformsAPI.py +501 -29
- pyPreservica/monitorAPI.py +2 -2
- pyPreservica/parAPI.py +1 -37
- pyPreservica/retentionAPI.py +58 -26
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +298 -480
- pyPreservica/webHooksAPI.py +42 -1
- pyPreservica/workflowAPI.py +17 -13
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/METADATA +20 -9
- pypreservica-3.3.4.dist-info/RECORD +20 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/WHEEL +1 -1
- pyPreservica/vocabularyAPI.py +0 -141
- pyPreservica-2.7.2.dist-info/RECORD +0 -20
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/top_level.txt +0 -0
pyPreservica/contentAPI.py
CHANGED
|
@@ -10,17 +10,43 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
-
from
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from typing import Generator, Callable, Optional, Union
|
|
14
15
|
from pyPreservica.common import *
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
19
|
+
class SortOrder(Enum):
|
|
20
|
+
asc = 1
|
|
21
|
+
desc = 2
|
|
22
|
+
|
|
23
|
+
class Field:
|
|
24
|
+
name: str
|
|
25
|
+
value: Optional[str]
|
|
26
|
+
operator: Optional[str]
|
|
27
|
+
sort_order: Optional[SortOrder]
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str, value: str, operator: Optional[str]=None, sort_order: Optional[SortOrder]=None):
|
|
30
|
+
self.name = name
|
|
31
|
+
self.value = value
|
|
32
|
+
self.operator = operator
|
|
33
|
+
self.sort_order = sort_order
|
|
34
|
+
|
|
35
|
+
|
|
18
36
|
|
|
19
37
|
class ContentAPI(AuthenticatedAPI):
|
|
38
|
+
"""
|
|
39
|
+
The ContentAPI class provides the search interface to the Preservica repository.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
45
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
46
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
|
|
48
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
49
|
+
protocol, request_hook, credentials_path)
|
|
24
50
|
self.callback = None
|
|
25
51
|
|
|
26
52
|
class SearchResult:
|
|
@@ -70,6 +96,29 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
70
96
|
logger.error(f"object_details failed with error code: {request.status_code}")
|
|
71
97
|
raise RuntimeError(request.status_code, f"object_details failed with error code: {request.status_code}")
|
|
72
98
|
|
|
99
|
+
|
|
100
|
+
def download_bytes(self, reference):
|
|
101
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
102
|
+
params = {'id': f'sdb:IO|{reference}'}
|
|
103
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/download', params=params, headers=headers,
|
|
104
|
+
stream=True) as req:
|
|
105
|
+
if req.status_code == requests.codes.ok:
|
|
106
|
+
file_bytes = BytesIO()
|
|
107
|
+
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
108
|
+
file_bytes.write(chunk)
|
|
109
|
+
file_bytes.seek(0)
|
|
110
|
+
return file_bytes
|
|
111
|
+
elif req.status_code == requests.codes.unauthorized:
|
|
112
|
+
self.token = self.__token__()
|
|
113
|
+
return self.download_bytes(reference)
|
|
114
|
+
elif req.status_code == requests.codes.not_found:
|
|
115
|
+
logger.error(f"The requested asset reference is not found in the repository: {reference}")
|
|
116
|
+
raise RuntimeError(reference, "The requested reference is not found in the repository")
|
|
117
|
+
else:
|
|
118
|
+
logger.error(f"download failed with error code: {req.status_code}")
|
|
119
|
+
raise RuntimeError(req.status_code, f"download failed with error code: {req.status_code}")
|
|
120
|
+
|
|
121
|
+
|
|
73
122
|
def download(self, reference, filename):
|
|
74
123
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
75
124
|
params = {'id': f'sdb:IO|{reference}'}
|
|
@@ -92,6 +141,27 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
92
141
|
logger.error(f"download failed with error code: {req.status_code}")
|
|
93
142
|
raise RuntimeError(req.status_code, f"download failed with error code: {req.status_code}")
|
|
94
143
|
|
|
144
|
+
def thumbnail_bytes(self, entity_type, reference: str, size: Thumbnail = Thumbnail.LARGE) -> Union[BytesIO, None]:
|
|
145
|
+
headers = {HEADER_TOKEN: self.token, 'accept': 'image/png'}
|
|
146
|
+
params = {'id': f'sdb:{entity_type}|{reference}', 'size': f'{size.value}'}
|
|
147
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/thumbnail', params=params, headers=headers, stream=True) as req:
|
|
148
|
+
if req.status_code == requests.codes.ok:
|
|
149
|
+
file_bytes = BytesIO()
|
|
150
|
+
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
151
|
+
file_bytes.write(chunk)
|
|
152
|
+
file_bytes.seek(0)
|
|
153
|
+
return file_bytes
|
|
154
|
+
elif req.status_code == requests.codes.unauthorized:
|
|
155
|
+
self.token = self.__token__()
|
|
156
|
+
return self.thumbnail_bytes(entity_type, reference, size)
|
|
157
|
+
elif req.status_code == requests.codes.not_found:
|
|
158
|
+
logger.error(req.content.decode("utf-8"))
|
|
159
|
+
logger.error(f"The requested reference is not found in the repository: {reference}")
|
|
160
|
+
raise RuntimeError(reference, "The requested reference is not found in the repository")
|
|
161
|
+
else:
|
|
162
|
+
logger.error(f"thumbnail failed with error code: {req.status_code}")
|
|
163
|
+
raise RuntimeError(req.status_code, f"thumbnail failed with error code: {req.status_code}")
|
|
164
|
+
|
|
95
165
|
def thumbnail(self, entity_type, reference, filename, size=Thumbnail.LARGE):
|
|
96
166
|
headers = {HEADER_TOKEN: self.token, 'accept': 'image/png'}
|
|
97
167
|
params = {'id': f'sdb:{entity_type}|{reference}', 'size': f'{size.value}'}
|
|
@@ -130,7 +200,8 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
130
200
|
logger.error(f"indexed_fields failed with error code: {results.status_code}")
|
|
131
201
|
raise RuntimeError(results.status_code, f"indexed_fields failed with error code: {results.status_code}")
|
|
132
202
|
|
|
133
|
-
def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
|
|
203
|
+
def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
|
|
204
|
+
list_indexes: list = None):
|
|
134
205
|
if list_indexes is None or len(list_indexes) == 0:
|
|
135
206
|
metadata_fields = ["xip.reference", "xip.title", "xip.description", "xip.document_type",
|
|
136
207
|
"xip.parent_ref", "xip.security_descriptor"]
|
|
@@ -193,7 +264,8 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
193
264
|
logger.error(f"search failed with error code: {results.status_code}")
|
|
194
265
|
raise RuntimeError(results.status_code, f"simple_search failed with error code: {results.status_code}")
|
|
195
266
|
|
|
196
|
-
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
|
|
267
|
+
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
|
|
268
|
+
filter_values: dict = None,
|
|
197
269
|
sort_values: dict = None):
|
|
198
270
|
if filter_values is None:
|
|
199
271
|
filter_values = {}
|
|
@@ -208,6 +280,97 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
208
280
|
writer.writeheader()
|
|
209
281
|
writer.writerows(self.search_index_filter_list(query, page_size, filter_values, sort_values))
|
|
210
282
|
|
|
283
|
+
def search_fields(self, query: str = "%", fields: list[Field]=None, page_size: int = 25) -> Generator:
|
|
284
|
+
"""
|
|
285
|
+
Run a search query with multiple fields
|
|
286
|
+
|
|
287
|
+
:param query: The main search query.
|
|
288
|
+
:param fields: List of search fields
|
|
289
|
+
:param page_size: The default search page size
|
|
290
|
+
:return: search result
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
if self.major_version < 7 and self.minor_version < 5:
|
|
294
|
+
raise RuntimeError("search_fields API call is not available when connected to a v7.5 System")
|
|
295
|
+
|
|
296
|
+
search_result = self._search_fields(query=query, fields=fields, start_index=0, page_size=page_size)
|
|
297
|
+
for e in search_result.results_list:
|
|
298
|
+
yield e
|
|
299
|
+
found = len(search_result.results_list)
|
|
300
|
+
while search_result.hits > found:
|
|
301
|
+
search_result = self._search_fields(query=query, fields=fields, start_index=found, page_size=page_size)
|
|
302
|
+
for e in search_result.results_list:
|
|
303
|
+
yield e
|
|
304
|
+
found = found + len(search_result.results_list)
|
|
305
|
+
|
|
306
|
+
def _search_fields(self, query: str = "%", fields: list[Field]=None, start_index: int = 0, page_size: int = 25):
|
|
307
|
+
|
|
308
|
+
start_from = str(start_index)
|
|
309
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded', HEADER_TOKEN: self.token}
|
|
310
|
+
|
|
311
|
+
if fields is None:
|
|
312
|
+
fields = []
|
|
313
|
+
|
|
314
|
+
field_list = []
|
|
315
|
+
sort_list = []
|
|
316
|
+
metadata_elements = []
|
|
317
|
+
for field in fields:
|
|
318
|
+
metadata_elements.append(field.name)
|
|
319
|
+
if field.value is None or field.value == "":
|
|
320
|
+
field_list.append('{' f' "name": "{field.name}", "values": [] ' + '}')
|
|
321
|
+
elif field.operator == "NOT":
|
|
322
|
+
field_list.append('{' f' "name": "{field.name}", "values": ["{field.value}"], "operator": "NOT" ' + '}')
|
|
323
|
+
else:
|
|
324
|
+
field_list.append('{' f' "name": "{field.name}", "values": ["{field.value}"] ' + '}')
|
|
325
|
+
|
|
326
|
+
if field.sort_order is not None:
|
|
327
|
+
sort_list.append(f'{{"sortFields": ["{field.name}"], "sortOrder": "{field.sort_order.name}"}}')
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
filter_terms = ','.join(field_list)
|
|
331
|
+
|
|
332
|
+
if len(sort_list) == 0:
|
|
333
|
+
query_term = ('{ "q": "%s", "fields": [ %s ] }' % (query, filter_terms))
|
|
334
|
+
else:
|
|
335
|
+
sort_terms = ','.join(sort_list)
|
|
336
|
+
query_term = ('{ "q": "%s", "fields": [ %s ], "sort": [ %s ]}' % (query, filter_terms, sort_terms))
|
|
337
|
+
|
|
338
|
+
if len(metadata_elements) == 0:
|
|
339
|
+
metadata_elements.append("xip.title")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
payload = {'start': start_from, 'max': str(page_size), 'metadata': list(metadata_elements), 'q': query_term}
|
|
343
|
+
logger.debug(payload)
|
|
344
|
+
results = self.session.post(f'{self.protocol}://{self.server}/api/content/search', data=payload,
|
|
345
|
+
headers=headers)
|
|
346
|
+
results_list = []
|
|
347
|
+
if results.status_code == requests.codes.ok:
|
|
348
|
+
json_doc = results.json()
|
|
349
|
+
metadata = json_doc['value']['metadata']
|
|
350
|
+
refs = list(json_doc['value']['objectIds'])
|
|
351
|
+
refs = list(map(lambda x: content_api_identifier_to_type(x), refs))
|
|
352
|
+
hits = int(json_doc['value']['totalHits'])
|
|
353
|
+
|
|
354
|
+
for m_row, r_row in zip(metadata, refs):
|
|
355
|
+
results_map = {'xip.reference': r_row[1]}
|
|
356
|
+
for li in m_row:
|
|
357
|
+
results_map[li['name']] = li['value']
|
|
358
|
+
results_list.append(results_map)
|
|
359
|
+
next_start = start_index + page_size
|
|
360
|
+
|
|
361
|
+
if self.callback is not None:
|
|
362
|
+
value = str(f'{len(results_list) + start_index}:{hits}')
|
|
363
|
+
self.callback(value)
|
|
364
|
+
|
|
365
|
+
search_results = self.SearchResult(metadata, refs, hits, results_list, next_start)
|
|
366
|
+
return search_results
|
|
367
|
+
elif results.status_code == requests.codes.unauthorized:
|
|
368
|
+
self.token = self.__token__()
|
|
369
|
+
return self._search_fields(query, fields, start_index, page_size)
|
|
370
|
+
else:
|
|
371
|
+
logger.error(f"search failed with error code: {results.status_code}")
|
|
372
|
+
raise RuntimeError(results.status_code, f"search_index_filter failed")
|
|
373
|
+
|
|
211
374
|
def search_index_filter_list(self, query: str = "%", page_size: int = 25, filter_values: dict = None,
|
|
212
375
|
sort_values: dict = None) -> Generator:
|
|
213
376
|
"""
|
|
@@ -245,7 +408,11 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
245
408
|
if value == "":
|
|
246
409
|
field_list.append('{' f' "name": "{key}", "values": [] ' + '}')
|
|
247
410
|
else:
|
|
248
|
-
|
|
411
|
+
if isinstance(value, str):
|
|
412
|
+
field_list.append('{' f' "name": "{key}", "values": ["{value}"] ' + '}')
|
|
413
|
+
if isinstance(value, list):
|
|
414
|
+
v = f' {",".join(f'"{w}"' for w in value)} '
|
|
415
|
+
field_list.append('{' f' "name": "{key}", "values":[ {v} ]' '}')
|
|
249
416
|
|
|
250
417
|
filter_terms = ','.join(field_list)
|
|
251
418
|
|
|
@@ -277,7 +444,11 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
277
444
|
if value == "":
|
|
278
445
|
field_list.append('{' f' "name": "{key}", "values": [] ' + '}')
|
|
279
446
|
else:
|
|
280
|
-
|
|
447
|
+
if isinstance(value, str):
|
|
448
|
+
field_list.append('{' f' "name": "{key}", "values": ["{value}"] ' + '}')
|
|
449
|
+
if isinstance(value, list):
|
|
450
|
+
v = f' {",".join(f'"{w}"' for w in value)} '
|
|
451
|
+
field_list.append('{' f' "name": "{key}", "values":[ {v} ]' '}')
|
|
281
452
|
|
|
282
453
|
filter_terms = ','.join(field_list)
|
|
283
454
|
|