pyPreservica 0.9.9__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 +26 -8
- pyPreservica/adminAPI.py +877 -0
- pyPreservica/authorityAPI.py +229 -0
- pyPreservica/common.py +553 -94
- pyPreservica/contentAPI.py +331 -65
- pyPreservica/entityAPI.py +1805 -446
- pyPreservica/mdformsAPI.py +572 -0
- pyPreservica/monitorAPI.py +153 -0
- pyPreservica/opex.py +98 -0
- pyPreservica/parAPI.py +226 -0
- pyPreservica/retentionAPI.py +155 -44
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +1120 -321
- pyPreservica/webHooksAPI.py +211 -0
- pyPreservica/workflowAPI.py +99 -47
- {pyPreservica-0.9.9.dist-info → pypreservica-3.3.4.dist-info}/METADATA +93 -66
- pypreservica-3.3.4.dist-info/RECORD +20 -0
- {pyPreservica-0.9.9.dist-info → pypreservica-3.3.4.dist-info}/WHEEL +5 -5
- pyPreservica-0.9.9.dist-info/RECORD +0 -12
- {pyPreservica-0.9.9.dist-info → pypreservica-3.3.4.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-0.9.9.dist-info → pypreservica-3.3.4.dist-info}/top_level.txt +0 -0
pyPreservica/contentAPI.py
CHANGED
|
@@ -10,14 +10,43 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from typing import Generator, Callable, Optional, Union
|
|
13
15
|
from pyPreservica.common import *
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
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
|
+
|
|
36
|
+
|
|
17
37
|
class ContentAPI(AuthenticatedAPI):
|
|
38
|
+
"""
|
|
39
|
+
The ContentAPI class provides the search interface to the Preservica repository.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
18
43
|
|
|
19
|
-
def __init__(self, username=None, password=None, tenant=None, server=None,
|
|
20
|
-
|
|
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'):
|
|
47
|
+
|
|
48
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
49
|
+
protocol, request_hook, credentials_path)
|
|
21
50
|
self.callback = None
|
|
22
51
|
|
|
23
52
|
class SearchResult:
|
|
@@ -31,10 +60,30 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
31
60
|
def search_callback(self, fn):
|
|
32
61
|
self.callback = fn
|
|
33
62
|
|
|
34
|
-
def
|
|
63
|
+
def user_security_tags(self, with_permissions: bool = False):
|
|
64
|
+
"""
|
|
65
|
+
Return available security tags
|
|
66
|
+
|
|
67
|
+
:return: dict of security tags
|
|
68
|
+
:rtype: dict
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
return self.security_tags_base(with_permissions=with_permissions)
|
|
72
|
+
|
|
73
|
+
def object_details(self, entity_type, reference: str) -> dict:
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
:param entity_type:
|
|
77
|
+
:param reference:
|
|
78
|
+
:return: Dictionary of object attributes
|
|
79
|
+
"""
|
|
35
80
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
if type(entity_type) == EntityType:
|
|
82
|
+
params = {'id': f'sdb:{entity_type.value}|{reference}'}
|
|
83
|
+
else:
|
|
84
|
+
params = {'id': f'sdb:{entity_type}|{reference}'}
|
|
85
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/content/object-details', params=params,
|
|
86
|
+
headers=headers)
|
|
38
87
|
if request.status_code == requests.codes.ok:
|
|
39
88
|
return request.json()["value"]
|
|
40
89
|
elif request.status_code == requests.codes.not_found:
|
|
@@ -47,11 +96,34 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
47
96
|
logger.error(f"object_details failed with error code: {request.status_code}")
|
|
48
97
|
raise RuntimeError(request.status_code, f"object_details failed with error code: {request.status_code}")
|
|
49
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
|
+
|
|
50
122
|
def download(self, reference, filename):
|
|
51
123
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
52
124
|
params = {'id': f'sdb:IO|{reference}'}
|
|
53
|
-
with
|
|
54
|
-
|
|
125
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/download', params=params, headers=headers,
|
|
126
|
+
stream=True) as req:
|
|
55
127
|
if req.status_code == requests.codes.ok:
|
|
56
128
|
with open(filename, 'wb') as file:
|
|
57
129
|
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
@@ -69,27 +141,43 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
69
141
|
logger.error(f"download failed with error code: {req.status_code}")
|
|
70
142
|
raise RuntimeError(req.status_code, f"download failed with error code: {req.status_code}")
|
|
71
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
|
+
|
|
72
165
|
def thumbnail(self, entity_type, reference, filename, size=Thumbnail.LARGE):
|
|
73
|
-
headers = {HEADER_TOKEN: self.token, '
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
params = {'id': f'sdb:SO|{reference}', 'size': f'{size.value}'}
|
|
78
|
-
else:
|
|
79
|
-
print(f"entity_type must be a folder or asset, IO or SO")
|
|
80
|
-
raise SystemExit
|
|
81
|
-
with requests.get(f'https://{self.server}/api/content/thumbnail', params=params, headers=headers) as req:
|
|
166
|
+
headers = {HEADER_TOKEN: self.token, 'accept': 'image/png'}
|
|
167
|
+
params = {'id': f'sdb:{entity_type}|{reference}', 'size': f'{size.value}'}
|
|
168
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/thumbnail', params=params, headers=headers,
|
|
169
|
+
stream=True) as req:
|
|
82
170
|
if req.status_code == requests.codes.ok:
|
|
83
171
|
with open(filename, 'wb') as file:
|
|
84
172
|
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
85
173
|
file.write(chunk)
|
|
86
174
|
file.flush()
|
|
87
|
-
file.close()
|
|
88
175
|
return filename
|
|
89
176
|
elif req.status_code == requests.codes.unauthorized:
|
|
90
177
|
self.token = self.__token__()
|
|
91
178
|
return self.thumbnail(entity_type, reference, filename, size)
|
|
92
179
|
elif req.status_code == requests.codes.not_found:
|
|
180
|
+
logger.error(req.content.decode("utf-8"))
|
|
93
181
|
logger.error(f"The requested reference is not found in the repository: {reference}")
|
|
94
182
|
raise RuntimeError(reference, "The requested reference is not found in the repository")
|
|
95
183
|
else:
|
|
@@ -98,12 +186,12 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
98
186
|
|
|
99
187
|
def indexed_fields(self):
|
|
100
188
|
headers = {HEADER_TOKEN: self.token}
|
|
101
|
-
results =
|
|
189
|
+
results = self.session.get(f'{self.protocol}://{self.server}/api/content/indexed-fields', headers=headers)
|
|
102
190
|
if results.status_code == requests.codes.ok:
|
|
103
|
-
fields =
|
|
191
|
+
fields = {}
|
|
104
192
|
for ob in results.json()["value"]:
|
|
105
193
|
field = f'{ob["shortName"]}.{ob["index"]}'
|
|
106
|
-
fields
|
|
194
|
+
fields[field] = ob["uri"]
|
|
107
195
|
return fields
|
|
108
196
|
elif results.status_code == requests.codes.unauthorized:
|
|
109
197
|
self.token = self.__token__()
|
|
@@ -112,52 +200,52 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
112
200
|
logger.error(f"indexed_fields failed with error code: {results.status_code}")
|
|
113
201
|
raise RuntimeError(results.status_code, f"indexed_fields failed with error code: {results.status_code}")
|
|
114
202
|
|
|
115
|
-
def simple_search_csv(self, query: str = "%", csv_file="search.csv",
|
|
116
|
-
|
|
117
|
-
if len(
|
|
203
|
+
def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
|
|
204
|
+
list_indexes: list = None):
|
|
205
|
+
if list_indexes is None or len(list_indexes) == 0:
|
|
118
206
|
metadata_fields = ["xip.reference", "xip.title", "xip.description", "xip.document_type",
|
|
119
207
|
"xip.parent_ref", "xip.security_descriptor"]
|
|
120
208
|
else:
|
|
121
|
-
metadata_fields = list(
|
|
209
|
+
metadata_fields = list(list_indexes)
|
|
122
210
|
if "xip.reference" not in metadata_fields:
|
|
123
211
|
metadata_fields.insert(0, "xip.reference")
|
|
124
212
|
with open(csv_file, newline='', mode="wt", encoding="utf-8") as csv_file:
|
|
125
213
|
writer = csv.DictWriter(csv_file, fieldnames=metadata_fields)
|
|
126
214
|
writer.writeheader()
|
|
127
|
-
writer.writerows(self.simple_search_list(query, page_size,
|
|
215
|
+
writer.writerows(self.simple_search_list(query, page_size, metadata_fields))
|
|
128
216
|
|
|
129
|
-
def simple_search_list(self, query: str = "%", page_size: int =
|
|
130
|
-
search_result = self.
|
|
217
|
+
def simple_search_list(self, query: str = "%", page_size: int = 50, list_indexes: list = None):
|
|
218
|
+
search_result = self._simple_search(query, 0, page_size, list_indexes)
|
|
131
219
|
for e in search_result.results_list:
|
|
132
220
|
yield e
|
|
133
221
|
found = len(search_result.results_list)
|
|
134
222
|
while search_result.hits > found:
|
|
135
|
-
search_result = self.
|
|
223
|
+
search_result = self._simple_search(query, found, page_size, list_indexes)
|
|
136
224
|
for e in search_result.results_list:
|
|
137
225
|
yield e
|
|
138
226
|
found = found + len(search_result.results_list)
|
|
139
227
|
|
|
140
|
-
def
|
|
228
|
+
def _simple_search(self, query: str = "%", start_index: int = 0, page_size: int = 10, list_indexes: list = None):
|
|
141
229
|
start_from = str(start_index)
|
|
142
230
|
headers = {'Content-Type': 'application/x-www-form-urlencoded', HEADER_TOKEN: self.token}
|
|
143
231
|
query_term = ('{ "q": "%s" }' % query)
|
|
144
|
-
if len(
|
|
232
|
+
if list_indexes is None or len(list_indexes) == 0:
|
|
145
233
|
metadata_fields = "xip.title,xip.description,xip.document_type,xip.parent_ref,xip.security_descriptor"
|
|
146
234
|
else:
|
|
147
|
-
metadata_fields = ','.join(
|
|
235
|
+
metadata_fields = ','.join(list_indexes)
|
|
148
236
|
payload = {'start': start_from, 'max': str(page_size), 'metadata': metadata_fields, 'q': query_term}
|
|
149
|
-
results =
|
|
150
|
-
|
|
237
|
+
results = self.session.post(f'{self.protocol}://{self.server}/api/content/search', data=payload,
|
|
238
|
+
headers=headers)
|
|
239
|
+
results_list = []
|
|
151
240
|
if results.status_code == requests.codes.ok:
|
|
152
|
-
|
|
153
|
-
metadata =
|
|
154
|
-
refs = list(
|
|
241
|
+
json_doc = results.json()
|
|
242
|
+
metadata = json_doc['value']['metadata']
|
|
243
|
+
refs = list(json_doc['value']['objectIds'])
|
|
155
244
|
refs = list(map(lambda x: content_api_identifier_to_type(x), refs))
|
|
156
|
-
hits = int(
|
|
245
|
+
hits = int(json_doc['value']['totalHits'])
|
|
157
246
|
|
|
158
247
|
for m_row, r_row in zip(metadata, refs):
|
|
159
|
-
results_map =
|
|
160
|
-
results_map['xip.reference'] = r_row[1]
|
|
248
|
+
results_map = {'xip.reference': r_row[1]}
|
|
161
249
|
for li in m_row:
|
|
162
250
|
results_map[li['name']] = li['value']
|
|
163
251
|
results_list.append(results_map)
|
|
@@ -171,65 +259,225 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
171
259
|
return search_results
|
|
172
260
|
elif results.status_code == requests.codes.unauthorized:
|
|
173
261
|
self.token = self.__token__()
|
|
174
|
-
return self.
|
|
262
|
+
return self._simple_search(query, start_index, page_size, list_indexes)
|
|
175
263
|
else:
|
|
176
264
|
logger.error(f"search failed with error code: {results.status_code}")
|
|
177
265
|
raise RuntimeError(results.status_code, f"simple_search failed with error code: {results.status_code}")
|
|
178
266
|
|
|
179
|
-
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
267
|
+
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
|
|
268
|
+
filter_values: dict = None,
|
|
269
|
+
sort_values: dict = None):
|
|
270
|
+
if filter_values is None:
|
|
271
|
+
filter_values = {}
|
|
272
|
+
if "xip.reference" not in filter_values:
|
|
273
|
+
filter_values["xip.reference"] = ""
|
|
185
274
|
|
|
186
|
-
header_fields = list(
|
|
275
|
+
header_fields = list(filter_values.keys())
|
|
187
276
|
index = header_fields.index("xip.reference")
|
|
188
277
|
header_fields.insert(0, header_fields.pop(index))
|
|
189
278
|
with open(csv_file, newline='', mode="wt", encoding="utf-8") as csv_file:
|
|
190
279
|
writer = csv.DictWriter(csv_file, fieldnames=header_fields)
|
|
191
280
|
writer.writeheader()
|
|
192
|
-
writer.writerows(self.search_index_filter_list(query, page_size,
|
|
281
|
+
writer.writerows(self.search_index_filter_list(query, page_size, filter_values, sort_values))
|
|
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
|
|
193
286
|
|
|
194
|
-
|
|
195
|
-
|
|
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)
|
|
196
297
|
for e in search_result.results_list:
|
|
197
298
|
yield e
|
|
198
299
|
found = len(search_result.results_list)
|
|
199
300
|
while search_result.hits > found:
|
|
200
|
-
search_result = self.
|
|
301
|
+
search_result = self._search_fields(query=query, fields=fields, start_index=found, page_size=page_size)
|
|
201
302
|
for e in search_result.results_list:
|
|
202
303
|
yield e
|
|
203
304
|
found = found + len(search_result.results_list)
|
|
204
305
|
|
|
205
|
-
def
|
|
306
|
+
def _search_fields(self, query: str = "%", fields: list[Field]=None, start_index: int = 0, page_size: int = 25):
|
|
307
|
+
|
|
206
308
|
start_from = str(start_index)
|
|
207
309
|
headers = {'Content-Type': 'application/x-www-form-urlencoded', HEADER_TOKEN: self.token}
|
|
208
310
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
|
|
374
|
+
def search_index_filter_list(self, query: str = "%", page_size: int = 25, filter_values: dict = None,
|
|
375
|
+
sort_values: dict = None) -> Generator:
|
|
376
|
+
"""
|
|
377
|
+
Run a search query with optional filters
|
|
378
|
+
|
|
379
|
+
:param query: The main search query.
|
|
380
|
+
:param page_size: The default search page size
|
|
381
|
+
:param filter_values: Dictionary of index names and values
|
|
382
|
+
:param sort_values: Dictionary of sort index names and values
|
|
383
|
+
:return: search result
|
|
384
|
+
"""
|
|
385
|
+
search_result = self._search_index_filter(query, 0, page_size, filter_values, sort_values)
|
|
386
|
+
for e in search_result.results_list:
|
|
387
|
+
yield e
|
|
388
|
+
found = len(search_result.results_list)
|
|
389
|
+
while search_result.hits > found:
|
|
390
|
+
search_result = self._search_index_filter(query, found, page_size, filter_values, sort_values)
|
|
391
|
+
for e in search_result.results_list:
|
|
392
|
+
yield e
|
|
393
|
+
found = found + len(search_result.results_list)
|
|
394
|
+
|
|
395
|
+
def search_index_filter_hits(self, query: str = "%", filter_values: dict = None) -> int:
|
|
396
|
+
"""
|
|
397
|
+
Run a search query with filters and return the number of hits only
|
|
398
|
+
|
|
399
|
+
:param query: The main search query.
|
|
400
|
+
:param filter_values: Dictionary of index names and values
|
|
401
|
+
:return: Number of search results
|
|
402
|
+
"""
|
|
403
|
+
start_from = str(0)
|
|
404
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded', HEADER_TOKEN: self.token}
|
|
405
|
+
|
|
406
|
+
field_list = []
|
|
407
|
+
for key, value in filter_values.items():
|
|
211
408
|
if value == "":
|
|
212
409
|
field_list.append('{' f' "name": "{key}", "values": [] ' + '}')
|
|
213
410
|
else:
|
|
214
|
-
|
|
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} ]' '}')
|
|
215
416
|
|
|
216
417
|
filter_terms = ','.join(field_list)
|
|
217
418
|
|
|
218
419
|
query_term = ('{ "q": "%s", "fields": [ %s ] }' % (query, filter_terms))
|
|
219
420
|
|
|
220
|
-
payload = {'start': start_from, 'max': str(
|
|
221
|
-
results =
|
|
222
|
-
|
|
421
|
+
payload = {'start': start_from, 'max': str(10), 'metadata': list(filter_values.keys()), 'q': query_term}
|
|
422
|
+
results = self.session.post(f'{self.protocol}://{self.server}/api/content/search', data=payload,
|
|
423
|
+
headers=headers)
|
|
223
424
|
if results.status_code == requests.codes.ok:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
425
|
+
json_doc = results.json()
|
|
426
|
+
return int(json_doc['value']['totalHits'])
|
|
427
|
+
elif results.status_code == requests.codes.unauthorized:
|
|
428
|
+
self.token = self.__token__()
|
|
429
|
+
return self.search_index_filter_hits(query, filter_values)
|
|
430
|
+
else:
|
|
431
|
+
logger.error(f"search failed with error code: {results.status_code}")
|
|
432
|
+
raise RuntimeError(results.status_code, f"_search_index_filter_hits failed")
|
|
433
|
+
|
|
434
|
+
def _search_index_filter(self, query: str = "%", start_index: int = 0, page_size: int = 25,
|
|
435
|
+
filter_values: dict = None, sort_values: dict = None):
|
|
436
|
+
start_from = str(start_index)
|
|
437
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded', HEADER_TOKEN: self.token}
|
|
438
|
+
|
|
439
|
+
if filter_values is None:
|
|
440
|
+
filter_values = {}
|
|
441
|
+
|
|
442
|
+
field_list = []
|
|
443
|
+
for key, value in filter_values.items():
|
|
444
|
+
if value == "":
|
|
445
|
+
field_list.append('{' f' "name": "{key}", "values": [] ' + '}')
|
|
446
|
+
else:
|
|
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} ]' '}')
|
|
452
|
+
|
|
453
|
+
filter_terms = ','.join(field_list)
|
|
454
|
+
|
|
455
|
+
if sort_values is None:
|
|
456
|
+
query_term = ('{ "q": "%s", "fields": [ %s ] }' % (query, filter_terms))
|
|
457
|
+
else:
|
|
458
|
+
sort_list = []
|
|
459
|
+
for key, value in sort_values.items():
|
|
460
|
+
direction = "asc"
|
|
461
|
+
if str(value).lower().startswith("d"):
|
|
462
|
+
direction = "desc"
|
|
463
|
+
sort_list.append(f'{{"sortFields": ["{key}"], "sortOrder": "{direction}"}}')
|
|
464
|
+
sort_terms = ','.join(sort_list)
|
|
465
|
+
query_term = ('{ "q": "%s", "fields": [ %s ], "sort": [ %s ]}' % (query, filter_terms, sort_terms))
|
|
466
|
+
|
|
467
|
+
payload = {'start': start_from, 'max': str(page_size), 'metadata': list(filter_values.keys()), 'q': query_term}
|
|
468
|
+
logger.debug(payload)
|
|
469
|
+
results = self.session.post(f'{self.protocol}://{self.server}/api/content/search', data=payload,
|
|
470
|
+
headers=headers)
|
|
471
|
+
results_list = []
|
|
472
|
+
if results.status_code == requests.codes.ok:
|
|
473
|
+
json_doc = results.json()
|
|
474
|
+
metadata = json_doc['value']['metadata']
|
|
475
|
+
refs = list(json_doc['value']['objectIds'])
|
|
227
476
|
refs = list(map(lambda x: content_api_identifier_to_type(x), refs))
|
|
228
|
-
hits = int(
|
|
477
|
+
hits = int(json_doc['value']['totalHits'])
|
|
229
478
|
|
|
230
479
|
for m_row, r_row in zip(metadata, refs):
|
|
231
|
-
results_map =
|
|
232
|
-
results_map['xip.reference'] = r_row[1]
|
|
480
|
+
results_map = {'xip.reference': r_row[1]}
|
|
233
481
|
for li in m_row:
|
|
234
482
|
results_map[li['name']] = li['value']
|
|
235
483
|
results_list.append(results_map)
|
|
@@ -243,7 +491,25 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
243
491
|
return search_results
|
|
244
492
|
elif results.status_code == requests.codes.unauthorized:
|
|
245
493
|
self.token = self.__token__()
|
|
246
|
-
return self.
|
|
494
|
+
return self._search_index_filter(query, start_index, page_size, filter_values)
|
|
247
495
|
else:
|
|
248
496
|
logger.error(f"search failed with error code: {results.status_code}")
|
|
249
497
|
raise RuntimeError(results.status_code, f"search_index_filter failed")
|
|
498
|
+
|
|
499
|
+
class ReportProgressCallBack:
|
|
500
|
+
def __init__(self):
|
|
501
|
+
self.current = 0
|
|
502
|
+
self.total = 0
|
|
503
|
+
self._lock = threading.Lock()
|
|
504
|
+
|
|
505
|
+
def __call__(self, value):
|
|
506
|
+
with self._lock:
|
|
507
|
+
values = value.split(":")
|
|
508
|
+
self.total = int(values[1])
|
|
509
|
+
self.current = int(values[0])
|
|
510
|
+
if self.total == 0:
|
|
511
|
+
percentage = 100.0
|
|
512
|
+
else:
|
|
513
|
+
percentage = (self.current / self.total) * 100
|
|
514
|
+
sys.stdout.write("\rProcessing Hits %s from %s (%.2f%%)" % (self.current, self.total, percentage))
|
|
515
|
+
sys.stdout.flush()
|