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/entityAPI.py
CHANGED
|
@@ -9,11 +9,14 @@ licence: Apache License 2.0
|
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import os.path
|
|
12
13
|
import uuid
|
|
14
|
+
import xml.etree.ElementTree
|
|
13
15
|
from datetime import datetime, timedelta, timezone
|
|
16
|
+
from io import BytesIO
|
|
14
17
|
from time import sleep
|
|
15
|
-
import
|
|
16
|
-
|
|
18
|
+
from typing import Any, Generator, Tuple, Iterable, Union, Callable
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
from pyPreservica.common import *
|
|
19
22
|
|
|
@@ -22,116 +25,171 @@ logger = logging.getLogger(__name__)
|
|
|
22
25
|
|
|
23
26
|
class EntityAPI(AuthenticatedAPI):
|
|
24
27
|
"""
|
|
25
|
-
A
|
|
26
|
-
https://us.preservica.com/api/entity/documentation.html
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Methods
|
|
30
|
-
-------
|
|
31
|
-
asset(reference):
|
|
32
|
-
Fetches the main XIP attributes for an asset by its reference
|
|
28
|
+
A class for the Preservica Repository web services Entity API
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
Fetches the main XIP attributes for a folder by its reference
|
|
36
|
-
|
|
37
|
-
content_object(reference):
|
|
38
|
-
Fetches the main XIP attributes for a content_object by its reference
|
|
30
|
+
https://us.preservica.com/api/entity/documentation.html
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
Get an entity by its type and reference
|
|
32
|
+
The EntityAPI allows users to interact with the Preservica repository
|
|
42
33
|
|
|
43
|
-
metadata(uri):
|
|
44
|
-
Return the descriptive metadata attached to an entity.
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
Return the metadata fragment for the entity by schema
|
|
35
|
+
"""
|
|
48
36
|
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
38
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
39
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
42
|
+
protocol, request_hook, credentials_path)
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
45
|
+
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
def user_security_tags(self, with_permissions: bool = False) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Return security tags available for the current user
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
:param with_permissions: Return the permissions for each security tag
|
|
52
|
+
:type with_permissions: bool
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
:return: dict of security tags
|
|
55
|
+
:rtype: dict
|
|
56
|
+
"""
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
deletes identifiers which belong to an entity
|
|
58
|
+
return self.security_tags_base(with_permissions=with_permissions)
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
def bitstream_chunks(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Generator:
|
|
61
|
+
"""
|
|
62
|
+
Generator function to return bitstream chunks, allows the clients to
|
|
63
|
+
process chunks as they are downloaded.
|
|
64
|
+
|
|
65
|
+
:param bitstream: A bitstream object
|
|
66
|
+
:type url: Bitstream
|
|
67
|
+
:param chunk_size: Optional size of the chunks to be downloaded
|
|
68
|
+
:type chunk_size: int
|
|
69
|
+
:return: Iterator
|
|
70
|
+
:rtype: Generator
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(bitstream, Bitstream):
|
|
73
|
+
logger.error("bitstream_content argument is not a Bitstream object")
|
|
74
|
+
raise RuntimeError("bitstream_bytes argument is not a Bitstream object")
|
|
75
|
+
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
76
|
+
if request.status_code == requests.codes.unauthorized:
|
|
77
|
+
self.token = self.__token__()
|
|
78
|
+
yield from self.bitstream_chunks(bitstream)
|
|
79
|
+
elif request.status_code == requests.codes.ok:
|
|
80
|
+
for chunk in request.iter_content(chunk_size=chunk_size):
|
|
81
|
+
yield chunk
|
|
82
|
+
else:
|
|
83
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_chunks",
|
|
84
|
+
request.content.decode('utf-8'))
|
|
85
|
+
logger.error(exception)
|
|
86
|
+
raise exception
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
def bitstream_bytes(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Union[BytesIO, None]:
|
|
89
|
+
"""
|
|
90
|
+
Download a file represented as a Bitstream to a byteIO array
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Tests - Yes
|
|
92
|
+
Returns the byteIO
|
|
93
|
+
Returns None if the file does not contain the correct number of bytes (default 2k)
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
96
|
+
:param bitstream: A Bitstream object
|
|
97
|
+
:type bitstream: Bitstream
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
:return: The file in bytes
|
|
100
|
+
:rtype: byteIO
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(bitstream, Bitstream):
|
|
103
|
+
logger.error("bitstream_content argument is not a Bitstream object")
|
|
104
|
+
raise RuntimeError("bitstream_bytes argument is not a Bitstream object")
|
|
105
|
+
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as response:
|
|
106
|
+
if response.status_code == requests.codes.unauthorized:
|
|
107
|
+
self.token = self.__token__()
|
|
108
|
+
return self.bitstream_bytes(bitstream)
|
|
109
|
+
elif response.status_code == requests.codes.ok:
|
|
110
|
+
file_bytes = BytesIO()
|
|
111
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
112
|
+
file_bytes.write(chunk)
|
|
113
|
+
file_bytes.seek(0)
|
|
114
|
+
if file_bytes.getbuffer().nbytes == bitstream.length:
|
|
115
|
+
logger.debug(f"Downloaded {bitstream.length} bytes from {bitstream.filename}")
|
|
116
|
+
return file_bytes
|
|
117
|
+
else:
|
|
118
|
+
logger.error("Downloaded file size did not match the Preservica held value")
|
|
119
|
+
return None
|
|
120
|
+
else:
|
|
121
|
+
exception = HTTPException(bitstream.filename, response.status_code, response.url, "bitstream_bytes",
|
|
122
|
+
response.content.decode('utf-8'))
|
|
123
|
+
logger.error(exception)
|
|
124
|
+
raise exception
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
def bitstream_location(self, bitstream: Bitstream) -> list:
|
|
127
|
+
""""
|
|
128
|
+
Retrieves information about a bitstreams storage locations
|
|
88
129
|
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
:param Bitstream bitstream: The bitstream object
|
|
131
|
+
:return: A list of strings containing all the storage locations of this bitstream
|
|
132
|
+
:rtype: list
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
"""
|
|
135
|
+
if not isinstance(bitstream, Bitstream):
|
|
136
|
+
logger.error("bitstream argument is not a Bitstream object")
|
|
137
|
+
raise RuntimeError("bitstream argument is not a Bitstream object")
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
Blocking call to change security tag
|
|
139
|
+
storage_locations = []
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
Return a set of representations for the asset
|
|
141
|
+
url: str = f'{self.protocol}://{self.server}/api/entity/content-objects/{bitstream.co_ref}/generations/{bitstream.gen_index}/bitstreams/{bitstream.bs_index}/storage-locations'
|
|
100
142
|
|
|
101
|
-
|
|
102
|
-
|
|
143
|
+
with self.session.get(url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
144
|
+
if request.status_code == requests.codes.ok:
|
|
145
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
146
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
147
|
+
logger.debug(xml_response)
|
|
148
|
+
locations = entity_response.find(f'.//{{{self.entity_ns}}}StorageLocation')
|
|
149
|
+
for adapter in locations:
|
|
150
|
+
storage_locations.append(adapter.attrib['name'])
|
|
151
|
+
return storage_locations
|
|
103
152
|
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
if request.status_code == requests.codes.unauthorized:
|
|
154
|
+
self.token = self.__token__()
|
|
155
|
+
return self.bitstream_location(bitstream)
|
|
156
|
+
else:
|
|
157
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_location",
|
|
158
|
+
request.content.decode('utf-8'))
|
|
159
|
+
logger.error(exception)
|
|
160
|
+
raise exception
|
|
106
161
|
|
|
107
|
-
"""
|
|
108
162
|
|
|
109
|
-
def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False):
|
|
110
|
-
super().__init__(username, password, tenant, server, use_shared_secret)
|
|
111
|
-
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
112
|
-
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
113
163
|
|
|
114
|
-
def bitstream_content(self, bitstream: Bitstream, filename: str):
|
|
164
|
+
def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
|
|
115
165
|
"""
|
|
116
166
|
Download a file represented as a Bitstream to a local filename
|
|
117
167
|
|
|
118
168
|
Returns the number of bytes written to the file
|
|
119
169
|
Returns None if the file does not contain the correct number of bytes
|
|
120
170
|
|
|
171
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
121
172
|
:param bitstream: A Bitstream object
|
|
173
|
+
:type bitstream: Bitstream
|
|
174
|
+
|
|
122
175
|
:param filename: The filename to write the bytes to
|
|
176
|
+
:type filename: str
|
|
177
|
+
|
|
178
|
+
:return: The size of the file in bytes
|
|
179
|
+
:rtype: int
|
|
180
|
+
|
|
123
181
|
"""
|
|
124
182
|
|
|
125
183
|
if not isinstance(bitstream, Bitstream):
|
|
126
184
|
logger.error("bitstream_content argument is not a Bitstream object")
|
|
127
185
|
raise RuntimeError("bitstream_content argument is not a Bitstream object")
|
|
128
|
-
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as
|
|
129
|
-
if
|
|
186
|
+
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
187
|
+
if request.status_code == requests.codes.unauthorized:
|
|
130
188
|
self.token = self.__token__()
|
|
131
189
|
return self.bitstream_content(bitstream, filename)
|
|
132
|
-
elif
|
|
190
|
+
elif request.status_code == requests.codes.ok:
|
|
133
191
|
with open(filename, 'wb') as file:
|
|
134
|
-
for chunk in
|
|
192
|
+
for chunk in request.iter_content(chunk_size=chunk_size):
|
|
135
193
|
file.write(chunk)
|
|
136
194
|
file.flush()
|
|
137
195
|
if os.path.getsize(filename) == bitstream.length:
|
|
@@ -142,15 +200,25 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
142
200
|
os.remove(filename)
|
|
143
201
|
return None
|
|
144
202
|
else:
|
|
145
|
-
|
|
146
|
-
|
|
203
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_content",
|
|
204
|
+
request.content.decode('utf-8'))
|
|
205
|
+
logger.error(exception)
|
|
206
|
+
raise exception
|
|
147
207
|
|
|
148
|
-
def download_opex(self, pid: str):
|
|
208
|
+
def download_opex(self, pid: str) -> str:
|
|
149
209
|
"""
|
|
150
|
-
Download
|
|
210
|
+
Download a completed OPEX export using the workflow process ID
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
:param pid: A process id which identifiers the export workflow
|
|
214
|
+
:type pid: str
|
|
215
|
+
|
|
216
|
+
:return: The downloaded zip file name
|
|
217
|
+
:rtype: str
|
|
218
|
+
|
|
151
219
|
"""
|
|
152
220
|
headers = {HEADER_TOKEN: self.__token__(), 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
153
|
-
download = self.session.get(f'
|
|
221
|
+
download = self.session.get(f'{self.protocol}://{self.server}/api/entity/actions/exports/{pid}/content',
|
|
154
222
|
stream=True, headers=headers)
|
|
155
223
|
if download.status_code == requests.codes.ok:
|
|
156
224
|
with open(f'{pid}.zip', 'wb') as file:
|
|
@@ -163,14 +231,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
163
231
|
self.token = self.__token__()
|
|
164
232
|
return self.download_opex(pid)
|
|
165
233
|
else:
|
|
166
|
-
|
|
167
|
-
|
|
234
|
+
exception = HTTPException(pid, download.status_code, download.url, "download_opex",
|
|
235
|
+
download.content.decode('utf-8'))
|
|
236
|
+
logger.error(exception)
|
|
237
|
+
raise exception
|
|
168
238
|
|
|
169
|
-
def __export_opex_start__(self, entity: Entity, **kwargs):
|
|
239
|
+
def __export_opex_start__(self, entity: Entity, **kwargs) -> str:
|
|
170
240
|
"""
|
|
171
241
|
Initiates export of the entity and downloads the opex package
|
|
172
242
|
|
|
173
|
-
By default includes content, metadata with the latest active generations
|
|
243
|
+
By default, includes content, metadata with the latest active generations
|
|
174
244
|
and the parent hierarchy.
|
|
175
245
|
|
|
176
246
|
Arguments are kwargs map
|
|
@@ -193,25 +263,25 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
193
263
|
|
|
194
264
|
include_content = "Content"
|
|
195
265
|
if 'IncludeContent' in kwargs:
|
|
196
|
-
value = kwargs.get("IncludeContent").
|
|
266
|
+
value = str(kwargs.get("IncludeContent")).strip()
|
|
197
267
|
if value.casefold() in map(str.casefold, include_content_options):
|
|
198
268
|
include_content = value
|
|
199
269
|
|
|
200
270
|
include_metadata = "Metadata"
|
|
201
271
|
if 'IncludeMetadata' in kwargs:
|
|
202
|
-
value = kwargs.get("IncludeMetadata").
|
|
272
|
+
value = str(kwargs.get("IncludeMetadata")).strip()
|
|
203
273
|
if value.casefold() in map(str.casefold, include_metadata_options):
|
|
204
274
|
include_metadata = value
|
|
205
275
|
|
|
206
276
|
include_generation = "All"
|
|
207
277
|
if 'IncludedGenerations' in kwargs:
|
|
208
|
-
value = kwargs.get("IncludedGenerations").
|
|
278
|
+
value = str(kwargs.get("IncludedGenerations")).strip()
|
|
209
279
|
if value.casefold() in map(str.casefold, include_generation_options):
|
|
210
280
|
include_generation = value
|
|
211
281
|
|
|
212
282
|
include_parent = "true"
|
|
213
283
|
if 'IncludeParentHierarchy' in kwargs:
|
|
214
|
-
value = kwargs.get("IncludeParentHierarchy").
|
|
284
|
+
value = str(kwargs.get("IncludeParentHierarchy")).strip()
|
|
215
285
|
if value.casefold() in map(str.casefold, include_parent_options):
|
|
216
286
|
include_parent = value
|
|
217
287
|
|
|
@@ -224,29 +294,53 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
224
294
|
|
|
225
295
|
logger.debug(xml_request)
|
|
226
296
|
|
|
227
|
-
request = self.session.post(
|
|
228
|
-
|
|
297
|
+
request = self.session.post(
|
|
298
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/exports',
|
|
299
|
+
headers=headers, data=xml_request)
|
|
229
300
|
|
|
230
301
|
if request.status_code == requests.codes.accepted:
|
|
231
302
|
return str(request.content.decode('utf-8'))
|
|
232
303
|
elif request.status_code == requests.codes.unauthorized:
|
|
233
304
|
self.token = self.__token__()
|
|
234
|
-
return self.__export_opex_start__(entity
|
|
305
|
+
return self.__export_opex_start__(entity, IncludeContent=include_content,
|
|
306
|
+
IncludeMetadata=include_metadata, IncludedGenerations=include_generation,
|
|
307
|
+
IncludeParentHierarchy=include_parent)
|
|
235
308
|
else:
|
|
236
|
-
|
|
237
|
-
|
|
309
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "__export_opex_start__",
|
|
310
|
+
request.content.decode('utf-8'))
|
|
311
|
+
logger.error(exception)
|
|
312
|
+
raise exception
|
|
238
313
|
|
|
239
|
-
def export_opex_async(self, entity: Entity, **kwargs):
|
|
314
|
+
def export_opex_async(self, entity: Entity, **kwargs) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Initiates export of the entity returns an id to track progress
|
|
317
|
+
"""
|
|
240
318
|
return self.__export_opex_start__(entity, **kwargs)
|
|
241
319
|
|
|
242
|
-
def export_opex_sync(self, entity: Entity, **kwargs):
|
|
320
|
+
def export_opex_sync(self, entity: Entity, **kwargs) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Initiates export of the entity and downloads the opex package
|
|
323
|
+
Blocks until the package is downloaded
|
|
324
|
+
|
|
325
|
+
By default, includes content, metadata with the latest active generations
|
|
326
|
+
and the parent hierarchy.
|
|
327
|
+
|
|
328
|
+
Arguments are kwargs map
|
|
329
|
+
|
|
330
|
+
IncludeContent
|
|
331
|
+
IncludeMetadata
|
|
332
|
+
IncludedGenerations
|
|
333
|
+
IncludeParentHierarchy
|
|
334
|
+
|
|
335
|
+
"""
|
|
243
336
|
return self.export_opex(entity, **kwargs)
|
|
244
337
|
|
|
245
|
-
def export_opex(self, entity: Entity, **kwargs):
|
|
338
|
+
def export_opex(self, entity: Entity, **kwargs) -> str:
|
|
246
339
|
"""
|
|
247
340
|
Initiates export of the entity and downloads the opex package
|
|
341
|
+
Blocks until the package is downloaded
|
|
248
342
|
|
|
249
|
-
By default includes content, metadata with the latest active generations
|
|
343
|
+
By default, includes content, metadata with the latest active generations
|
|
250
344
|
and the parent hierarchy.
|
|
251
345
|
|
|
252
346
|
Arguments are kwargs map
|
|
@@ -256,6 +350,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
256
350
|
IncludedGenerations
|
|
257
351
|
IncludeParentHierarchy
|
|
258
352
|
|
|
353
|
+
|
|
354
|
+
:param Entity entity: The entity to export Asset or Folder
|
|
355
|
+
:param str IncludeContent: "Content", "NoContent"
|
|
356
|
+
:param str IncludeMetadata: "Metadata", "NoMetadata", "MetadataWithEvents"
|
|
357
|
+
:param str IncludedGenerations: "LatestActive", "AllActive", "All"
|
|
358
|
+
:param str IncludeParentHierarchy: "true", "false"
|
|
359
|
+
:return: The path to the opex ZIP file
|
|
360
|
+
:rtype: str
|
|
361
|
+
|
|
259
362
|
"""
|
|
260
363
|
status = "ACTIVE"
|
|
261
364
|
pid = self.__export_opex_start__(entity, **kwargs)
|
|
@@ -268,75 +371,107 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
268
371
|
logger.error(status)
|
|
269
372
|
raise RuntimeError(f"export progress failed {status}")
|
|
270
373
|
|
|
271
|
-
def download(self, entity: Entity, filename: str):
|
|
374
|
+
def download(self, entity: Entity, filename: str) -> str:
|
|
272
375
|
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
Returns the filename of the new file
|
|
376
|
+
Download the first generation of the access representation of an asset
|
|
276
377
|
|
|
277
|
-
|
|
278
|
-
|
|
378
|
+
:param Entity entity: The entity
|
|
379
|
+
:param str filename: The file the image is written to
|
|
380
|
+
:return: The filename
|
|
381
|
+
:rtype: str
|
|
279
382
|
"""
|
|
280
383
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
281
384
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}'}
|
|
282
|
-
with self.session.get(f'
|
|
283
|
-
stream=True) as
|
|
284
|
-
if
|
|
385
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/download', params=params, headers=headers,
|
|
386
|
+
stream=True) as request:
|
|
387
|
+
if request.status_code == requests.codes.ok:
|
|
285
388
|
with open(filename, 'wb') as file:
|
|
286
|
-
for chunk in
|
|
389
|
+
for chunk in request.iter_content(chunk_size=CHUNK_SIZE):
|
|
287
390
|
file.write(chunk)
|
|
288
391
|
file.flush()
|
|
289
392
|
return filename
|
|
290
|
-
elif
|
|
393
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
291
394
|
self.token = self.__token__()
|
|
292
395
|
return self.download(entity, filename)
|
|
293
396
|
else:
|
|
294
|
-
|
|
295
|
-
|
|
397
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "download",
|
|
398
|
+
request.content.decode('utf-8'))
|
|
399
|
+
logger.error(exception)
|
|
400
|
+
raise exception
|
|
296
401
|
|
|
297
|
-
def
|
|
402
|
+
def has_thumbnail(self, entity: Entity) -> bool:
|
|
298
403
|
"""
|
|
299
|
-
|
|
404
|
+
Does the entity have a thumbnail image attached
|
|
405
|
+
Returns false if the entity has no thumbnail
|
|
300
406
|
|
|
301
|
-
|
|
407
|
+
:param entity: The entity
|
|
408
|
+
"""
|
|
409
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
410
|
+
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}', 'size': f'{Thumbnail.SMALL.value}'}
|
|
411
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/thumbnail', params=params,
|
|
412
|
+
headers=headers) as request:
|
|
413
|
+
if request.status_code == requests.codes.ok:
|
|
414
|
+
return True
|
|
415
|
+
if request.status_code == requests.codes.not_found:
|
|
416
|
+
return False
|
|
417
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
418
|
+
self.token = self.__token__()
|
|
419
|
+
return self.has_thumbnail(entity)
|
|
420
|
+
else:
|
|
421
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "has_thumbnail",
|
|
422
|
+
request.content.decode('utf-8'))
|
|
423
|
+
logger.error(exception)
|
|
424
|
+
raise exception
|
|
302
425
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
426
|
+
def thumbnail(self, entity: Entity, filename: str, size=Thumbnail.LARGE):
|
|
427
|
+
"""
|
|
428
|
+
Get the thumbnail image for an asset or folder
|
|
429
|
+
|
|
430
|
+
:param Entity entity: The entity
|
|
431
|
+
:param str filename: The file the image is written to
|
|
432
|
+
:param Thumbnail size: The size of the thumbnail image
|
|
433
|
+
:return: The filename
|
|
434
|
+
:rtype: str
|
|
306
435
|
"""
|
|
307
436
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
308
437
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}', 'size': f'{size.value}'}
|
|
309
|
-
with self.session.get(f'
|
|
310
|
-
|
|
438
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/thumbnail', params=params,
|
|
439
|
+
headers=headers, stream=True) as request:
|
|
440
|
+
if request.status_code == requests.codes.ok:
|
|
311
441
|
with open(filename, 'wb') as file:
|
|
312
|
-
for chunk in
|
|
442
|
+
for chunk in request.iter_content(chunk_size=CHUNK_SIZE):
|
|
313
443
|
file.write(chunk)
|
|
314
444
|
file.flush()
|
|
315
445
|
return filename
|
|
316
|
-
elif
|
|
446
|
+
elif request.status_code == requests.codes.not_found:
|
|
447
|
+
return None
|
|
448
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
317
449
|
self.token = self.__token__()
|
|
318
450
|
return self.thumbnail(entity, filename, size=size)
|
|
319
451
|
else:
|
|
320
|
-
|
|
321
|
-
|
|
452
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "thumbnail",
|
|
453
|
+
request.content.decode('utf-8'))
|
|
454
|
+
logger.error(exception)
|
|
455
|
+
raise exception
|
|
322
456
|
|
|
323
457
|
def delete_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
324
458
|
"""
|
|
325
|
-
|
|
459
|
+
Delete identifiers on an Entity object
|
|
326
460
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
461
|
+
:param Entity entity: The entity the identifiers are deleted from
|
|
462
|
+
:param str identifier_type: The identifier type
|
|
463
|
+
:param str identifier_value: The identifier value
|
|
464
|
+
:return: entity
|
|
465
|
+
:rtype: Entity
|
|
466
|
+
"""
|
|
333
467
|
|
|
334
468
|
if (self.major_version < 7) and (self.minor_version < 1):
|
|
335
469
|
raise RuntimeError("delete_identifiers API call is not available when connected to a v6.0 System")
|
|
336
470
|
|
|
337
471
|
headers = {HEADER_TOKEN: self.token}
|
|
338
|
-
request = self.session.get(
|
|
339
|
-
|
|
472
|
+
request = self.session.get(
|
|
473
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
474
|
+
headers=headers)
|
|
340
475
|
if request.status_code == requests.codes.ok:
|
|
341
476
|
xml_response = str(request.content.decode('utf-8'))
|
|
342
477
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
@@ -354,7 +489,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
354
489
|
_aipid = identifier.text
|
|
355
490
|
if _ref == entity.reference and _type == identifier_type and _value == identifier_value:
|
|
356
491
|
del_req = self.session.delete(
|
|
357
|
-
f'
|
|
492
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers/{_aipid}',
|
|
358
493
|
headers=headers)
|
|
359
494
|
if del_req.status_code == requests.codes.unauthorized:
|
|
360
495
|
self.token = self.__token__()
|
|
@@ -371,17 +506,66 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
371
506
|
logger.error(request)
|
|
372
507
|
raise RuntimeError(request.status_code, "delete_identifier failed")
|
|
373
508
|
|
|
374
|
-
def
|
|
509
|
+
def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
|
|
375
510
|
"""
|
|
376
|
-
|
|
511
|
+
Get all external identifiers on an entity
|
|
512
|
+
|
|
513
|
+
Returns the set of external identifiers on the entity
|
|
514
|
+
|
|
515
|
+
:param entity: The Entity (Asset or Folder)
|
|
516
|
+
:param external_identifier_type: Optional identifier type to filter the results
|
|
517
|
+
:type entity: Entity
|
|
518
|
+
"""
|
|
519
|
+
headers = {HEADER_TOKEN: self.token}
|
|
520
|
+
request = self.session.get(
|
|
521
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
522
|
+
headers=headers)
|
|
523
|
+
if request.status_code == requests.codes.ok:
|
|
524
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
525
|
+
logger.debug(xml_response)
|
|
526
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
527
|
+
identifier_list = entity_response.findall(f'.//{{{self.xip_ns}}}Identifier')
|
|
528
|
+
result = set()
|
|
529
|
+
for identifier in identifier_list:
|
|
530
|
+
identifier_value = identifier_type = identifier_id = ""
|
|
531
|
+
for child in identifier:
|
|
532
|
+
if child.tag.endswith("Type"):
|
|
533
|
+
identifier_type = child.text
|
|
534
|
+
if child.tag.endswith("Value"):
|
|
535
|
+
identifier_value = child.text
|
|
536
|
+
if child.tag.endswith("ApiId"):
|
|
537
|
+
identifier_id = child.text
|
|
538
|
+
if external_identifier_type is None:
|
|
539
|
+
external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
|
|
540
|
+
external_id.identifier_id = identifier_id
|
|
541
|
+
result.add(external_id)
|
|
542
|
+
else:
|
|
543
|
+
if identifier_type == external_identifier_type:
|
|
544
|
+
external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
|
|
545
|
+
external_id.identifier_id = identifier_id
|
|
546
|
+
result.add(external_id)
|
|
547
|
+
return result
|
|
548
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
549
|
+
self.token = self.__token__()
|
|
550
|
+
return self.entity_identifiers(entity)
|
|
551
|
+
else:
|
|
552
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "identifiers_for_entity",
|
|
553
|
+
request.content.decode('utf-8'))
|
|
554
|
+
logger.error(exception)
|
|
555
|
+
raise exception
|
|
377
556
|
|
|
378
|
-
|
|
557
|
+
def identifiers_for_entity(self, entity: Entity) -> set[Tuple]:
|
|
558
|
+
"""
|
|
559
|
+
Return a set of identifiers which belong to the entity
|
|
379
560
|
|
|
380
|
-
|
|
561
|
+
:param Entity entity: The entity
|
|
562
|
+
:return: Set of identifiers as tuples
|
|
563
|
+
:rtype: set(Tuple)
|
|
381
564
|
"""
|
|
382
565
|
headers = {HEADER_TOKEN: self.token}
|
|
383
|
-
request = self.session.get(
|
|
384
|
-
|
|
566
|
+
request = self.session.get(
|
|
567
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
568
|
+
headers=headers)
|
|
385
569
|
if request.status_code == requests.codes.ok:
|
|
386
570
|
xml_response = str(request.content.decode('utf-8'))
|
|
387
571
|
logger.debug(xml_response)
|
|
@@ -401,21 +585,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
401
585
|
self.token = self.__token__()
|
|
402
586
|
return self.identifiers_for_entity(entity)
|
|
403
587
|
else:
|
|
404
|
-
|
|
405
|
-
|
|
588
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "identifiers_for_entity",
|
|
589
|
+
request.content.decode('utf-8'))
|
|
590
|
+
logger.error(exception)
|
|
591
|
+
raise exception
|
|
406
592
|
|
|
407
|
-
def identifier(self, identifier_type: str, identifier_value: str):
|
|
593
|
+
def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
|
|
408
594
|
"""
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
Returns the set of entities which have the external identifier
|
|
595
|
+
Return a set of entities with external identifiers which match the type and value
|
|
412
596
|
|
|
413
|
-
|
|
414
|
-
|
|
597
|
+
:param str identifier_type: The identifier type
|
|
598
|
+
:param str identifier_value: The identifier value
|
|
599
|
+
:return: Set of entity objects which have a reference and title attribute
|
|
600
|
+
:rtype: set(Entity)
|
|
415
601
|
"""
|
|
416
602
|
headers = {HEADER_TOKEN: self.token}
|
|
417
603
|
payload = {'type': identifier_type, 'value': identifier_value}
|
|
418
|
-
request = self.session.get(f'
|
|
604
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/entities/by-identifier', params=payload,
|
|
419
605
|
headers=headers)
|
|
420
606
|
if request.status_code == requests.codes.ok:
|
|
421
607
|
xml_response = str(request.content.decode('utf-8'))
|
|
@@ -425,33 +611,34 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
425
611
|
result = set()
|
|
426
612
|
for entity in entity_list:
|
|
427
613
|
if entity.attrib['type'] == EntityType.FOLDER.value:
|
|
428
|
-
|
|
429
|
-
result.add(
|
|
614
|
+
folder = Folder(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
615
|
+
result.add(folder)
|
|
430
616
|
elif entity.attrib['type'] == EntityType.ASSET.value:
|
|
431
|
-
|
|
432
|
-
result.add(
|
|
617
|
+
asset = Asset(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
618
|
+
result.add(asset)
|
|
433
619
|
elif entity.attrib['type'] == EntityType.CONTENT_OBJECT.value:
|
|
434
|
-
|
|
435
|
-
result.add(
|
|
620
|
+
co = ContentObject(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
621
|
+
result.add(co)
|
|
436
622
|
return result
|
|
437
623
|
elif request.status_code == requests.codes.unauthorized:
|
|
438
624
|
self.token = self.__token__()
|
|
439
625
|
return self.identifier(identifier_type, identifier_value)
|
|
440
626
|
else:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
627
|
+
exception = HTTPException(payload, request.status_code, request.url, "identifier",
|
|
628
|
+
request.content.decode('utf-8'))
|
|
629
|
+
logger.error(exception)
|
|
630
|
+
raise exception
|
|
444
631
|
|
|
445
632
|
def add_identifier(self, entity: Entity, identifier_type: str, identifier_value: str):
|
|
446
633
|
"""
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
Returns the internal identifier DB key
|
|
634
|
+
Add a new external identifier to an Entity object
|
|
450
635
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
636
|
+
:param Entity entity: The entity the identifier is added to
|
|
637
|
+
:param str identifier_type: The identifier type
|
|
638
|
+
:param str identifier_value: The identifier value
|
|
639
|
+
:return: An internal id for this external identifier
|
|
640
|
+
:rtype: str
|
|
641
|
+
"""
|
|
455
642
|
|
|
456
643
|
if self.major_version < 7 and self.minor_version < 1:
|
|
457
644
|
raise RuntimeError("add_identifier API call is not available when connected to a v6.0 System")
|
|
@@ -465,7 +652,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
465
652
|
end_point = f"/{entity.path}/{entity.reference}/identifiers"
|
|
466
653
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
467
654
|
logger.debug(xml_request)
|
|
468
|
-
request = self.session.post(f'
|
|
655
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
656
|
+
headers=headers)
|
|
469
657
|
if request.status_code == requests.codes.ok:
|
|
470
658
|
xml_string = str(request.content.decode("utf-8"))
|
|
471
659
|
identifier_response = xml.etree.ElementTree.fromstring(xml_string)
|
|
@@ -478,17 +666,276 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
478
666
|
self.token = self.__token__()
|
|
479
667
|
return self.add_identifier(entity, identifier_type, identifier_value)
|
|
480
668
|
else:
|
|
481
|
-
|
|
669
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "add_identifier",
|
|
670
|
+
request.content.decode('utf-8'))
|
|
671
|
+
logger.error(exception)
|
|
672
|
+
raise exception
|
|
482
673
|
|
|
483
|
-
def
|
|
674
|
+
def update_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
484
675
|
"""
|
|
485
|
-
|
|
676
|
+
Update external identifiers based on Entity and Type
|
|
486
677
|
|
|
487
|
-
|
|
678
|
+
Returns the internal identifier DB Key
|
|
679
|
+
|
|
680
|
+
:param entity: The entity to delete identifiers from
|
|
681
|
+
:param identifier_type: The type of the identifier to delete.
|
|
682
|
+
:param identifier_value: The value of the identifier to delete.
|
|
683
|
+
"""
|
|
488
684
|
|
|
489
|
-
|
|
490
|
-
|
|
685
|
+
if (self.major_version < 7) and (self.minor_version < 1):
|
|
686
|
+
raise RuntimeError("update_identifiers API call is not available when connected to a v6.0 System")
|
|
687
|
+
|
|
688
|
+
headers = {HEADER_TOKEN: self.token}
|
|
689
|
+
response = self.session.get(
|
|
690
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
691
|
+
headers=headers)
|
|
692
|
+
|
|
693
|
+
if response.status_code == requests.codes.ok:
|
|
694
|
+
xml_response = str(response.content.decode('utf-8'))
|
|
695
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
696
|
+
identifier_list = entity_response.findall(f'.//{{{self.xip_ns}}}Identifier')
|
|
697
|
+
for identifier_element in identifier_list:
|
|
698
|
+
_ref = _type = _value = _aipid = None
|
|
699
|
+
for identifier in identifier_element:
|
|
700
|
+
if identifier.tag.endswith("Entity"):
|
|
701
|
+
_ref = identifier.text
|
|
702
|
+
if identifier.tag.endswith("Type") and identifier_type is not None:
|
|
703
|
+
_type = identifier.text
|
|
704
|
+
if identifier.tag.endswith("Value") and identifier_value is not None:
|
|
705
|
+
_value = identifier.text
|
|
706
|
+
if identifier.tag.endswith("ApiId"):
|
|
707
|
+
_aipid = identifier.text
|
|
708
|
+
if _ref == entity.reference and _type == identifier_type:
|
|
709
|
+
|
|
710
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
711
|
+
|
|
712
|
+
xml_object = xml.etree.ElementTree.Element('Identifier', {"xmlns": self.xip_ns})
|
|
713
|
+
xml.etree.ElementTree.SubElement(xml_object, "Type").text = identifier_type
|
|
714
|
+
xml.etree.ElementTree.SubElement(xml_object, "Value").text = identifier_value
|
|
715
|
+
xml.etree.ElementTree.SubElement(xml_object, "Entity").text = entity.reference
|
|
716
|
+
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
717
|
+
|
|
718
|
+
put_response = self.session.put(
|
|
719
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers/{_aipid}',
|
|
720
|
+
headers=headers, data=xml_request)
|
|
721
|
+
if put_response.status_code == requests.codes.ok:
|
|
722
|
+
xml_string = str(put_response.content.decode("utf-8"))
|
|
723
|
+
identifier_response = xml.etree.ElementTree.fromstring(xml_string)
|
|
724
|
+
aip_id = identifier_response.find(f'.//{{{self.xip_ns}}}ApiId')
|
|
725
|
+
if hasattr(aip_id, 'text'):
|
|
726
|
+
return aip_id.text
|
|
727
|
+
else:
|
|
728
|
+
return None
|
|
729
|
+
if put_response.status_code == requests.codes.unauthorized:
|
|
730
|
+
self.token = self.__token__()
|
|
731
|
+
return self.update_identifiers(entity, identifier_type, identifier_value)
|
|
732
|
+
if put_response.status_code == requests.codes.no_content:
|
|
733
|
+
pass
|
|
734
|
+
else:
|
|
735
|
+
return None
|
|
736
|
+
return entity
|
|
737
|
+
elif response.status_code == requests.codes.unauthorized:
|
|
738
|
+
self.token = self.__token__()
|
|
739
|
+
return self.update_identifiers(entity, identifier_type, identifier_value)
|
|
740
|
+
else:
|
|
741
|
+
logger.error(response)
|
|
742
|
+
raise RuntimeError(response.status_code, "update_identifiers failed")
|
|
743
|
+
|
|
744
|
+
def delete_relationships(self, entity: Entity, relationship_type: str = None):
|
|
745
|
+
"""
|
|
746
|
+
Delete a relationship between two entities by its internal id
|
|
747
|
+
|
|
748
|
+
This function only deletes the relationship FROM the specified entity to another entity
|
|
749
|
+
It does not delete relationships TO this entity
|
|
750
|
+
|
|
751
|
+
If relationship_type is not specified all relationships FROM this entity are deleted.
|
|
752
|
+
|
|
753
|
+
:param entity:
|
|
754
|
+
:type entity: Entity
|
|
755
|
+
|
|
756
|
+
:param relationship_type: The relationship type to delete
|
|
757
|
+
:type relationship_type: str
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
if (self.major_version < 7) and (self.minor_version < 4) and (self.patch_version < 1):
|
|
761
|
+
raise RuntimeError("add_relation API call is only available with a Preservica v6.3.1 system or higher")
|
|
762
|
+
|
|
763
|
+
for relationship in self.relationships(entity=entity):
|
|
764
|
+
if relationship.direction == RelationshipDirection.FROM:
|
|
765
|
+
assert relationship.this_ref == entity.reference
|
|
766
|
+
if relationship_type is None:
|
|
767
|
+
self.__delete_relationship(relationship)
|
|
768
|
+
if relationship_type == relationship.relationship_type:
|
|
769
|
+
self.__delete_relationship(relationship)
|
|
770
|
+
|
|
771
|
+
def __delete_relationship(self, relationship: Relationship):
|
|
772
|
+
"""
|
|
773
|
+
Delete a relationship between two entities by its internal id
|
|
774
|
+
|
|
775
|
+
:param relationship:
|
|
776
|
+
:return:
|
|
491
777
|
"""
|
|
778
|
+
headers = {HEADER_TOKEN: self.token}
|
|
779
|
+
entity = self.entity(relationship.entity_type, relationship.this_ref)
|
|
780
|
+
end_point = f"{entity.path}/{entity.reference}/links/{relationship.api_id}"
|
|
781
|
+
request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers)
|
|
782
|
+
if request.status_code == requests.codes.no_content:
|
|
783
|
+
return None
|
|
784
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
785
|
+
self.token = self.__token__()
|
|
786
|
+
return self.__delete_relationship(relationship)
|
|
787
|
+
else:
|
|
788
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "delete_relationships",
|
|
789
|
+
request.content.decode('utf-8'))
|
|
790
|
+
logger.error(exception)
|
|
791
|
+
raise exception
|
|
792
|
+
|
|
793
|
+
def relationships(self, entity: Entity, page_size: int = 25) -> Generator[Relationship, None, None]:
|
|
794
|
+
"""
|
|
795
|
+
List the relationship links between entities
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
:param page_size: The number of items returned in a single server call
|
|
799
|
+
:type: page_size: int
|
|
800
|
+
|
|
801
|
+
:param entity: The Source Entity
|
|
802
|
+
:type: entity: An Entity type such as Asset, Folder etc
|
|
803
|
+
|
|
804
|
+
:return: Generator
|
|
805
|
+
:rtype: Relationship
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
paged_set = self.__relationships__(entity, maximum=page_size, next_page=None)
|
|
809
|
+
for e in paged_set.results:
|
|
810
|
+
yield e
|
|
811
|
+
while paged_set.has_more:
|
|
812
|
+
paged_set = self.__relationships__(entity, maximum=page_size, next_page=paged_set.next_page)
|
|
813
|
+
for e in paged_set.results:
|
|
814
|
+
yield e
|
|
815
|
+
|
|
816
|
+
def __relationships__(self, entity: Entity, maximum: int = 50, next_page: str = None) -> PagedSet:
|
|
817
|
+
"""
|
|
818
|
+
List the relationship links between entities
|
|
819
|
+
|
|
820
|
+
:param next_page: URL to next page of results
|
|
821
|
+
:type: next_page: str
|
|
822
|
+
|
|
823
|
+
:param maximum: The number of items returned in a single server call
|
|
824
|
+
:type: maximum: int
|
|
825
|
+
|
|
826
|
+
:param entity: The Source Entity
|
|
827
|
+
:type: from_entity: Entity
|
|
828
|
+
|
|
829
|
+
:return: relationship links
|
|
830
|
+
:rtype: list
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
headers = {HEADER_TOKEN: self.token}
|
|
834
|
+
end_point = f"{entity.path}/{entity.reference}/links"
|
|
835
|
+
|
|
836
|
+
if next_page is None:
|
|
837
|
+
params = {'start': '0', 'max': str(maximum)}
|
|
838
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers,
|
|
839
|
+
params=params)
|
|
840
|
+
else:
|
|
841
|
+
request = self.session.get(next_page, headers=headers)
|
|
842
|
+
|
|
843
|
+
if request.status_code == requests.codes.ok:
|
|
844
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
845
|
+
logger.debug(xml_response)
|
|
846
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
847
|
+
links = entity_response.findall(f'.//{{{self.entity_ns}}}Link')
|
|
848
|
+
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Paging/{{{self.entity_ns}}}Next')
|
|
849
|
+
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}Paging/{{{self.entity_ns}}}TotalResults')
|
|
850
|
+
results = []
|
|
851
|
+
for link in links:
|
|
852
|
+
link_type = link.attrib['linkType']
|
|
853
|
+
link_direction = link.attrib['linkDirection']
|
|
854
|
+
title = link.attrib['title']
|
|
855
|
+
other_ref = link.attrib['ref']
|
|
856
|
+
this_ref = entity.reference
|
|
857
|
+
entity_type = link.attrib['type']
|
|
858
|
+
api_id = link.attrib['apiId']
|
|
859
|
+
results.append(Relationship(api_id, link_type, RelationshipDirection(link_direction), other_ref, title,
|
|
860
|
+
EntityType(entity_type), this_ref, api_id))
|
|
861
|
+
has_more = True
|
|
862
|
+
url = None
|
|
863
|
+
if next_url is None:
|
|
864
|
+
has_more = False
|
|
865
|
+
else:
|
|
866
|
+
url = next_url.text
|
|
867
|
+
|
|
868
|
+
return PagedSet(results, has_more, int(total_hits.text), url)
|
|
869
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
870
|
+
return self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
871
|
+
else:
|
|
872
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "relationships",
|
|
873
|
+
request.content.decode('utf-8'))
|
|
874
|
+
logger.error(exception)
|
|
875
|
+
raise exception
|
|
876
|
+
|
|
877
|
+
def add_relation(self, from_entity: Entity, relationship_type: str, to_entity: Entity):
|
|
878
|
+
"""
|
|
879
|
+
Add a new relationship link between two Assets or Folders
|
|
880
|
+
|
|
881
|
+
:param from_entity: The Source entity to link from
|
|
882
|
+
:type from_entity: Entity
|
|
883
|
+
|
|
884
|
+
:param to_entity: The Target entity
|
|
885
|
+
:type to_entity: Entity
|
|
886
|
+
|
|
887
|
+
:param relationship_type: The Relationship type
|
|
888
|
+
:type relationship_type: str
|
|
889
|
+
|
|
890
|
+
:return: relationship_type
|
|
891
|
+
:rtype: str
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
if (self.major_version < 7) and (self.minor_version < 4) and (self.patch_version < 1):
|
|
895
|
+
raise RuntimeError("add_relation API call is only available with a Preservica v6.3.1 system or higher")
|
|
896
|
+
|
|
897
|
+
assert from_entity.entity_type is not EntityType.CONTENT_OBJECT
|
|
898
|
+
assert to_entity.entity_type is not EntityType.CONTENT_OBJECT
|
|
899
|
+
|
|
900
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
901
|
+
|
|
902
|
+
xml_object = xml.etree.ElementTree.Element('Link ', {"xmlns": self.xip_ns})
|
|
903
|
+
xml.etree.ElementTree.SubElement(xml_object, "Type").text = relationship_type
|
|
904
|
+
xml.etree.ElementTree.SubElement(xml_object, "FromEntity").text = from_entity.reference
|
|
905
|
+
xml.etree.ElementTree.SubElement(xml_object, "ToEntity").text = to_entity.reference
|
|
906
|
+
|
|
907
|
+
end_point = f"/{from_entity.path}/{from_entity.reference}/links"
|
|
908
|
+
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
909
|
+
logger.debug(xml_request)
|
|
910
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
911
|
+
headers=headers)
|
|
912
|
+
if request.status_code == requests.codes.ok:
|
|
913
|
+
xml_string = str(request.content.decode("utf-8"))
|
|
914
|
+
logger.debug(xml_string)
|
|
915
|
+
link_response = xml.etree.ElementTree.fromstring(xml_string)
|
|
916
|
+
relation = link_response.find(f'.//{{{self.xip_ns}}}Link')
|
|
917
|
+
relation_type = relation.find(f'.//{{{self.xip_ns}}}Type')
|
|
918
|
+
return relation_type.text
|
|
919
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
920
|
+
self.token = self.__token__()
|
|
921
|
+
return self.add_relation(from_entity, relationship_type, to_entity)
|
|
922
|
+
else:
|
|
923
|
+
exception = HTTPException(from_entity.reference, request.status_code, request.url, "add_relation",
|
|
924
|
+
request.content.decode('utf-8'))
|
|
925
|
+
logger.error(exception)
|
|
926
|
+
raise exception
|
|
927
|
+
|
|
928
|
+
def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
|
|
929
|
+
"""
|
|
930
|
+
Delete an existing descriptive XML document on an entity by its schema
|
|
931
|
+
This call will delete all fragments with the same schema
|
|
932
|
+
|
|
933
|
+
:param Entity entity: The entity to add the metadata to
|
|
934
|
+
:param str schema: The metadata schema URI
|
|
935
|
+
:return: The updated Entity
|
|
936
|
+
:rtype: Entity
|
|
937
|
+
"""
|
|
938
|
+
|
|
492
939
|
headers = {HEADER_TOKEN: self.token}
|
|
493
940
|
for url in entity.metadata:
|
|
494
941
|
if schema == entity.metadata[url]:
|
|
@@ -499,18 +946,52 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
499
946
|
self.token = self.__token__()
|
|
500
947
|
return self.delete_metadata(entity, schema)
|
|
501
948
|
else:
|
|
502
|
-
|
|
949
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "delete_metadata",
|
|
950
|
+
request.content.decode('utf-8'))
|
|
951
|
+
logger.error(exception)
|
|
952
|
+
raise exception
|
|
953
|
+
|
|
503
954
|
return self.entity(entity.entity_type, entity.reference)
|
|
504
955
|
|
|
505
|
-
|
|
956
|
+
|
|
957
|
+
def add_group_metadata(self, csv_file: str) -> str:
|
|
506
958
|
"""
|
|
507
|
-
|
|
959
|
+
Perform bulk additions of metadata with a CSV file.
|
|
960
|
+
This is designed for metadata which populates a New Gen Metadata Group
|
|
961
|
+
Returns The process ID which will track the updates
|
|
962
|
+
Requires DataManagement permission
|
|
963
|
+
|
|
964
|
+
:param str csv_file: The path of the CSV metadata file
|
|
965
|
+
:return: The process ID
|
|
966
|
+
:rtype: str
|
|
967
|
+
"""
|
|
968
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/csv;charset=UTF-8'}
|
|
969
|
+
|
|
970
|
+
url = f'{self.protocol}://{self.server}/api/entity/actions/metadata-csv-edits'
|
|
971
|
+
|
|
972
|
+
with open(csv_file, 'rb') as fd:
|
|
973
|
+
with self.session.post(url, headers=headers, data=fd) as request:
|
|
974
|
+
if request.status_code == requests.codes.unauthorized:
|
|
975
|
+
self.token = self.__token__()
|
|
976
|
+
return self.add_group_metadata(csv_file)
|
|
977
|
+
elif request.status_code == requests.codes.accepted:
|
|
978
|
+
return str(request.content.decode('utf-8'))
|
|
979
|
+
else:
|
|
980
|
+
exception = HTTPException(None, request.status_code, request.url, "add_group_metadata",
|
|
981
|
+
request.content.decode('utf-8'))
|
|
982
|
+
logger.error(exception)
|
|
983
|
+
raise exception
|
|
508
984
|
|
|
509
|
-
Returns The updated Entity
|
|
510
985
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
986
|
+
def update_metadata(self, entity: EntityT, schema: str, data: Any) -> EntityT:
|
|
987
|
+
"""
|
|
988
|
+
Update an existing descriptive XML document on an entity
|
|
989
|
+
|
|
990
|
+
:param Entity entity: The entity to add the metadata to
|
|
991
|
+
:param str schema: The metadata schema URI
|
|
992
|
+
:param data data: The XML document as a string or as a file bytes
|
|
993
|
+
:return: The updated Entity
|
|
994
|
+
:rtype: Entity
|
|
514
995
|
"""
|
|
515
996
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
516
997
|
|
|
@@ -520,11 +1001,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
520
1001
|
for url in entity.metadata:
|
|
521
1002
|
if schema == entity.metadata[url]:
|
|
522
1003
|
mref = url[url.rfind(f"{entity.reference}/metadata/") + len(f"{entity.reference}/metadata/"):]
|
|
523
|
-
xml_object = xml.etree.ElementTree.Element('MetadataContainer',
|
|
524
|
-
{"schemaUri": schema, "xmlns": self.xip_ns})
|
|
525
|
-
xml.etree.ElementTree.SubElement(xml_object, "Ref").text = mref
|
|
526
|
-
xml.etree.ElementTree.SubElement(xml_object, "Entity").text = entity.reference
|
|
527
|
-
content = xml.etree.ElementTree.SubElement(xml_object, "Content")
|
|
1004
|
+
xml_object = xml.etree.ElementTree.Element('xip:MetadataContainer',
|
|
1005
|
+
{"schemaUri": schema, "xmlns:xip": self.xip_ns})
|
|
1006
|
+
xml.etree.ElementTree.SubElement(xml_object, "xip:Ref").text = mref
|
|
1007
|
+
xml.etree.ElementTree.SubElement(xml_object, "xip:Entity").text = entity.reference
|
|
1008
|
+
content = xml.etree.ElementTree.SubElement(xml_object, "xip:Content")
|
|
528
1009
|
if isinstance(data, str):
|
|
529
1010
|
ob = xml.etree.ElementTree.fromstring(data)
|
|
530
1011
|
content.append(ob)
|
|
@@ -533,7 +1014,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
533
1014
|
content.append(tree.getroot())
|
|
534
1015
|
else:
|
|
535
1016
|
raise RuntimeError("Unknown data type")
|
|
536
|
-
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
1017
|
+
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8').decode("utf-8")
|
|
537
1018
|
logger.debug(xml_request)
|
|
538
1019
|
request = self.session.put(url, data=xml_request, headers=headers)
|
|
539
1020
|
if request.status_code == requests.codes.ok:
|
|
@@ -542,24 +1023,64 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
542
1023
|
self.token = self.__token__()
|
|
543
1024
|
return self.update_metadata(entity, schema, data)
|
|
544
1025
|
else:
|
|
545
|
-
|
|
1026
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "update_metadata",
|
|
1027
|
+
request.content.decode('utf-8'))
|
|
1028
|
+
logger.error(exception)
|
|
1029
|
+
raise exception
|
|
546
1030
|
return self.entity(entity.entity_type, entity.reference)
|
|
547
1031
|
|
|
548
|
-
def
|
|
1032
|
+
def add_metadata_as_fragment(self, entity: EntityT, schema: str, xml_fragment: str) -> EntityT:
|
|
549
1033
|
"""
|
|
550
|
-
Add a metadata fragment with a given namespace URI
|
|
1034
|
+
Add a metadata fragment with a given namespace URI to an Entity
|
|
1035
|
+
Don't parse the xml fragment which may add extra namespaces etc
|
|
551
1036
|
|
|
552
1037
|
Returns The updated Entity
|
|
553
1038
|
|
|
554
|
-
:param
|
|
555
|
-
:param entity: The
|
|
556
|
-
:param schema: The schema URI of the XML document
|
|
1039
|
+
:param str xml_fragment: The new XML as a string
|
|
1040
|
+
:param Entity entity: The entity to update
|
|
1041
|
+
:param str schema: The schema URI of the XML document
|
|
1042
|
+
:rtype: Entity
|
|
557
1043
|
"""
|
|
558
1044
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
559
1045
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1046
|
+
xml_doc = f"""<xip:MetadataContainer xmlns="{schema}" schemaUri="{schema}" xmlns:xip="{self.xip_ns}">
|
|
1047
|
+
<xip:Entity>{entity.reference}</xip:Entity>
|
|
1048
|
+
<xip:Content>
|
|
1049
|
+
{xml_fragment}
|
|
1050
|
+
</xip:Content>
|
|
1051
|
+
</xip:MetadataContainer>"""
|
|
1052
|
+
|
|
1053
|
+
end_point = f"/{entity.path}/{entity.reference}/metadata"
|
|
1054
|
+
logger.debug(xml_doc)
|
|
1055
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_doc,
|
|
1056
|
+
headers=headers)
|
|
1057
|
+
if request.status_code == requests.codes.ok:
|
|
1058
|
+
return self.entity(entity_type=entity.entity_type, reference=entity.reference)
|
|
1059
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1060
|
+
self.token = self.__token__()
|
|
1061
|
+
return self.add_metadata(entity, schema, xml_fragment)
|
|
1062
|
+
else:
|
|
1063
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "add_metadata",
|
|
1064
|
+
request.content.decode('utf-8'))
|
|
1065
|
+
logger.error(exception)
|
|
1066
|
+
raise exception
|
|
1067
|
+
|
|
1068
|
+
def add_metadata(self, entity: EntityT, schema: str, data) -> EntityT:
|
|
1069
|
+
"""
|
|
1070
|
+
Add a new descriptive XML document to an existing entity
|
|
1071
|
+
|
|
1072
|
+
:param Entity entity: The entity to add the metadata to
|
|
1073
|
+
:param str schema: The metadata schema URI
|
|
1074
|
+
:param data data: The XML document as a string or as file bytes
|
|
1075
|
+
:return: The updated entity with the new metadata
|
|
1076
|
+
:rtype: Entity
|
|
1077
|
+
"""
|
|
1078
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
1079
|
+
|
|
1080
|
+
xml_object = xml.etree.ElementTree.Element('xip:MetadataContainer', {"schemaUri": schema,
|
|
1081
|
+
"xmlns:xip": self.xip_ns})
|
|
1082
|
+
xml.etree.ElementTree.SubElement(xml_object, "xip:Entity").text = entity.reference
|
|
1083
|
+
content = xml.etree.ElementTree.SubElement(xml_object, "xip:Content")
|
|
563
1084
|
if isinstance(data, str):
|
|
564
1085
|
ob = xml.etree.ElementTree.fromstring(data)
|
|
565
1086
|
content.append(ob)
|
|
@@ -571,23 +1092,30 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
571
1092
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
572
1093
|
end_point = f"/{entity.path}/{entity.reference}/metadata"
|
|
573
1094
|
logger.debug(xml_request)
|
|
574
|
-
request = self.session.post(f'
|
|
1095
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
1096
|
+
headers=headers)
|
|
575
1097
|
if request.status_code == requests.codes.ok:
|
|
576
1098
|
return self.entity(entity_type=entity.entity_type, reference=entity.reference)
|
|
577
1099
|
elif request.status_code == requests.codes.unauthorized:
|
|
578
1100
|
self.token = self.__token__()
|
|
579
1101
|
return self.add_metadata(entity, schema, data)
|
|
580
1102
|
else:
|
|
581
|
-
|
|
1103
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "add_metadata",
|
|
1104
|
+
request.content.decode('utf-8'))
|
|
1105
|
+
logger.error(exception)
|
|
1106
|
+
raise exception
|
|
582
1107
|
|
|
583
|
-
def save(self, entity:
|
|
1108
|
+
def save(self, entity: EntityT) -> EntityT:
|
|
584
1109
|
"""
|
|
585
|
-
|
|
1110
|
+
Updates the title and description of an entity
|
|
1111
|
+
The security tag and parent are not saved via this method call
|
|
586
1112
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
:
|
|
1113
|
+
:param entity: The entity (asset, folder, content_object) to be updated
|
|
1114
|
+
:type entity: Entity
|
|
1115
|
+
:return: The updated entity
|
|
1116
|
+
:rtype: Entity
|
|
590
1117
|
"""
|
|
1118
|
+
|
|
591
1119
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
592
1120
|
|
|
593
1121
|
xml_object = xml.etree.ElementTree.Element(entity.tag, {"xmlns": self.xip_ns})
|
|
@@ -595,34 +1123,51 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
595
1123
|
xml.etree.ElementTree.SubElement(xml_object, "Title").text = entity.title
|
|
596
1124
|
xml.etree.ElementTree.SubElement(xml_object, "Description").text = entity.description
|
|
597
1125
|
xml.etree.ElementTree.SubElement(xml_object, "SecurityTag").text = entity.security_tag
|
|
1126
|
+
|
|
1127
|
+
if entity.custom_type is not None:
|
|
1128
|
+
xml.etree.ElementTree.SubElement(xml_object, "CustomType").text = entity.custom_type
|
|
1129
|
+
|
|
598
1130
|
if entity.parent is not None:
|
|
599
1131
|
xml.etree.ElementTree.SubElement(xml_object, "Parent").text = entity.parent
|
|
600
1132
|
|
|
601
1133
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
602
1134
|
logger.debug(xml_request)
|
|
603
|
-
request = self.session.put(f'
|
|
1135
|
+
request = self.session.put(f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}',
|
|
604
1136
|
data=xml_request, headers=headers)
|
|
605
1137
|
if request.status_code == requests.codes.ok:
|
|
606
1138
|
xml_response = str(request.content.decode('utf-8'))
|
|
607
1139
|
response = self.entity_from_string(xml_response)
|
|
608
1140
|
if isinstance(entity, Asset):
|
|
609
|
-
|
|
610
|
-
response['security_tag'],
|
|
611
|
-
response['parent'], response['metadata'])
|
|
612
|
-
elif isinstance(entity, Folder):
|
|
613
|
-
return Folder(response['reference'], response['title'], response['description'],
|
|
1141
|
+
asset = Asset(response['reference'], response['title'], response['description'],
|
|
614
1142
|
response['security_tag'],
|
|
615
1143
|
response['parent'], response['metadata'])
|
|
1144
|
+
if 'CustomType' in response:
|
|
1145
|
+
asset.custom_type = response['CustomType']
|
|
1146
|
+
return asset
|
|
1147
|
+
elif isinstance(entity, Folder):
|
|
1148
|
+
folder = Folder(response['reference'], response['title'], response['description'],
|
|
1149
|
+
response['security_tag'],
|
|
1150
|
+
response['parent'], response['metadata'])
|
|
1151
|
+
if 'CustomType' in response:
|
|
1152
|
+
folder.custom_type = response['CustomType']
|
|
1153
|
+
return folder
|
|
616
1154
|
elif isinstance(entity, ContentObject):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1155
|
+
content_object = ContentObject(response['reference'], response['title'],
|
|
1156
|
+
response['description'],
|
|
1157
|
+
response['security_tag'],
|
|
1158
|
+
response['parent'], response['metadata'])
|
|
1159
|
+
if 'CustomType' in response:
|
|
1160
|
+
content_object.custom_type = response['CustomType']
|
|
1161
|
+
return content_object
|
|
1162
|
+
return None
|
|
621
1163
|
elif request.status_code == requests.codes.unauthorized:
|
|
622
1164
|
self.token = self.__token__()
|
|
623
1165
|
return self.save(entity)
|
|
624
1166
|
else:
|
|
625
|
-
|
|
1167
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "save",
|
|
1168
|
+
request.content.decode('utf-8'))
|
|
1169
|
+
logger.error(exception)
|
|
1170
|
+
raise exception
|
|
626
1171
|
|
|
627
1172
|
def move_async(self, entity: Entity, dest_folder: Folder) -> str:
|
|
628
1173
|
"""
|
|
@@ -634,8 +1179,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
634
1179
|
|
|
635
1180
|
Returns The updated Entity
|
|
636
1181
|
|
|
637
|
-
:param entity:
|
|
638
|
-
:param dest_folder: The
|
|
1182
|
+
:param Entity entity: The entity to move either asset or folder
|
|
1183
|
+
:param Entity dest_folder: The new destination folder. This can be None to move a folder to the root of the repository
|
|
1184
|
+
:return: Progress ID token
|
|
1185
|
+
:rtype: str
|
|
639
1186
|
"""
|
|
640
1187
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
641
1188
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -644,19 +1191,34 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
644
1191
|
data = dest_folder.reference
|
|
645
1192
|
else:
|
|
646
1193
|
data = "@root@"
|
|
647
|
-
request = self.session.put(
|
|
648
|
-
|
|
1194
|
+
request = self.session.put(
|
|
1195
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/parent-ref',
|
|
1196
|
+
data=data, headers=headers)
|
|
649
1197
|
if request.status_code == requests.codes.accepted:
|
|
650
1198
|
return request.content.decode()
|
|
651
1199
|
elif request.status_code == requests.codes.unauthorized:
|
|
652
1200
|
self.token = self.__token__()
|
|
653
1201
|
return self.move_async(entity, dest_folder)
|
|
654
1202
|
else:
|
|
655
|
-
|
|
1203
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "move_async",
|
|
1204
|
+
request.content.decode('utf-8'))
|
|
1205
|
+
logger.error(exception)
|
|
1206
|
+
raise exception
|
|
1207
|
+
|
|
1208
|
+
def get_progress(self, pid: str) -> AsyncProgress:
|
|
1209
|
+
return AsyncProgress[self.get_async_progress(pid)]
|
|
656
1210
|
|
|
657
1211
|
def get_async_progress(self, pid: str) -> str:
|
|
1212
|
+
"""
|
|
1213
|
+
Return the status of a running process
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
:param pid: The progress ID
|
|
1217
|
+
:return: Workflow status
|
|
1218
|
+
:rtype: str
|
|
1219
|
+
"""
|
|
658
1220
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
659
|
-
request = self.session.get(f"
|
|
1221
|
+
request = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{pid}", headers=headers)
|
|
660
1222
|
if request.status_code == requests.codes.ok:
|
|
661
1223
|
entity_response = xml.etree.ElementTree.fromstring(request.content.decode("utf-8"))
|
|
662
1224
|
status = entity_response.find(".//{http://status.preservica.com}Status")
|
|
@@ -668,18 +1230,20 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
668
1230
|
self.token = self.__token__()
|
|
669
1231
|
return self.get_async_progress(pid)
|
|
670
1232
|
else:
|
|
671
|
-
|
|
1233
|
+
exception = HTTPException(pid, request.status_code, request.url, "get_async_progress",
|
|
1234
|
+
request.content.decode('utf-8'))
|
|
1235
|
+
logger.error(exception)
|
|
1236
|
+
raise exception
|
|
672
1237
|
|
|
673
|
-
def move_sync(self, entity:
|
|
1238
|
+
def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
674
1239
|
"""
|
|
675
|
-
Move an
|
|
676
|
-
|
|
1240
|
+
Move an entity (asset or folder) to a new folder
|
|
1241
|
+
This call blocks until the move is complete
|
|
677
1242
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
:
|
|
682
|
-
:param dest_folder: The Folder which will become the new parent of this entity
|
|
1243
|
+
:param Entity entity: The entity to move either asset or folder
|
|
1244
|
+
:param Entity dest_folder: The new destination folder. This can be None to move a folder to the root of the repository
|
|
1245
|
+
:return: The updated entity
|
|
1246
|
+
:rtype: Entity
|
|
683
1247
|
"""
|
|
684
1248
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
685
1249
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -688,8 +1252,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
688
1252
|
data = dest_folder.reference
|
|
689
1253
|
else:
|
|
690
1254
|
data = "@root@"
|
|
691
|
-
request = self.session.put(
|
|
692
|
-
|
|
1255
|
+
request = self.session.put(
|
|
1256
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/parent-ref',
|
|
1257
|
+
data=data, headers=headers)
|
|
693
1258
|
if request.status_code == requests.codes.accepted:
|
|
694
1259
|
sleep_sec = 1
|
|
695
1260
|
while True:
|
|
@@ -704,17 +1269,20 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
704
1269
|
self.token = self.__token__()
|
|
705
1270
|
return self.move_sync(entity, dest_folder)
|
|
706
1271
|
else:
|
|
707
|
-
|
|
1272
|
+
exception = HTTPException(entity, request.status_code, request.url, "move_sync",
|
|
1273
|
+
request.content.decode('utf-8'))
|
|
1274
|
+
logger.error(exception)
|
|
1275
|
+
raise exception
|
|
708
1276
|
|
|
709
|
-
def move(self, entity:
|
|
1277
|
+
def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
710
1278
|
"""
|
|
711
|
-
Move an
|
|
712
|
-
|
|
1279
|
+
Move an entity (asset or folder) to a new folder
|
|
1280
|
+
This call is an alias for the move_sync (blocking) method.
|
|
713
1281
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
:
|
|
717
|
-
:
|
|
1282
|
+
:param Entity entity: The entity to move either asset or folder
|
|
1283
|
+
:param Entity dest_folder: The new destination folder. This can be None to move a folder to the root of the repository
|
|
1284
|
+
:return: The updated entity
|
|
1285
|
+
:rtype: Entity
|
|
718
1286
|
"""
|
|
719
1287
|
return self.move_sync(entity, dest_folder)
|
|
720
1288
|
|
|
@@ -742,7 +1310,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
742
1310
|
|
|
743
1311
|
xml_request = xml.etree.ElementTree.tostring(structural_object, encoding='utf-8')
|
|
744
1312
|
logger.debug(xml_request)
|
|
745
|
-
request = self.session.post(f'
|
|
1313
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity/structural-objects', data=xml_request,
|
|
746
1314
|
headers=headers)
|
|
747
1315
|
if request.status_code == requests.codes.ok:
|
|
748
1316
|
xml_response = str(request.content.decode('utf-8'))
|
|
@@ -755,35 +1323,81 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
755
1323
|
self.token = self.__token__()
|
|
756
1324
|
return self.create_folder(title, description, security_tag, parent=parent)
|
|
757
1325
|
else:
|
|
758
|
-
|
|
1326
|
+
exception = HTTPException(title, request.status_code, request.url, "create_folder",
|
|
1327
|
+
request.content.decode('utf-8'))
|
|
1328
|
+
logger.error(exception)
|
|
1329
|
+
raise exception
|
|
1330
|
+
|
|
1331
|
+
def all_metadata(self, entity: Entity) -> Generator[Tuple[str, str], None, None]:
|
|
1332
|
+
"""
|
|
1333
|
+
Retrieve all metadata fragments on an entity
|
|
1334
|
+
|
|
1335
|
+
Returns XML documents in a tuple
|
|
1336
|
+
|
|
1337
|
+
:param Entity entity: The entity with the metadata
|
|
1338
|
+
:return: A list of Tuples, the first value is the schmea and the second is the metadata
|
|
1339
|
+
:rtype: Generator[Tuple[str, str]]
|
|
1340
|
+
"""
|
|
1341
|
+
|
|
1342
|
+
for uri, schema in entity.metadata.items():
|
|
1343
|
+
yield tuple((str(schema), self.metadata(uri)))
|
|
759
1344
|
|
|
760
|
-
def metadata_for_entity(self, entity: Entity, schema: str) ->
|
|
1345
|
+
def metadata_for_entity(self, entity: Entity, schema: str) -> Union[str, None]:
|
|
761
1346
|
"""
|
|
762
|
-
|
|
1347
|
+
Fetch the first metadata document which matches the schema URI from an entity
|
|
1348
|
+
|
|
1349
|
+
:param Entity entity: The entity containing the metadata
|
|
1350
|
+
:param str schema: The metadata schema URI
|
|
1351
|
+
:return: The first XML document on the entity matching the schema URI
|
|
1352
|
+
:rtype: str
|
|
1353
|
+
"""
|
|
1354
|
+
|
|
1355
|
+
# if the entity is a lightweight enum version request the full object
|
|
1356
|
+
if entity.metadata is None:
|
|
1357
|
+
entity = self.entity(entity.entity_type, entity.reference)
|
|
1358
|
+
|
|
1359
|
+
for uri, schema_name in entity.metadata.items():
|
|
1360
|
+
if schema == schema_name:
|
|
1361
|
+
return self.metadata(uri)
|
|
1362
|
+
return None
|
|
1363
|
+
|
|
1364
|
+
def metadata_tag_for_entity(self, entity: Entity, schema: str, tag: str, isXpath: bool = False) -> Union[str, None]:
|
|
1365
|
+
"""
|
|
1366
|
+
Retrieve the first value of the tag from a metadata template given by schema
|
|
763
1367
|
|
|
764
1368
|
Returns XML document as a string
|
|
765
1369
|
|
|
1370
|
+
:param isXpath: True if the tag name is a fully qualified xpath expression
|
|
766
1371
|
:param entity: The entity with the metadata
|
|
767
1372
|
:param schema: The schema URI
|
|
1373
|
+
:param tag: The tag name
|
|
768
1374
|
"""
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1375
|
+
|
|
1376
|
+
xml_doc = self.metadata_for_entity(entity, schema)
|
|
1377
|
+
if xml_doc:
|
|
1378
|
+
xml_object = xml.etree.ElementTree.fromstring(xml_doc)
|
|
1379
|
+
if not isXpath:
|
|
1380
|
+
return xml_object.find(f'.//{{*}}{tag}').text
|
|
1381
|
+
else:
|
|
1382
|
+
return xml_object.find(tag).text
|
|
772
1383
|
return None
|
|
773
1384
|
|
|
774
|
-
def security_tag_sync(self, entity:
|
|
1385
|
+
def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
|
|
775
1386
|
"""
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1387
|
+
Change the security tag of an asset or folder
|
|
1388
|
+
This is a blocking call which returns after all entities have been updated.
|
|
1389
|
+
|
|
1390
|
+
:param entity: The entity (asset, folder) to be updated
|
|
1391
|
+
:type entity: Entity
|
|
1392
|
+
:param new_tag: The new security tag to be set on the entity
|
|
1393
|
+
:type new_tag: str
|
|
1394
|
+
:return: The updated entity
|
|
1395
|
+
:rtype: Entity
|
|
782
1396
|
"""
|
|
783
1397
|
self.token = self.__token__()
|
|
784
1398
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
785
1399
|
end_point = f"/{entity.path}/{entity.reference}/security-descriptor"
|
|
786
|
-
request = self.session.put(f'
|
|
1400
|
+
request = self.session.put(f'{self.protocol}://{self.server}/api/entity{end_point}?includeDescendants=false',
|
|
787
1401
|
data=new_tag, headers=headers)
|
|
788
1402
|
if request.status_code == requests.codes.accepted:
|
|
789
1403
|
sleep_sec = 1
|
|
@@ -798,158 +1412,366 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
798
1412
|
self.token = self.__token__()
|
|
799
1413
|
return self.security_tag_sync(entity, new_tag)
|
|
800
1414
|
else:
|
|
801
|
-
|
|
1415
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "security_tag_sync",
|
|
1416
|
+
request.content.decode('utf-8'))
|
|
1417
|
+
logger.error(exception)
|
|
1418
|
+
raise exception
|
|
802
1419
|
|
|
803
1420
|
def security_tag_async(self, entity: Entity, new_tag: str):
|
|
804
1421
|
"""
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1422
|
+
Change the security tag of an asset or folder
|
|
1423
|
+
This is a non blocking call which returns immediately.
|
|
1424
|
+
|
|
1425
|
+
:param entity: The entity (asset, folder) to be updated
|
|
1426
|
+
:type entity: Entity
|
|
1427
|
+
:param new_tag: The new security tag to be set on the entity
|
|
1428
|
+
:type new_tag: str
|
|
1429
|
+
:return: A progress id which can be used to monitor the workflow
|
|
1430
|
+
:rtype: str
|
|
811
1431
|
"""
|
|
812
1432
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
813
1433
|
end_point = f"/{entity.path}/{entity.reference}/security-descriptor"
|
|
814
|
-
request = self.session.put(f'
|
|
1434
|
+
request = self.session.put(f'{self.protocol}://{self.server}/api/entity{end_point}?includeDescendants=false',
|
|
815
1435
|
data=new_tag, headers=headers)
|
|
816
1436
|
if request.status_code == requests.codes.accepted:
|
|
817
|
-
return request.content.decode("utf-8")
|
|
1437
|
+
return request.content.decode("utf-8")
|
|
1438
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1439
|
+
self.token = self.__token__()
|
|
1440
|
+
return self.security_tag_async(entity, new_tag)
|
|
1441
|
+
else:
|
|
1442
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "security_tag_async",
|
|
1443
|
+
request.content.decode('utf-8'))
|
|
1444
|
+
logger.error(exception)
|
|
1445
|
+
raise exception
|
|
1446
|
+
|
|
1447
|
+
def metadata(self, uri: str) -> str:
|
|
1448
|
+
"""
|
|
1449
|
+
Fetch the metadata document by its identifier, this is the key from the entity metadata map
|
|
1450
|
+
|
|
1451
|
+
:param str uri: The metadata identifier
|
|
1452
|
+
:return: An XML document as a string
|
|
1453
|
+
:rtype: str
|
|
1454
|
+
"""
|
|
1455
|
+
request = self.session.get(uri, headers={HEADER_TOKEN: self.token})
|
|
1456
|
+
if request.status_code == requests.codes.ok:
|
|
1457
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
1458
|
+
logger.debug(xml_response)
|
|
1459
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
1460
|
+
content = entity_response.find(f'.//{{{self.xip_ns}}}Content')
|
|
1461
|
+
return xml.etree.ElementTree.tostring(content[0], encoding='utf-8', method='xml').decode('utf-8')
|
|
1462
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1463
|
+
self.token = self.__token__()
|
|
1464
|
+
return self.metadata(uri)
|
|
1465
|
+
else:
|
|
1466
|
+
exception = HTTPException(uri, request.status_code, request.url, "metadata",
|
|
1467
|
+
request.content.decode('utf-8'))
|
|
1468
|
+
logger.error(exception)
|
|
1469
|
+
raise exception
|
|
1470
|
+
|
|
1471
|
+
def entity(self, entity_type: EntityType, reference: str) -> EntityT:
|
|
1472
|
+
"""
|
|
1473
|
+
Returns a generic entity based on its reference identifier
|
|
1474
|
+
|
|
1475
|
+
:param entity_type: The type of entity
|
|
1476
|
+
:type entity_type: EntityType
|
|
1477
|
+
:param reference: The unique identifier for the entity
|
|
1478
|
+
:type reference: str
|
|
1479
|
+
:return: The entity either Asset, Folder or ContentObject
|
|
1480
|
+
:rtype: Entity
|
|
1481
|
+
:raises RuntimeError: if the identifier is incorrect
|
|
1482
|
+
"""
|
|
1483
|
+
if entity_type is EntityType.CONTENT_OBJECT:
|
|
1484
|
+
return self.content_object(reference)
|
|
1485
|
+
if entity_type is EntityType.FOLDER:
|
|
1486
|
+
return self.folder(reference)
|
|
1487
|
+
if entity_type is EntityType.ASSET:
|
|
1488
|
+
return self.asset(reference)
|
|
1489
|
+
return None
|
|
1490
|
+
|
|
1491
|
+
def add_physical_asset(self, title: str, description: str, parent: Folder, security_tag: str = "open") -> Asset:
|
|
1492
|
+
"""
|
|
1493
|
+
Create a new asset which represents a physical object
|
|
1494
|
+
|
|
1495
|
+
Returns Asset
|
|
1496
|
+
|
|
1497
|
+
:param str title: The title of the new Asset
|
|
1498
|
+
:param str description: The description of the new Asset
|
|
1499
|
+
:param Folder parent: The parent folder
|
|
1500
|
+
:param str security_tag: The security tag, defaults to open
|
|
1501
|
+
:return: The new physical object
|
|
1502
|
+
:rtype: Asset
|
|
1503
|
+
"""
|
|
1504
|
+
|
|
1505
|
+
if (self.major_version < 7) and (self.minor_version < 4):
|
|
1506
|
+
raise RuntimeError(
|
|
1507
|
+
"add_physical_asset API call is only available with a Preservica v6.4.0 system or higher")
|
|
1508
|
+
|
|
1509
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
1510
|
+
|
|
1511
|
+
xip_object = xml.etree.ElementTree.Element('XIP ', {"xmlns": self.xip_ns})
|
|
1512
|
+
io_object = xml.etree.ElementTree.SubElement(xip_object, "InformationObject")
|
|
1513
|
+
xml.etree.ElementTree.SubElement(io_object, "Title").text = str(title)
|
|
1514
|
+
xml.etree.ElementTree.SubElement(io_object, "Description").text = str(description)
|
|
1515
|
+
xml.etree.ElementTree.SubElement(io_object, "SecurityTag").text = str(security_tag)
|
|
1516
|
+
xml.etree.ElementTree.SubElement(io_object, "Parent").text = parent.reference
|
|
1517
|
+
rep_object = xml.etree.ElementTree.SubElement(xip_object, "Representation")
|
|
1518
|
+
xml.etree.ElementTree.SubElement(rep_object, "Type").text = "Physical"
|
|
1519
|
+
|
|
1520
|
+
xml_request = xml.etree.ElementTree.tostring(xip_object, encoding='utf-8')
|
|
1521
|
+
|
|
1522
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}', data=xml_request,
|
|
1523
|
+
headers=headers)
|
|
1524
|
+
if request.status_code == requests.codes.ok:
|
|
1525
|
+
xml_string = str(request.content.decode("utf-8"))
|
|
1526
|
+
entity = self.entity_from_string(xml_string)
|
|
1527
|
+
return Asset(entity['reference'], entity['title'], entity['description'],
|
|
1528
|
+
entity['security_tag'], entity['parent'],
|
|
1529
|
+
entity['metadata'])
|
|
1530
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1531
|
+
self.token = self.__token__()
|
|
1532
|
+
return self.add_physical_asset(title, description, parent, security_tag)
|
|
1533
|
+
else:
|
|
1534
|
+
exception = HTTPException(title, request.status_code, request.url, "add_physical_asset",
|
|
1535
|
+
request.content.decode('utf-8'))
|
|
1536
|
+
logger.error(exception)
|
|
1537
|
+
raise exception
|
|
1538
|
+
|
|
1539
|
+
def merge_assets(self, assets: list[Asset], title: str, description: str) -> str:
|
|
1540
|
+
"""
|
|
1541
|
+
Create a new Asset with the content from each Asset in supplied list
|
|
1542
|
+
This call will create a new multipart Asset which contains all the content from list of Assets.
|
|
1543
|
+
|
|
1544
|
+
The return value is the progress status of the merge operation.
|
|
1545
|
+
"""
|
|
1546
|
+
|
|
1547
|
+
headers = {
|
|
1548
|
+
HEADER_TOKEN: self.token,
|
|
1549
|
+
"Content-Type": "application/xml;charset=UTF-8",
|
|
1550
|
+
"accept": "text/plain;charset=UTF-8",
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
merge_object = xml.etree.ElementTree.Element("MergeAction", {"xmlns": self.entity_ns, "xmlns:xip": self.xip_ns})
|
|
1554
|
+
xml.etree.ElementTree.SubElement(merge_object, "Title").text = str(title)
|
|
1555
|
+
xml.etree.ElementTree.SubElement(merge_object, "Description").text = str(description)
|
|
1556
|
+
for a in assets:
|
|
1557
|
+
xml.etree.ElementTree.SubElement(merge_object, "Entity", {
|
|
1558
|
+
"excludeIdentifiers": "true",
|
|
1559
|
+
"excludeLinks": "true",
|
|
1560
|
+
"excludeMetadata": "true",
|
|
1561
|
+
"ref": a.reference,
|
|
1562
|
+
"type": EntityType.ASSET.value}
|
|
1563
|
+
)
|
|
1564
|
+
# order_object = xml.etree.ElementTree.SubElement(merge_object, "Order")
|
|
1565
|
+
# for a in assets:
|
|
1566
|
+
# xml.etree.ElementTree.SubElement(order_object, "Entity", {
|
|
1567
|
+
# "ref": a.reference,
|
|
1568
|
+
# "type": EntityType.CONTENT_OBJECT.value}
|
|
1569
|
+
# )
|
|
1570
|
+
xml_request = xml.etree.ElementTree.tostring(merge_object, encoding="utf-8")
|
|
1571
|
+
print(xml_request)
|
|
1572
|
+
request = self.session.post(
|
|
1573
|
+
f"{self.protocol}://{self.server}/api/entity/actions/merges", data=xml_request, headers=headers)
|
|
1574
|
+
if request.status_code == requests.codes.accepted:
|
|
1575
|
+
return request.content.decode('utf-8')
|
|
1576
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1577
|
+
self.token = self.__token__()
|
|
1578
|
+
return self.merge_assets(assets, title, description)
|
|
1579
|
+
else:
|
|
1580
|
+
exception = HTTPException(
|
|
1581
|
+
"",
|
|
1582
|
+
request.status_code,
|
|
1583
|
+
request.url,
|
|
1584
|
+
"merge_assets",
|
|
1585
|
+
request.content.decode("utf-8"),
|
|
1586
|
+
)
|
|
1587
|
+
logger.error(exception)
|
|
1588
|
+
raise exception
|
|
1589
|
+
|
|
1590
|
+
def merge_folder(self, folder: Folder) -> str:
|
|
1591
|
+
"""
|
|
1592
|
+
Create a new Asset with the content from each Asset in the Folder
|
|
1593
|
+
|
|
1594
|
+
This call will create a new multipart Asset which contains all the content from the Folder.
|
|
1595
|
+
|
|
1596
|
+
The new Asset which is created will have the same title, description and parent as the Folder.
|
|
1597
|
+
|
|
1598
|
+
The return value is the progress status of the merge operation.
|
|
1599
|
+
"""
|
|
1600
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8', 'accept': 'text/plain;charset=UTF-8'}
|
|
1601
|
+
payload = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
1602
|
+
<MergeAction xmlns="{self.entity_ns}" xmlns:xip="{self.xip_ns}">
|
|
1603
|
+
<Title>{folder.title}</Title>
|
|
1604
|
+
<Description>{folder.description}</Description>
|
|
1605
|
+
<Entity excludeIdentifiers="true" excludeLinks="true" excludeMetadata="true" ref="{folder.reference}" type="SO"/>
|
|
1606
|
+
</MergeAction>"""
|
|
1607
|
+
request = self.session.post(
|
|
1608
|
+
f"{self.protocol}://{self.server}/api/entity/actions/merges", data=payload, headers=headers)
|
|
1609
|
+
if request.status_code == requests.codes.accepted:
|
|
1610
|
+
return request.content.decode('utf-8')
|
|
818
1611
|
elif request.status_code == requests.codes.unauthorized:
|
|
819
1612
|
self.token = self.__token__()
|
|
820
|
-
return self.
|
|
1613
|
+
return self.merge_folder(folder)
|
|
821
1614
|
else:
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1615
|
+
exception = HTTPException(
|
|
1616
|
+
folder.reference,
|
|
1617
|
+
request.status_code,
|
|
1618
|
+
request.url,
|
|
1619
|
+
"merge_folder",
|
|
1620
|
+
request.content.decode("utf-8"),
|
|
1621
|
+
)
|
|
1622
|
+
logger.error(exception)
|
|
1623
|
+
raise exception
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def xml_asset(self, reference: str) -> str:
|
|
825
1627
|
"""
|
|
826
|
-
|
|
1628
|
+
Retrieve an Asset by its reference
|
|
827
1629
|
|
|
828
|
-
|
|
1630
|
+
Returns an XML document of the full Asset
|
|
829
1631
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1632
|
+
:param reference: The unique identifier of the entity
|
|
1633
|
+
"""
|
|
1634
|
+
headers = {HEADER_TOKEN: self.token}
|
|
1635
|
+
params = {"expand": "structure"}
|
|
1636
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}/{reference}', params=params, headers=headers)
|
|
833
1637
|
if request.status_code == requests.codes.ok:
|
|
834
1638
|
xml_response = str(request.content.decode('utf-8'))
|
|
835
|
-
|
|
836
|
-
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
837
|
-
content = entity_response.find(f'.//{{{self.xip_ns}}}Content')
|
|
838
|
-
return xml.etree.ElementTree.tostring(content[0], encoding='utf-8', method='xml').decode('utf-8')
|
|
1639
|
+
return xml_response
|
|
839
1640
|
elif request.status_code == requests.codes.unauthorized:
|
|
840
1641
|
self.token = self.__token__()
|
|
841
|
-
return self.
|
|
1642
|
+
return self.xml_asset(reference)
|
|
1643
|
+
elif request.status_code == requests.codes.not_found:
|
|
1644
|
+
exception = ReferenceNotFoundException(reference, request.status_code, request.url, "xml_asset")
|
|
1645
|
+
logger.error(exception)
|
|
1646
|
+
raise exception
|
|
842
1647
|
else:
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
"""
|
|
848
|
-
Retrieve an entity by its type and reference
|
|
1648
|
+
exception = HTTPException(reference, request.status_code, request.url, "xml_asset",
|
|
1649
|
+
request.content.decode('utf-8'))
|
|
1650
|
+
logger.error(exception)
|
|
1651
|
+
raise exception
|
|
849
1652
|
|
|
850
|
-
Returns Entity (Asset, Folder, ContentObject)
|
|
851
|
-
|
|
852
|
-
:param entity_type: The type of entity to fetch
|
|
853
|
-
:param reference: The unique identifier of the entity
|
|
854
|
-
"""
|
|
855
|
-
if entity_type is EntityType.CONTENT_OBJECT:
|
|
856
|
-
return self.content_object(reference)
|
|
857
|
-
if entity_type is EntityType.FOLDER:
|
|
858
|
-
return self.folder(reference)
|
|
859
|
-
if entity_type is EntityType.ASSET:
|
|
860
|
-
return self.asset(reference)
|
|
861
1653
|
|
|
862
1654
|
def asset(self, reference: str) -> Asset:
|
|
1655
|
+
|
|
863
1656
|
"""
|
|
864
|
-
|
|
1657
|
+
Returns an asset object back by its internal reference identifier
|
|
865
1658
|
|
|
866
|
-
|
|
1659
|
+
:param reference: The unique identifier for the asset usually its uuid
|
|
1660
|
+
:type reference: str
|
|
1661
|
+
:return: The Asset object
|
|
1662
|
+
:rtype: Asset
|
|
1663
|
+
:raises RuntimeError: if the identifier is incorrect
|
|
867
1664
|
|
|
868
|
-
:param reference: The unique identifier of the entity
|
|
869
1665
|
"""
|
|
870
1666
|
headers = {HEADER_TOKEN: self.token}
|
|
871
|
-
request = self.session.get(f'
|
|
1667
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}/{reference}', headers=headers)
|
|
872
1668
|
if request.status_code == requests.codes.ok:
|
|
873
1669
|
xml_response = str(request.content.decode('utf-8'))
|
|
874
1670
|
entity = self.entity_from_string(xml_response)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1671
|
+
asset = Asset(entity['reference'], entity['title'], entity['description'],
|
|
1672
|
+
entity['security_tag'], entity['parent'],
|
|
1673
|
+
entity['metadata'])
|
|
1674
|
+
if 'CustomType' in entity:
|
|
1675
|
+
asset.custom_type = entity['CustomType']
|
|
1676
|
+
return asset
|
|
878
1677
|
elif request.status_code == requests.codes.unauthorized:
|
|
879
1678
|
self.token = self.__token__()
|
|
880
1679
|
return self.asset(reference)
|
|
881
1680
|
elif request.status_code == requests.codes.not_found:
|
|
882
|
-
|
|
883
|
-
logger.error(
|
|
884
|
-
raise
|
|
1681
|
+
exception = ReferenceNotFoundException(reference, request.status_code, request.url, "asset")
|
|
1682
|
+
logger.error(exception)
|
|
1683
|
+
raise exception
|
|
885
1684
|
else:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1685
|
+
exception = HTTPException(reference, request.status_code, request.url, "asset",
|
|
1686
|
+
request.content.decode('utf-8'))
|
|
1687
|
+
logger.error(exception)
|
|
1688
|
+
raise exception
|
|
889
1689
|
|
|
890
1690
|
def folder(self, reference: str) -> Folder:
|
|
891
1691
|
"""
|
|
892
|
-
|
|
1692
|
+
Returns a folder object back by its internal reference identifier
|
|
893
1693
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1694
|
+
:param reference: The unique identifier for the folder usually its uuid
|
|
1695
|
+
:type reference: str
|
|
1696
|
+
:return: The Folder object
|
|
1697
|
+
:rtype: Folder
|
|
1698
|
+
:raises RuntimeError: if the identifier is incorrect
|
|
897
1699
|
"""
|
|
898
1700
|
headers = {HEADER_TOKEN: self.token}
|
|
899
|
-
request = self.session.get(f'
|
|
1701
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{SO_PATH}/{reference}', headers=headers)
|
|
900
1702
|
if request.status_code == requests.codes.ok:
|
|
901
1703
|
xml_response = str(request.content.decode('utf-8'))
|
|
902
1704
|
entity = self.entity_from_string(xml_response)
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1705
|
+
folder = Folder(entity['reference'], entity['title'], entity['description'],
|
|
1706
|
+
entity['security_tag'], entity['parent'],
|
|
1707
|
+
entity['metadata'])
|
|
1708
|
+
if 'CustomType' in entity:
|
|
1709
|
+
folder.custom_type = entity['CustomType']
|
|
1710
|
+
return folder
|
|
906
1711
|
elif request.status_code == requests.codes.unauthorized:
|
|
907
1712
|
self.token = self.__token__()
|
|
908
1713
|
return self.folder(reference)
|
|
909
1714
|
elif request.status_code == requests.codes.not_found:
|
|
910
|
-
|
|
1715
|
+
exception = ReferenceNotFoundException(reference, request.status_code, request.url, "folder")
|
|
1716
|
+
logger.error(exception)
|
|
1717
|
+
raise exception
|
|
911
1718
|
else:
|
|
912
|
-
|
|
1719
|
+
exception = HTTPException(reference, request.status_code, request.url, "folder",
|
|
1720
|
+
request.content.decode('utf-8'))
|
|
1721
|
+
logger.error(exception)
|
|
1722
|
+
raise exception
|
|
913
1723
|
|
|
914
1724
|
def content_object(self, reference: str) -> ContentObject:
|
|
915
1725
|
"""
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
Returns ContentObject
|
|
1726
|
+
Returns a content object back by its internal reference identifier
|
|
919
1727
|
|
|
920
|
-
|
|
1728
|
+
:param reference: The unique identifier for the content object usually its uuid
|
|
1729
|
+
:type reference: str
|
|
1730
|
+
:return: The content object
|
|
1731
|
+
:rtype: ContentObject
|
|
1732
|
+
:raises RuntimeError: if the identifier is incorrect
|
|
921
1733
|
"""
|
|
922
1734
|
headers = {HEADER_TOKEN: self.token}
|
|
923
|
-
request = self.session.get(f'
|
|
1735
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{reference}', headers=headers)
|
|
924
1736
|
if request.status_code == requests.codes.ok:
|
|
925
1737
|
xml_response = str(request.content.decode('utf-8'))
|
|
926
1738
|
entity = self.entity_from_string(xml_response)
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1739
|
+
content_object = ContentObject(entity['reference'], entity['title'], entity['description'],
|
|
1740
|
+
entity['security_tag'], entity['parent'],
|
|
1741
|
+
entity['metadata'])
|
|
1742
|
+
if 'CustomType' in entity:
|
|
1743
|
+
content_object.custom_type = entity['CustomType']
|
|
1744
|
+
return content_object
|
|
930
1745
|
elif request.status_code == requests.codes.unauthorized:
|
|
931
1746
|
self.token = self.__token__()
|
|
932
1747
|
return self.content_object(reference)
|
|
933
1748
|
elif request.status_code == requests.codes.not_found:
|
|
934
|
-
|
|
1749
|
+
exception = ReferenceNotFoundException(reference, request.status_code, request.url, "content_object")
|
|
1750
|
+
logger.error(exception)
|
|
1751
|
+
raise exception
|
|
935
1752
|
else:
|
|
936
|
-
|
|
1753
|
+
exception = HTTPException(reference, request.status_code, request.url, "content_object",
|
|
1754
|
+
request.content.decode('utf-8'))
|
|
1755
|
+
logger.error(exception)
|
|
1756
|
+
raise exception
|
|
937
1757
|
|
|
938
|
-
def content_objects(self, representation: Representation) ->
|
|
1758
|
+
def content_objects(self, representation: Representation) -> list[ContentObject]:
|
|
939
1759
|
"""
|
|
940
|
-
|
|
1760
|
+
Return a list of content objects for a representation
|
|
941
1761
|
|
|
942
|
-
|
|
1762
|
+
:param representation: The representation
|
|
1763
|
+
:type representation: Representation
|
|
1764
|
+
:return: List of content objects
|
|
1765
|
+
:rtype: list(ContentObject)
|
|
943
1766
|
|
|
944
|
-
:param representation:
|
|
945
1767
|
"""
|
|
946
1768
|
headers = {HEADER_TOKEN: self.token}
|
|
947
1769
|
if not isinstance(representation, Representation):
|
|
948
|
-
logger.
|
|
949
|
-
return
|
|
1770
|
+
logger.warning("representation is not of type Representation")
|
|
1771
|
+
return []
|
|
950
1772
|
request = self.session.get(f'{representation.url}', headers=headers)
|
|
951
1773
|
if request.status_code == requests.codes.ok:
|
|
952
|
-
results =
|
|
1774
|
+
results = []
|
|
953
1775
|
xml_response = str(request.content.decode('utf-8'))
|
|
954
1776
|
logger.debug(xml_response)
|
|
955
1777
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
@@ -965,17 +1787,22 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
965
1787
|
self.token = self.__token__()
|
|
966
1788
|
return self.content_objects(representation)
|
|
967
1789
|
else:
|
|
968
|
-
|
|
969
|
-
|
|
1790
|
+
exception = HTTPException(representation.name, request.status_code, request.url, "content_objects",
|
|
1791
|
+
request.content.decode('utf-8'))
|
|
1792
|
+
logger.error(exception)
|
|
1793
|
+
raise exception
|
|
970
1794
|
|
|
971
|
-
def generation(self, url: str):
|
|
1795
|
+
def generation(self, url: str, content_ref: str = None) -> Generation:
|
|
972
1796
|
"""
|
|
973
|
-
|
|
1797
|
+
Retrieve a list of generation objects
|
|
974
1798
|
|
|
975
|
-
|
|
1799
|
+
:param url:
|
|
1800
|
+
:param content_ref:
|
|
1801
|
+
|
|
1802
|
+
:return: Generation
|
|
1803
|
+
:rtype: Generation
|
|
1804
|
+
"""
|
|
976
1805
|
|
|
977
|
-
:param url:
|
|
978
|
-
"""
|
|
979
1806
|
headers = {HEADER_TOKEN: self.token}
|
|
980
1807
|
request = self.session.get(url, headers=headers)
|
|
981
1808
|
if request.status_code == requests.codes.ok:
|
|
@@ -985,19 +1812,61 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
985
1812
|
ge = entity_response.find(f'.//{{{self.xip_ns}}}Generation')
|
|
986
1813
|
format_group = entity_response.find(f'.//{{{self.xip_ns}}}FormatGroup')
|
|
987
1814
|
effective_date = entity_response.find(f'.//{{{self.xip_ns}}}EffectiveDate')
|
|
1815
|
+
|
|
1816
|
+
formats = entity_response.findall(f'.//{{{self.xip_ns}}}Formats/{{{self.xip_ns}}}Format')
|
|
1817
|
+
formats_list = []
|
|
1818
|
+
for tech_format in formats:
|
|
1819
|
+
format_dict = {'Valid': tech_format.attrib['valid']}
|
|
1820
|
+
puid = tech_format.find(f'.//{{{self.xip_ns}}}PUID')
|
|
1821
|
+
format_dict['PUID'] = puid.text if hasattr(puid, 'text') else None
|
|
1822
|
+
priority = tech_format.find(f'.//{{{self.xip_ns}}}Priority')
|
|
1823
|
+
format_dict['Priority'] = priority.text if hasattr(priority, 'text') else None
|
|
1824
|
+
method = tech_format.find(f'.//{{{self.xip_ns}}}IdentificationMethod')
|
|
1825
|
+
format_dict['IdentificationMethod'] = method.text if hasattr(method, 'text') else None
|
|
1826
|
+
name = tech_format.find(f'.//{{{self.xip_ns}}}FormatName')
|
|
1827
|
+
format_dict['FormatName'] = name.text if hasattr(name, 'text') else None
|
|
1828
|
+
version = tech_format.find(f'.//{{{self.xip_ns}}}FormatVersion')
|
|
1829
|
+
format_dict['FormatVersion'] = version.text if hasattr(version, 'text') else None
|
|
1830
|
+
formats_list.append(format_dict)
|
|
1831
|
+
|
|
1832
|
+
index = int(url.rsplit("/", 1)[-1])
|
|
1833
|
+
|
|
1834
|
+
properties = entity_response.findall(f'.//{{{self.xip_ns}}}Properties/{{{self.xip_ns}}}Property')
|
|
1835
|
+
property_set = []
|
|
1836
|
+
for tech_props in properties:
|
|
1837
|
+
tech_props_dict = {}
|
|
1838
|
+
puid = tech_props.find(f'.//{{{self.xip_ns}}}PUID')
|
|
1839
|
+
tech_props_dict['PUID'] = puid.text if hasattr(puid, 'text') else None
|
|
1840
|
+
name = tech_props.find(f'.//{{{self.xip_ns}}}PropertyName')
|
|
1841
|
+
tech_props_dict['PropertyName'] = name.text if hasattr(name, 'text') else None
|
|
1842
|
+
value = tech_props.find(f'.//{{{self.xip_ns}}}Value')
|
|
1843
|
+
tech_props_dict['Value'] = value.text if hasattr(value, 'text') else None
|
|
1844
|
+
property_set.append(tech_props_dict)
|
|
1845
|
+
|
|
988
1846
|
bitstreams = entity_response.findall(f'./{{{self.entity_ns}}}Bitstreams/{{{self.entity_ns}}}Bitstream')
|
|
989
|
-
bitstream_list =
|
|
1847
|
+
bitstream_list = []
|
|
990
1848
|
for bit in bitstreams:
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1849
|
+
bs: Bitstream = self.bitstream(bit.text)
|
|
1850
|
+
bs.gen_index = index
|
|
1851
|
+
if content_ref is not None:
|
|
1852
|
+
bs.co_ref = content_ref
|
|
1853
|
+
bitstream_list.append(bs)
|
|
1854
|
+
generation = Generation(strtobool(ge.attrib['original']), strtobool(ge.attrib['active']),
|
|
1855
|
+
format_group.text if hasattr(format_group, 'text') else None,
|
|
1856
|
+
effective_date.text if hasattr(effective_date, 'text') else None,
|
|
1857
|
+
bitstream_list)
|
|
1858
|
+
generation.formats = formats_list
|
|
1859
|
+
generation.properties = property_set
|
|
1860
|
+
generation.gen_index = index
|
|
1861
|
+
return generation
|
|
996
1862
|
elif request.status_code == requests.codes.unauthorized:
|
|
997
1863
|
self.token = self.__token__()
|
|
998
1864
|
return self.generation(url)
|
|
999
1865
|
else:
|
|
1000
|
-
|
|
1866
|
+
exception = HTTPException(url, request.status_code, request.url, "generation",
|
|
1867
|
+
request.content.decode('utf-8'))
|
|
1868
|
+
logger.error(exception)
|
|
1869
|
+
raise exception
|
|
1001
1870
|
|
|
1002
1871
|
def _integrity_checks(self, bitstream: Bitstream, maximum: int = 10, next_page: str = None):
|
|
1003
1872
|
headers = {HEADER_TOKEN: self.token}
|
|
@@ -1016,7 +1885,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1016
1885
|
|
|
1017
1886
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Paging/{{{self.entity_ns}}}Next')
|
|
1018
1887
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}Paging/{{{self.entity_ns}}}TotalResults')
|
|
1019
|
-
results =
|
|
1888
|
+
results = []
|
|
1020
1889
|
for history in histories:
|
|
1021
1890
|
xip_type = history.find(f'./{{{self.xip_ns}}}Type')
|
|
1022
1891
|
xip_success = history.find(f'./{{{self.xip_ns}}}Success')
|
|
@@ -1039,15 +1908,18 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1039
1908
|
else:
|
|
1040
1909
|
url = next_url.text
|
|
1041
1910
|
|
|
1042
|
-
return PagedSet(results, has_more, total_hits.text, url)
|
|
1911
|
+
return PagedSet(results, has_more, int(total_hits.text), url)
|
|
1043
1912
|
|
|
1044
1913
|
elif request.status_code == requests.codes.unauthorized:
|
|
1045
1914
|
self.token = self.__token__()
|
|
1046
1915
|
return self._integrity_checks(bitstream, maximum, next_page)
|
|
1047
1916
|
else:
|
|
1048
|
-
|
|
1917
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "_integrity_checks",
|
|
1918
|
+
request.content.decode('utf-8'))
|
|
1919
|
+
logger.error(exception)
|
|
1920
|
+
raise exception
|
|
1049
1921
|
|
|
1050
|
-
def integrity_checks(self, bitstream: Bitstream):
|
|
1922
|
+
def integrity_checks(self, bitstream: Bitstream) -> Generator:
|
|
1051
1923
|
"""
|
|
1052
1924
|
Return integrity checks for a bitstream
|
|
1053
1925
|
|
|
@@ -1061,13 +1933,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1061
1933
|
for entity in paged_set.results:
|
|
1062
1934
|
yield entity
|
|
1063
1935
|
|
|
1064
|
-
def bitstream(self, url: str):
|
|
1936
|
+
def bitstream(self, url: str) -> Bitstream:
|
|
1065
1937
|
"""
|
|
1066
|
-
|
|
1938
|
+
Fetch a bitstream object from the server using its URL
|
|
1067
1939
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1940
|
+
:param url: The URL to the bitstream
|
|
1941
|
+
:type url: str
|
|
1942
|
+
:return: a bitstream object
|
|
1943
|
+
:rtype: Bitstream
|
|
1071
1944
|
"""
|
|
1072
1945
|
headers = {HEADER_TOKEN: self.token}
|
|
1073
1946
|
request = self.session.get(url, headers=headers)
|
|
@@ -1079,26 +1952,40 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1079
1952
|
filesize = entity_response.find(f'.//{{{self.xip_ns}}}FileSize')
|
|
1080
1953
|
fixity_values = entity_response.findall(f'.//{{{self.xip_ns}}}Fixity')
|
|
1081
1954
|
content = entity_response.find(f'.//{{{self.entity_ns}}}Content')
|
|
1082
|
-
|
|
1955
|
+
|
|
1956
|
+
index = int(url.rsplit("/", 1)[-1])
|
|
1957
|
+
|
|
1958
|
+
fixity = {}
|
|
1083
1959
|
for f in fixity_values:
|
|
1084
1960
|
fixity[f[0].text] = f[1].text
|
|
1085
1961
|
bitstream = Bitstream(filename.text if hasattr(filename, 'text') else None,
|
|
1086
|
-
filesize.text if hasattr(filesize, 'text') else None, fixity,
|
|
1962
|
+
int(filesize.text) if hasattr(filesize, 'text') else None, fixity,
|
|
1087
1963
|
content.text if hasattr(content, 'text') else None)
|
|
1964
|
+
|
|
1965
|
+
bitstream.bs_index = index
|
|
1088
1966
|
return bitstream
|
|
1089
1967
|
elif request.status_code == requests.codes.unauthorized:
|
|
1090
1968
|
self.token = self.__token__()
|
|
1091
1969
|
return self.bitstream(url)
|
|
1092
1970
|
else:
|
|
1093
|
-
|
|
1094
|
-
|
|
1971
|
+
exception = HTTPException(url, request.status_code, request.url, "bitstream",
|
|
1972
|
+
request.content.decode('utf-8'))
|
|
1973
|
+
logger.error(exception)
|
|
1974
|
+
raise exception
|
|
1095
1975
|
|
|
1096
1976
|
def replace_generation_sync(self, content_object: ContentObject, file_name, fixity_algorithm=None,
|
|
1097
1977
|
fixity_value=None) -> str:
|
|
1098
1978
|
"""
|
|
1099
|
-
|
|
1979
|
+
Replace the last active generation of a content object with a new digital file.
|
|
1100
1980
|
|
|
1101
|
-
|
|
1981
|
+
Starts the workflow and blocks until the workflow completes.
|
|
1982
|
+
|
|
1983
|
+
:param ContentObject content_object: The content object to replace
|
|
1984
|
+
:param str file_name: The path to the new content object
|
|
1985
|
+
:param str fixity_algorithm: Optional fixity algorithm
|
|
1986
|
+
:param str fixity_value: Optional fixity value
|
|
1987
|
+
:return: Completed workflow status
|
|
1988
|
+
:rtype: str
|
|
1102
1989
|
|
|
1103
1990
|
"""
|
|
1104
1991
|
|
|
@@ -1117,7 +2004,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1117
2004
|
"""
|
|
1118
2005
|
Replace the last active generation of a content object with a new digital file.
|
|
1119
2006
|
|
|
1120
|
-
Starts the workflow and returns
|
|
2007
|
+
Starts the workflow and returns a process ID
|
|
2008
|
+
|
|
2009
|
+
:param ContentObject content_object: The content object to replace
|
|
2010
|
+
:param str file_name: The path to the new content object
|
|
2011
|
+
:param str fixity_algorithm: Optional fixity algorithm
|
|
2012
|
+
:param str fixity_value: Optional fixity value
|
|
2013
|
+
:return: Process ID
|
|
2014
|
+
:rtype: str
|
|
1121
2015
|
|
|
1122
2016
|
"""
|
|
1123
2017
|
if (self.major_version < 7) and (self.minor_version < 2) and (self.patch_version < 1):
|
|
@@ -1131,7 +2025,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1131
2025
|
bitstream = generation.bitstreams.pop()
|
|
1132
2026
|
for algo, value in bitstream.fixity.items():
|
|
1133
2027
|
fixity_algorithm = algo
|
|
1134
|
-
|
|
2028
|
+
if "MD5" in fixity_algorithm.upper():
|
|
2029
|
+
fixity_value = FileHash(hashlib.md5)(file_name)
|
|
2030
|
+
if "SHA1" in fixity_algorithm.upper() or "SHA-1" in fixity_algorithm.upper():
|
|
2031
|
+
fixity_value = FileHash(hashlib.sha1)(file_name)
|
|
2032
|
+
if "SHA256" in fixity_algorithm.upper() or "SHA-256" in fixity_algorithm.upper():
|
|
2033
|
+
fixity_value = FileHash(hashlib.sha256)(file_name)
|
|
2034
|
+
if "SHA512" in fixity_algorithm.upper() or "SHA-512" in fixity_algorithm.upper():
|
|
2035
|
+
fixity_value = FileHash(hashlib.sha512)(file_name)
|
|
1135
2036
|
|
|
1136
2037
|
if fixity_algorithm and fixity_value:
|
|
1137
2038
|
if "MD5" in fixity_algorithm.upper():
|
|
@@ -1148,7 +2049,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1148
2049
|
|
|
1149
2050
|
with open(file_name, 'rb') as f:
|
|
1150
2051
|
request = self.session.post(
|
|
1151
|
-
f'
|
|
2052
|
+
f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{content_object.reference}/generations',
|
|
1152
2053
|
params=params, data=f, headers=headers)
|
|
1153
2054
|
|
|
1154
2055
|
if request.status_code == requests.codes.ok:
|
|
@@ -1157,71 +2058,107 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1157
2058
|
return self.replace_generation_async(content_object=content_object, file_name=file_name,
|
|
1158
2059
|
fixity_algorithm=fixity_algorithm, fixity_value=fixity_value)
|
|
1159
2060
|
else:
|
|
1160
|
-
|
|
2061
|
+
exception = HTTPException(content_object.reference, request.status_code, request.url,
|
|
2062
|
+
"replace_generation_async", request.content.decode('utf-8'))
|
|
2063
|
+
logger.error(exception)
|
|
2064
|
+
raise exception
|
|
1161
2065
|
|
|
1162
|
-
def generations(self, content_object: ContentObject) -> list:
|
|
2066
|
+
def generations(self, content_object: ContentObject) -> list[Generation]:
|
|
1163
2067
|
"""
|
|
1164
|
-
|
|
2068
|
+
Return a list of Generation objects for a content object
|
|
1165
2069
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
:
|
|
2070
|
+
:param content_object: The content object
|
|
2071
|
+
:type content_object: ContentObject
|
|
2072
|
+
:return: list of generations
|
|
2073
|
+
:rtype: list(Generation)
|
|
1169
2074
|
"""
|
|
1170
2075
|
headers = {HEADER_TOKEN: self.token}
|
|
1171
2076
|
request = self.session.get(
|
|
1172
|
-
f'
|
|
2077
|
+
f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{content_object.reference}/generations',
|
|
2078
|
+
headers=headers)
|
|
1173
2079
|
if request.status_code == requests.codes.ok:
|
|
1174
2080
|
xml_response = str(request.content.decode('utf-8'))
|
|
1175
2081
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
1176
2082
|
generations = entity_response.findall(f'.//{{{self.entity_ns}}}Generation')
|
|
1177
|
-
result =
|
|
2083
|
+
result = []
|
|
1178
2084
|
for g in generations:
|
|
1179
2085
|
if hasattr(g, 'text'):
|
|
1180
|
-
generation = self.generation(g.text)
|
|
2086
|
+
generation = self.generation(g.text, content_object.reference)
|
|
2087
|
+
generation.asset = content_object.asset
|
|
1181
2088
|
generation.content_object = content_object
|
|
2089
|
+
generation.representation_type = content_object.representation_type
|
|
1182
2090
|
result.append(generation)
|
|
1183
2091
|
return result
|
|
1184
2092
|
elif request.status_code == requests.codes.unauthorized:
|
|
1185
2093
|
self.token = self.__token__()
|
|
1186
2094
|
return self.generations(content_object)
|
|
1187
2095
|
else:
|
|
1188
|
-
|
|
2096
|
+
exception = HTTPException(content_object.reference, request.status_code, request.url,
|
|
2097
|
+
"generations", request.content.decode('utf-8'))
|
|
2098
|
+
logger.error(exception)
|
|
2099
|
+
raise exception
|
|
2100
|
+
|
|
2101
|
+
def bitstreams_for_asset(self, asset: Union[Asset, Entity]) -> Iterable[Bitstream]:
|
|
2102
|
+
"""
|
|
2103
|
+
Return all the active bitstreams within an asset.
|
|
2104
|
+
This includes all the representations and content objects
|
|
2105
|
+
|
|
2106
|
+
:param asset: The asset
|
|
2107
|
+
:return: Iterable
|
|
2108
|
+
"""
|
|
1189
2109
|
|
|
1190
|
-
|
|
2110
|
+
for representation in self.representations(asset):
|
|
2111
|
+
for content_object in self.content_objects(representation):
|
|
2112
|
+
for generation in self.generations(content_object):
|
|
2113
|
+
if generation.active:
|
|
2114
|
+
for bitstream in generation.bitstreams:
|
|
2115
|
+
bitstream.representation = representation
|
|
2116
|
+
bitstream.content_object = content_object
|
|
2117
|
+
bitstream.generation = generation
|
|
2118
|
+
yield bitstream
|
|
2119
|
+
|
|
2120
|
+
def representations(self, asset: Asset) -> set[Representation]:
|
|
1191
2121
|
"""
|
|
1192
|
-
|
|
2122
|
+
Return a set of representations for the asset
|
|
1193
2123
|
|
|
1194
|
-
|
|
2124
|
+
Representations are used to define how the information object are composed in terms of technology and structure.
|
|
1195
2125
|
|
|
1196
|
-
:param asset:
|
|
2126
|
+
:param asset: The asset containing the required representations
|
|
2127
|
+
:type asset: Asset
|
|
2128
|
+
:return: Set of Representation objects
|
|
2129
|
+
:rtype: set(Representation)
|
|
1197
2130
|
"""
|
|
1198
2131
|
headers = {HEADER_TOKEN: self.token}
|
|
1199
2132
|
if not isinstance(asset, Asset):
|
|
1200
|
-
return
|
|
1201
|
-
request = self.session.get(
|
|
1202
|
-
|
|
2133
|
+
return set()
|
|
2134
|
+
request = self.session.get(
|
|
2135
|
+
f'{self.protocol}://{self.server}/api/entity/{asset.path}/{asset.reference}/representations',
|
|
2136
|
+
headers=headers)
|
|
1203
2137
|
if request.status_code == requests.codes.ok:
|
|
1204
2138
|
xml_response = str(request.content.decode('utf-8'))
|
|
1205
2139
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
1206
2140
|
representations = entity_response.findall(f'.//{{{self.entity_ns}}}Representation')
|
|
1207
2141
|
result = set()
|
|
1208
2142
|
for r in representations:
|
|
1209
|
-
representation = Representation(asset, r.get('type'), r.get("name",
|
|
2143
|
+
representation = Representation(asset, r.get('type'), r.get("name", None), r.text)
|
|
1210
2144
|
result.add(representation)
|
|
1211
2145
|
return result
|
|
1212
2146
|
elif request.status_code == requests.codes.unauthorized:
|
|
1213
2147
|
self.token = self.__token__()
|
|
1214
2148
|
return self.representations(asset)
|
|
1215
2149
|
else:
|
|
1216
|
-
|
|
2150
|
+
exception = HTTPException(asset.reference, request.status_code, request.url,
|
|
2151
|
+
"representations", request.content.decode('utf-8'))
|
|
2152
|
+
logger.error(exception)
|
|
2153
|
+
raise exception
|
|
1217
2154
|
|
|
1218
2155
|
def remove_thumbnail(self, entity: Entity):
|
|
1219
2156
|
"""
|
|
1220
|
-
|
|
2157
|
+
Remove the thumbnail for the entity to the uploaded image
|
|
1221
2158
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
2159
|
+
:param entity: The entity with the thumbnail
|
|
2160
|
+
:type entity: Entity
|
|
2161
|
+
"""
|
|
1225
2162
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1226
2163
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1227
2164
|
|
|
@@ -1230,24 +2167,68 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1230
2167
|
|
|
1231
2168
|
headers = {HEADER_TOKEN: self.token}
|
|
1232
2169
|
|
|
1233
|
-
request = self.session.delete(
|
|
1234
|
-
|
|
2170
|
+
request = self.session.delete(
|
|
2171
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/preview',
|
|
2172
|
+
headers=headers)
|
|
1235
2173
|
if request.status_code == requests.codes.no_content:
|
|
1236
2174
|
return str(request.content.decode('utf-8'))
|
|
1237
2175
|
elif request.status_code == requests.codes.unauthorized:
|
|
1238
2176
|
self.token = self.__token__()
|
|
1239
2177
|
return self.remove_thumbnail(entity)
|
|
1240
2178
|
else:
|
|
1241
|
-
|
|
2179
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2180
|
+
"remove_thumbnail", request.content.decode('utf-8'))
|
|
2181
|
+
logger.error(exception)
|
|
2182
|
+
raise exception
|
|
2183
|
+
|
|
2184
|
+
|
|
2185
|
+
def add_access_representation(self, entity: Entity, access_file: str, name: str = "Access"):
|
|
2186
|
+
"""
|
|
2187
|
+
Add a new Access representation to an existing asset.
|
|
2188
|
+
|
|
2189
|
+
:param Entity entity: The existing asset which will receive the new representation
|
|
2190
|
+
:param str access_file: The new digital file
|
|
2191
|
+
:param str name: The name of the new access representation defaults to "Access"
|
|
2192
|
+
:return:
|
|
2193
|
+
"""
|
|
2194
|
+
|
|
2195
|
+
if self.major_version < 7 and self.minor_version < 12:
|
|
2196
|
+
raise RuntimeError("Add Representation API is only available when connected to a v6.12 System")
|
|
2197
|
+
|
|
2198
|
+
if isinstance(entity, Folder) or isinstance(entity, ContentObject):
|
|
2199
|
+
raise RuntimeError("Add Representation cannot be added to Folders and Content Objects")
|
|
2200
|
+
|
|
2201
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
2202
|
+
|
|
2203
|
+
filename = os.path.basename(access_file)
|
|
2204
|
+
|
|
2205
|
+
params = {'type': 'Access', 'name': name, 'filename': filename}
|
|
2206
|
+
|
|
2207
|
+
with open(access_file, 'rb') as fd:
|
|
2208
|
+
request = self.session.post(
|
|
2209
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/representations',
|
|
2210
|
+
data=fd, headers=headers, params=params)
|
|
2211
|
+
if request.status_code == requests.codes.accepted:
|
|
2212
|
+
return str(request.content.decode('utf-8'))
|
|
2213
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
2214
|
+
self.token = self.__token__()
|
|
2215
|
+
return self.add_access_representation(entity, access_file, name)
|
|
2216
|
+
else:
|
|
2217
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2218
|
+
"add_access_representation", request.content.decode('utf-8'))
|
|
2219
|
+
logger.error(exception)
|
|
2220
|
+
raise exception
|
|
1242
2221
|
|
|
1243
2222
|
def add_thumbnail(self, entity: Entity, image_file: str):
|
|
1244
2223
|
"""
|
|
1245
|
-
|
|
2224
|
+
Set the thumbnail for the entity to the uploaded image
|
|
1246
2225
|
|
|
2226
|
+
Supported image formats are png, jpeg, tiff, gif and bmp. The image must be 10MB or less in size.
|
|
2227
|
+
|
|
2228
|
+
:param Entity entity: The entity
|
|
2229
|
+
:param str image_file: The path to the image
|
|
2230
|
+
"""
|
|
1247
2231
|
|
|
1248
|
-
:param entity: The Entity
|
|
1249
|
-
:param image_file: Path to image file
|
|
1250
|
-
"""
|
|
1251
2232
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1252
2233
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1253
2234
|
|
|
@@ -1256,9 +2237,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1256
2237
|
|
|
1257
2238
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
1258
2239
|
|
|
1259
|
-
with open(image_file, 'rb') as
|
|
1260
|
-
request = self.session.put(
|
|
1261
|
-
|
|
2240
|
+
with open(image_file, 'rb') as fd:
|
|
2241
|
+
request = self.session.put(
|
|
2242
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/preview',
|
|
2243
|
+
data=fd, headers=headers)
|
|
1262
2244
|
|
|
1263
2245
|
if request.status_code == requests.codes.no_content:
|
|
1264
2246
|
return str(request.content.decode('utf-8'))
|
|
@@ -1266,75 +2248,417 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1266
2248
|
self.token = self.__token__()
|
|
1267
2249
|
return self.add_thumbnail(entity, image_file)
|
|
1268
2250
|
else:
|
|
1269
|
-
|
|
2251
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2252
|
+
"add_thumbnail", request.content.decode('utf-8'))
|
|
2253
|
+
logger.error(exception)
|
|
2254
|
+
raise exception
|
|
2255
|
+
|
|
2256
|
+
def _event_actions(self, entity: Entity, maximum: int):
|
|
2257
|
+
"""
|
|
2258
|
+
event actions performed against this entity
|
|
2259
|
+
"""
|
|
2260
|
+
if self.major_version < 7 and self.minor_version < 1:
|
|
2261
|
+
logger.error("Entity events is only available when connected to a v6.1 System")
|
|
2262
|
+
raise RuntimeError("Entity events is only available when connected to a v6.1 System")
|
|
2263
|
+
|
|
2264
|
+
headers = {HEADER_TOKEN: self.token}
|
|
2265
|
+
params = {'start': str(0), 'max': str(maximum)}
|
|
2266
|
+
|
|
2267
|
+
request = self.session.get(
|
|
2268
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/event-actions',
|
|
2269
|
+
params=params, headers=headers)
|
|
2270
|
+
|
|
2271
|
+
if request.status_code == requests.codes.ok:
|
|
2272
|
+
return None
|
|
2273
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
2274
|
+
self.token = self.__token__()
|
|
2275
|
+
return self._event_actions(entity, maximum=maximum)
|
|
2276
|
+
else:
|
|
2277
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2278
|
+
"_event_actions", request.content.decode('utf-8'))
|
|
2279
|
+
logger.error(exception)
|
|
2280
|
+
raise exception
|
|
1270
2281
|
|
|
1271
|
-
def all_descendants(self,
|
|
2282
|
+
def all_descendants(self, folder: Union[Folder, Entity] = None) -> Generator[Entity, None, None]:
|
|
1272
2283
|
"""
|
|
1273
|
-
|
|
2284
|
+
Return all child entities recursively of a folder or repository down to the assets using a lazy iterator.
|
|
2285
|
+
The paging is done internally using a default page
|
|
2286
|
+
size of 100 elements. Callers can iterate over the result to get all children with a single call.
|
|
1274
2287
|
|
|
1275
|
-
|
|
2288
|
+
:param str folder: The parent folder reference, None for the children of root folders
|
|
2289
|
+
:return: A set of entity objects (Folders and Assets)
|
|
2290
|
+
:rtype: set(Entity)
|
|
1276
2291
|
|
|
1277
|
-
:param folder_reference: The folder to find children of
|
|
1278
2292
|
"""
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2293
|
+
for entity in self.descendants(folder=folder):
|
|
2294
|
+
yield entity
|
|
2295
|
+
if entity.entity_type == EntityType.FOLDER:
|
|
2296
|
+
yield from self.all_descendants(folder=entity)
|
|
2297
|
+
|
|
2298
|
+
def descendants(self, folder: Union[str, Folder] = None) -> Generator[Entity, None, None]:
|
|
2299
|
+
|
|
2300
|
+
"""
|
|
2301
|
+
Return the immediate child entities of a folder using a lazy iterator. The paging is done internally using a default page
|
|
2302
|
+
size of 100 elements. Callers can iterate over the result to get all children with a single call.
|
|
2303
|
+
|
|
2304
|
+
:param str folder: The parent folder reference, None for the children of root folders
|
|
2305
|
+
:return: A set of entity objects (Folders and Assets)
|
|
2306
|
+
:rtype: set(Entity)
|
|
1284
2307
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
2308
|
+
"""
|
|
2309
|
+
|
|
2310
|
+
maximum = 100
|
|
2311
|
+
paged_set = self.children(folder, maximum=maximum, next_page=None)
|
|
1288
2312
|
for entity in paged_set.results:
|
|
1289
2313
|
yield entity
|
|
1290
2314
|
while paged_set.has_more:
|
|
1291
|
-
paged_set = self.children(
|
|
2315
|
+
paged_set = self.children(folder, maximum=maximum, next_page=paged_set.next_page)
|
|
1292
2316
|
for entity in paged_set.results:
|
|
1293
2317
|
yield entity
|
|
1294
2318
|
|
|
1295
|
-
|
|
2319
|
+
|
|
2320
|
+
|
|
2321
|
+
def children(self, folder: Union[str, Folder] = None, maximum: int = 100, next_page: str = None) -> PagedSet:
|
|
2322
|
+
|
|
2323
|
+
"""
|
|
2324
|
+
Return the child entities of a folder one page at a time. The caller is responsible for
|
|
2325
|
+
requesting the next page of results.
|
|
2326
|
+
|
|
2327
|
+
This function is deprecated, use descendants instead as the paging is automatic
|
|
2328
|
+
|
|
2329
|
+
:param str folder: The parent folder reference, None for the children of root folders
|
|
2330
|
+
:param int maximum: The maximum size of the result set in each page
|
|
2331
|
+
:param str next_page: A URL for the next page of results
|
|
2332
|
+
:return: A set of entity objects
|
|
2333
|
+
:rtype: set(Entity)
|
|
2334
|
+
"""
|
|
2335
|
+
|
|
1296
2336
|
headers = {HEADER_TOKEN: self.token}
|
|
1297
2337
|
data = {'start': str(0), 'max': str(maximum)}
|
|
2338
|
+
|
|
2339
|
+
if isinstance(folder, Folder):
|
|
2340
|
+
folder_reference = folder.reference
|
|
2341
|
+
else:
|
|
2342
|
+
folder_reference = folder
|
|
1298
2343
|
if next_page is None:
|
|
1299
2344
|
if folder_reference is None:
|
|
1300
|
-
request = self.session.get(f'
|
|
2345
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/root/children', params=data,
|
|
1301
2346
|
headers=headers)
|
|
1302
2347
|
else:
|
|
2348
|
+
if hasattr(folder, "reference"):
|
|
2349
|
+
folder_reference = folder.reference
|
|
1303
2350
|
request = self.session.get(
|
|
1304
|
-
f'
|
|
1305
|
-
|
|
2351
|
+
f'{self.protocol}://{self.server}/api/entity/structural-objects/{folder_reference}/children',
|
|
2352
|
+
params=data, headers=headers)
|
|
1306
2353
|
else:
|
|
1307
2354
|
request = self.session.get(next_page, headers=headers)
|
|
2355
|
+
logger.debug(request.url)
|
|
1308
2356
|
if request.status_code == requests.codes.ok:
|
|
1309
2357
|
xml_response = str(request.content.decode('utf-8'))
|
|
2358
|
+
logger.debug(xml_response)
|
|
1310
2359
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
1311
2360
|
children = entity_response.findall(f'.//{{{self.entity_ns}}}Child')
|
|
1312
2361
|
result = set()
|
|
1313
2362
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
1314
2363
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
1315
|
-
for
|
|
1316
|
-
if
|
|
1317
|
-
|
|
1318
|
-
result.add(
|
|
2364
|
+
for child in children:
|
|
2365
|
+
if child.attrib['type'] == EntityType.FOLDER.value:
|
|
2366
|
+
folder = Folder(child.attrib['ref'], child.attrib['title'], None, None, folder_reference, None)
|
|
2367
|
+
result.add(folder)
|
|
1319
2368
|
else:
|
|
1320
|
-
|
|
1321
|
-
result.add(
|
|
2369
|
+
asset = Asset(child.attrib['ref'], child.attrib['title'], None, None, folder_reference, None)
|
|
2370
|
+
result.add(asset)
|
|
1322
2371
|
has_more = True
|
|
1323
2372
|
url = None
|
|
1324
2373
|
if next_url is None:
|
|
1325
2374
|
has_more = False
|
|
1326
2375
|
else:
|
|
1327
2376
|
url = next_url.text
|
|
1328
|
-
return PagedSet(result, has_more, total_hits.text, url)
|
|
2377
|
+
return PagedSet(result, has_more, int(total_hits.text), url)
|
|
1329
2378
|
elif request.status_code == requests.codes.unauthorized:
|
|
1330
2379
|
self.token = self.__token__()
|
|
1331
2380
|
return self.children(folder_reference, maximum=maximum, next_page=next_page)
|
|
1332
2381
|
else:
|
|
1333
|
-
|
|
2382
|
+
exception = HTTPException(folder_reference, request.status_code, request.url,
|
|
2383
|
+
"children", request.content.decode('utf-8'))
|
|
2384
|
+
logger.error(exception)
|
|
2385
|
+
raise exception
|
|
2386
|
+
|
|
2387
|
+
def all_ingest_events(self, previous_days: int = 1) -> Generator:
|
|
2388
|
+
"""
|
|
2389
|
+
Returns a list of ingest only events for the user's tenancy
|
|
2390
|
+
|
|
2391
|
+
This method uses a generator function to make repeated calls to the server for every page of results.
|
|
2392
|
+
|
|
2393
|
+
:param int previous_days: The number of days to look back for events
|
|
2394
|
+
:return: A generator of events
|
|
2395
|
+
:rtype: Generator
|
|
2396
|
+
"""
|
|
2397
|
+
|
|
2398
|
+
self.token = self.__token__()
|
|
2399
|
+
previous = datetime.utcnow() - timedelta(days=previous_days)
|
|
2400
|
+
from_date = previous.replace(tzinfo=timezone.utc).isoformat()
|
|
2401
|
+
to_date = datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()
|
|
2402
|
+
paged_set = self._all_events_page(maximum=25, next_page=None, type="Ingest", from_date=from_date,
|
|
2403
|
+
to_date=to_date)
|
|
2404
|
+
for entity in paged_set.results:
|
|
2405
|
+
yield entity
|
|
2406
|
+
while paged_set.has_more:
|
|
2407
|
+
paged_set = self._all_events_page(maximum=25, next_page=paged_set.next_page, type="Ingest",
|
|
2408
|
+
from_date=from_date, to_date=to_date)
|
|
2409
|
+
for entity in paged_set.results:
|
|
2410
|
+
yield entity
|
|
2411
|
+
|
|
2412
|
+
def all_events(self) -> Generator:
|
|
2413
|
+
"""
|
|
2414
|
+
Returns a list of events for the user's tenancy
|
|
2415
|
+
|
|
2416
|
+
This method uses a generator function to make repeated calls to the server for every page of results.
|
|
2417
|
+
|
|
2418
|
+
:return: A generator of events
|
|
2419
|
+
:rtype: Generator
|
|
2420
|
+
"""
|
|
2421
|
+
self.token = self.__token__()
|
|
2422
|
+
paged_set = self._all_events_page()
|
|
2423
|
+
for entity in paged_set.results:
|
|
2424
|
+
yield entity
|
|
2425
|
+
while paged_set.has_more:
|
|
2426
|
+
paged_set = self._all_events_page(next_page=paged_set.next_page)
|
|
2427
|
+
for entity in paged_set.results:
|
|
2428
|
+
yield entity
|
|
2429
|
+
|
|
2430
|
+
def _entity_from_event_page(self, event_id: str, maximum: int = 25, next_page: str = None):
|
|
2431
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
2432
|
+
if next_page is None:
|
|
2433
|
+
url = f'{self.protocol}://{self.server}/api/entity/events/{event_id}/event-actions'
|
|
2434
|
+
response = requests.get(url, params={'start': 0, 'max': maximum}, headers=headers)
|
|
2435
|
+
else:
|
|
2436
|
+
response = requests.get(next_page, headers=headers)
|
|
2437
|
+
if response.status_code == requests.codes.unauthorized:
|
|
2438
|
+
self.token = self.__token__()
|
|
2439
|
+
return self._entity_from_event_page(event_id, maximum, next_page)
|
|
2440
|
+
if response.status_code == 200:
|
|
2441
|
+
xml_response = str(response.content.decode('utf-8'))
|
|
2442
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
2443
|
+
actions = entity_response.findall(f'.//{{{self.xip_ns}}}EventAction')
|
|
2444
|
+
result_list = []
|
|
2445
|
+
for action in actions:
|
|
2446
|
+
item: dict = {}
|
|
2447
|
+
event = action.find(f'.//{{{self.xip_ns}}}Event')
|
|
2448
|
+
event_type = event.attrib["type"]
|
|
2449
|
+
item['EventType'] = event_type
|
|
2450
|
+
entity_date = action.find(f'.//{{{self.xip_ns}}}Date')
|
|
2451
|
+
item['Date'] = entity_date.text
|
|
2452
|
+
entity_ref = action.find(f'.//{{{self.xip_ns}}}Entity')
|
|
2453
|
+
item['Entity'] = entity_ref.text
|
|
2454
|
+
result_list.append(item)
|
|
2455
|
+
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
2456
|
+
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
2457
|
+
has_more = True
|
|
2458
|
+
url = None
|
|
2459
|
+
if next_url is None:
|
|
2460
|
+
has_more = False
|
|
2461
|
+
else:
|
|
2462
|
+
url = next_url.text
|
|
2463
|
+
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2464
|
+
return None
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def entity_from_event(self, event_id: str) -> Generator:
|
|
2470
|
+
"""
|
|
2471
|
+
Returns an entity from the user's tenancy
|
|
2472
|
+
:rtype: Generator
|
|
2473
|
+
|
|
2474
|
+
"""
|
|
2475
|
+
self.token = self.__token__()
|
|
2476
|
+
paged_set = self._entity_from_event_page(event_id, 25, None)
|
|
2477
|
+
for entity in paged_set.results:
|
|
2478
|
+
yield entity
|
|
2479
|
+
while paged_set.has_more:
|
|
2480
|
+
paged_set = self._entity_from_event_page(event_id, 25, next_page=paged_set.next_page)
|
|
2481
|
+
for entity in paged_set.results:
|
|
2482
|
+
yield entity
|
|
2483
|
+
|
|
2484
|
+
def _all_events_page(self, maximum: int = 25, next_page: str = None, **kwargs) -> PagedSet:
|
|
2485
|
+
"""
|
|
2486
|
+
event actions performed against this repository
|
|
2487
|
+
"""
|
|
2488
|
+
headers = {HEADER_TOKEN: self.token}
|
|
2489
|
+
|
|
2490
|
+
params = {'start': str(0), 'max': str(maximum)}
|
|
2491
|
+
if "type" in kwargs:
|
|
2492
|
+
params["types"] = kwargs.get("type")
|
|
2493
|
+
if "from_date" in kwargs:
|
|
2494
|
+
params["from"] = kwargs.get("from_date")
|
|
2495
|
+
if "to_date" in kwargs:
|
|
2496
|
+
params["to"] = kwargs.get("to_date")
|
|
2497
|
+
if "username" in kwargs:
|
|
2498
|
+
params["username"] = kwargs.get("username")
|
|
2499
|
+
|
|
2500
|
+
if next_page is None:
|
|
2501
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
|
|
2502
|
+
headers=headers)
|
|
2503
|
+
else:
|
|
2504
|
+
request = self.session.get(next_page, headers=headers)
|
|
2505
|
+
|
|
2506
|
+
if request.status_code == requests.codes.ok:
|
|
2507
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
2508
|
+
logger.debug(xml_response)
|
|
2509
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
2510
|
+
events = entity_response.findall(f'.//{{{self.xip_ns}}}Event')
|
|
2511
|
+
result_list = []
|
|
2512
|
+
for event in events:
|
|
2513
|
+
result = {'eventType': event.attrib['type']}
|
|
2514
|
+
date_node = event.find(f'.//{{{self.xip_ns}}}Date')
|
|
2515
|
+
result['Date'] = date_node.text if hasattr(date_node, 'text') else None
|
|
2516
|
+
user_node = event.find(f'.//{{{self.xip_ns}}}User')
|
|
2517
|
+
result['User'] = user_node.text if hasattr(user_node, 'text') else None
|
|
2518
|
+
ref_node = event.find(f'.//{{{self.xip_ns}}}Ref')
|
|
2519
|
+
result['Ref'] = ref_node.text if hasattr(ref_node, 'text') else None
|
|
2520
|
+
|
|
2521
|
+
workflow_name = event.find(f'.//{{{self.xip_ns}}}WorkflowName')
|
|
2522
|
+
if workflow_name is not None:
|
|
2523
|
+
result['WorkflowName'] = workflow_name.text
|
|
2524
|
+
|
|
2525
|
+
workflow_instance_id = event.find(f'.//{{{self.xip_ns}}}WorkflowInstanceId')
|
|
2526
|
+
if workflow_instance_id is not None:
|
|
2527
|
+
result['WorkflowInstanceId'] = workflow_instance_id.text
|
|
2528
|
+
|
|
2529
|
+
serialised_command = event.find(f'.//{{{self.xip_ns}}}SerialisedCommand')
|
|
2530
|
+
if serialised_command is not None:
|
|
2531
|
+
result['SerialisedCommand'] = serialised_command.text
|
|
2532
|
+
|
|
2533
|
+
result_list.append(result)
|
|
2534
|
+
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
2535
|
+
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
2536
|
+
has_more = True
|
|
2537
|
+
url = None
|
|
2538
|
+
if next_url is None:
|
|
2539
|
+
has_more = False
|
|
2540
|
+
else:
|
|
2541
|
+
url = next_url.text
|
|
2542
|
+
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2543
|
+
|
|
2544
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
2545
|
+
self.token = self.__token__()
|
|
2546
|
+
return self._all_events_page(maximum, next_page, **kwargs)
|
|
2547
|
+
else:
|
|
2548
|
+
exception = HTTPException("", request.status_code, request.url,
|
|
2549
|
+
"_all_events_page", request.content.decode('utf-8'))
|
|
2550
|
+
logger.error(exception)
|
|
2551
|
+
raise exception
|
|
2552
|
+
|
|
2553
|
+
def _entity_events_page(self, entity: Entity, maximum: int = 25, next_page: str = None) -> PagedSet:
|
|
2554
|
+
"""
|
|
2555
|
+
event actions performed against this entity
|
|
2556
|
+
"""
|
|
2557
|
+
if self.major_version < 7 and self.minor_version < 1:
|
|
2558
|
+
logger.error("Entity events is only available when connected to a v6.1 System")
|
|
2559
|
+
raise RuntimeError("Entity events is only available when connected to a v6.1 System")
|
|
2560
|
+
|
|
2561
|
+
headers = {HEADER_TOKEN: self.token}
|
|
2562
|
+
params = {'start': str(0), 'max': str(maximum)}
|
|
2563
|
+
if next_page is None:
|
|
2564
|
+
request = self.session.get(
|
|
2565
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/event-actions',
|
|
2566
|
+
params=params, headers=headers)
|
|
2567
|
+
else:
|
|
2568
|
+
request = self.session.get(next_page, headers=headers)
|
|
2569
|
+
|
|
2570
|
+
if request.status_code == requests.codes.ok:
|
|
2571
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
2572
|
+
logger.debug(xml_response)
|
|
2573
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
2574
|
+
event_actions = entity_response.findall(f'.//{{{self.xip_ns}}}EventAction')
|
|
2575
|
+
result_list = []
|
|
2576
|
+
for event_action in event_actions:
|
|
2577
|
+
result = {'commandType': ''}
|
|
2578
|
+
if 'commandType' in event_action.attrib:
|
|
2579
|
+
result['commandType'] = event_action.attrib['commandType']
|
|
2580
|
+
|
|
2581
|
+
event = event_action.find(f'.//{{{self.xip_ns}}}Event')
|
|
2582
|
+
if 'type' in event_action.attrib:
|
|
2583
|
+
result['eventType'] = event.attrib['type']
|
|
2584
|
+
|
|
2585
|
+
date_node = event.find(f'.//{{{self.xip_ns}}}Date')
|
|
2586
|
+
if date_node is not None:
|
|
2587
|
+
result['Date'] = date_node.text
|
|
2588
|
+
|
|
2589
|
+
user_node = event.find(f'.//{{{self.xip_ns}}}User')
|
|
2590
|
+
if user_node is not None:
|
|
2591
|
+
result['User'] = user_node.text
|
|
2592
|
+
|
|
2593
|
+
ref_node = event.find(f'.//{{{self.xip_ns}}}Ref')
|
|
2594
|
+
if ref_node is not None:
|
|
2595
|
+
result['Ref'] = ref_node.text
|
|
2596
|
+
|
|
2597
|
+
workflow_name = event.find(f'.//{{{self.xip_ns}}}WorkflowName')
|
|
2598
|
+
if workflow_name is not None:
|
|
2599
|
+
result['WorkflowName'] = workflow_name.text
|
|
2600
|
+
|
|
2601
|
+
workflow_instance_id = event.find(f'.//{{{self.xip_ns}}}WorkflowInstanceId')
|
|
2602
|
+
if workflow_instance_id is not None:
|
|
2603
|
+
result['WorkflowInstanceId'] = workflow_instance_id.text
|
|
2604
|
+
|
|
2605
|
+
serialised_command = event_action.find(f'.//{{{self.xip_ns}}}SerialisedCommand')
|
|
2606
|
+
if serialised_command is not None:
|
|
2607
|
+
result['SerialisedCommand'] = serialised_command.text
|
|
2608
|
+
|
|
2609
|
+
result_list.append(result)
|
|
2610
|
+
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
2611
|
+
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
2612
|
+
has_more = True
|
|
2613
|
+
url = None
|
|
2614
|
+
if next_url is None:
|
|
2615
|
+
has_more = False
|
|
2616
|
+
else:
|
|
2617
|
+
url = next_url.text
|
|
2618
|
+
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2619
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
2620
|
+
self.token = self.__token__()
|
|
2621
|
+
return self._entity_events_page(entity)
|
|
2622
|
+
else:
|
|
2623
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2624
|
+
"_all_events_page", request.content.decode('utf-8'))
|
|
2625
|
+
logger.error(exception)
|
|
2626
|
+
raise exception
|
|
2627
|
+
|
|
2628
|
+
def entity_events(self, entity: Entity) -> Generator:
|
|
2629
|
+
"""
|
|
2630
|
+
Returns a list of event actions performed against this entity
|
|
2631
|
+
|
|
2632
|
+
This method uses a generator function to make repeated calls to the server for every page of results.
|
|
2633
|
+
|
|
2634
|
+
:param Entity entity: The entity
|
|
2635
|
+
:return: A list of events
|
|
2636
|
+
:rtype: list
|
|
2637
|
+
|
|
2638
|
+
"""
|
|
2639
|
+
self.token = self.__token__()
|
|
2640
|
+
paged_set = self._entity_events_page(entity)
|
|
2641
|
+
for entity in paged_set.results:
|
|
2642
|
+
yield entity
|
|
2643
|
+
while paged_set.has_more:
|
|
2644
|
+
paged_set = self._entity_events_page(entity, next_page=paged_set.next_page)
|
|
2645
|
+
for entity in paged_set.results:
|
|
2646
|
+
yield entity
|
|
2647
|
+
|
|
2648
|
+
def updated_entities(self, previous_days: int = 1) -> Generator:
|
|
2649
|
+
"""
|
|
2650
|
+
Fetch a list of entities which have changed (been updated) over the previous n days.
|
|
2651
|
+
|
|
2652
|
+
This method uses a generator function to make repeated calls to the server for every page of results.
|
|
2653
|
+
|
|
2654
|
+
:param int previous_days: The number of days to check for changes.
|
|
2655
|
+
:return: A list of entities
|
|
2656
|
+
:rtype: list
|
|
2657
|
+
|
|
2658
|
+
"""
|
|
1334
2659
|
|
|
1335
|
-
def updated_entities(self, previous_days: int = 1):
|
|
1336
2660
|
self.token = self.__token__()
|
|
1337
|
-
maximum =
|
|
2661
|
+
maximum = 25
|
|
1338
2662
|
paged_set = self._updated_entities_page(previous_days=previous_days, maximum=maximum, next_page=None)
|
|
1339
2663
|
for entity in paged_set.results:
|
|
1340
2664
|
yield entity
|
|
@@ -1350,7 +2674,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1350
2674
|
today = x.replace(tzinfo=timezone.utc).isoformat()
|
|
1351
2675
|
if next_page is None:
|
|
1352
2676
|
params = {'date': today, 'start': '0', 'max': str(maximum)}
|
|
1353
|
-
request = self.session.get(f'
|
|
2677
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/entities/updated-since',
|
|
1354
2678
|
headers=headers, params=params)
|
|
1355
2679
|
else:
|
|
1356
2680
|
request = self.session.get(next_page, headers=headers)
|
|
@@ -1359,18 +2683,18 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1359
2683
|
logger.debug(xml_response)
|
|
1360
2684
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
1361
2685
|
entities = entity_response.findall(f'.//{{{self.entity_ns}}}Entity')
|
|
1362
|
-
result =
|
|
1363
|
-
for
|
|
1364
|
-
if 'type' in
|
|
1365
|
-
if
|
|
1366
|
-
|
|
1367
|
-
result.append(
|
|
1368
|
-
elif
|
|
1369
|
-
|
|
1370
|
-
result.append(
|
|
1371
|
-
elif
|
|
1372
|
-
|
|
1373
|
-
result.append(
|
|
2686
|
+
result = []
|
|
2687
|
+
for entity in entities:
|
|
2688
|
+
if 'type' in entity.attrib:
|
|
2689
|
+
if entity.attrib['type'] == EntityType.FOLDER.value:
|
|
2690
|
+
folder = Folder(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
2691
|
+
result.append(folder)
|
|
2692
|
+
elif entity.attrib['type'] == EntityType.ASSET.value:
|
|
2693
|
+
asset = Asset(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
2694
|
+
result.append(asset)
|
|
2695
|
+
elif entity.attrib['type'] == EntityType.CONTENT_OBJECT.value:
|
|
2696
|
+
co = ContentObject(entity.attrib['ref'], entity.attrib['title'], None, None, None, None)
|
|
2697
|
+
result.append(co)
|
|
1374
2698
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
1375
2699
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
1376
2700
|
has_more = True
|
|
@@ -1379,28 +2703,48 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1379
2703
|
has_more = False
|
|
1380
2704
|
else:
|
|
1381
2705
|
url = next_url.text
|
|
1382
|
-
return PagedSet(result, has_more, total_hits.text, url)
|
|
2706
|
+
return PagedSet(result, has_more, int(total_hits.text), url)
|
|
1383
2707
|
elif request.status_code == requests.codes.unauthorized:
|
|
1384
2708
|
self.token = self.__token__()
|
|
1385
2709
|
return self._updated_entities_page(previous_days=previous_days, maximum=maximum,
|
|
1386
2710
|
next_page=next_page)
|
|
1387
2711
|
else:
|
|
1388
|
-
|
|
1389
|
-
|
|
2712
|
+
exception = HTTPException(previous_days, request.status_code, request.url,
|
|
2713
|
+
"_updated_entities_page", request.content.decode('utf-8'))
|
|
2714
|
+
logger.error(exception)
|
|
2715
|
+
raise exception
|
|
2716
|
+
|
|
2717
|
+
def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2718
|
+
"""
|
|
2719
|
+
Initiate and approve the deletion of an asset.
|
|
1390
2720
|
|
|
1391
|
-
|
|
2721
|
+
:param Asset asset: The asset to delete
|
|
2722
|
+
:param str operator_comment: The comments from the operator which are added to the logs
|
|
2723
|
+
:param str supervisor_comment: The comments from the supervisor which are added to the logs
|
|
2724
|
+
:return: The asset reference
|
|
2725
|
+
:rtype: str
|
|
2726
|
+
"""
|
|
1392
2727
|
if isinstance(asset, Asset):
|
|
1393
|
-
return self.
|
|
2728
|
+
return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
|
|
1394
2729
|
else:
|
|
1395
2730
|
raise RuntimeError("delete_asset only deletes assets")
|
|
1396
2731
|
|
|
1397
|
-
def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str):
|
|
2732
|
+
def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2733
|
+
"""
|
|
2734
|
+
Initiate and approve the deletion of a folder.
|
|
2735
|
+
|
|
2736
|
+
:param Folder folder: The folder to delete
|
|
2737
|
+
:param str operator_comment: The comments from the operator which are added to the logs
|
|
2738
|
+
:param str supervisor_comment: The comments from the supervisor which are added to the logs
|
|
2739
|
+
:return: The folder reference
|
|
2740
|
+
:rtype: str
|
|
2741
|
+
"""
|
|
1398
2742
|
if isinstance(folder, Folder):
|
|
1399
|
-
return self.
|
|
2743
|
+
return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
|
|
1400
2744
|
else:
|
|
1401
2745
|
raise RuntimeError("delete_folder only deletes folders")
|
|
1402
2746
|
|
|
1403
|
-
def
|
|
2747
|
+
def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
1404
2748
|
"""
|
|
1405
2749
|
Delete an asset from the repository
|
|
1406
2750
|
|
|
@@ -1411,7 +2755,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1411
2755
|
|
|
1412
2756
|
# check manager password is available:
|
|
1413
2757
|
config = configparser.ConfigParser()
|
|
1414
|
-
config.read('
|
|
2758
|
+
config.read(credentials_path, encoding='utf-8')
|
|
1415
2759
|
try:
|
|
1416
2760
|
manager_username = config['credentials']['manager.username']
|
|
1417
2761
|
manager_password = config['credentials']['manager.password']
|
|
@@ -1419,6 +2763,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1419
2763
|
except KeyError:
|
|
1420
2764
|
raise RuntimeError("No manager password set in credentials.properties")
|
|
1421
2765
|
|
|
2766
|
+
self.token = self.__token__()
|
|
2767
|
+
|
|
1422
2768
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
1423
2769
|
xml_object = xml.etree.ElementTree.Element('DeletionAction',
|
|
1424
2770
|
{"xmlns:xip": self.xip_ns, "xmlns": self.entity_ns})
|
|
@@ -1427,17 +2773,19 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1427
2773
|
comment_el.text = operator_comment
|
|
1428
2774
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
1429
2775
|
logger.debug(xml_request)
|
|
1430
|
-
request = self.session.delete(f'
|
|
2776
|
+
request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}',
|
|
1431
2777
|
data=xml_request, headers=headers)
|
|
1432
2778
|
logger.debug(request.content.decode("utf-8"))
|
|
1433
2779
|
if request.status_code == requests.codes.accepted:
|
|
1434
2780
|
progress = request.content.decode("utf-8")
|
|
1435
|
-
req = self.session.get(f"
|
|
2781
|
+
req = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{progress}", headers=headers)
|
|
1436
2782
|
while True:
|
|
1437
2783
|
if req.status_code == requests.codes.ok:
|
|
1438
2784
|
entity_response = xml.etree.ElementTree.fromstring(req.content.decode("utf-8"))
|
|
1439
2785
|
status = entity_response.find(".//{http://status.preservica.com}Status")
|
|
1440
2786
|
if hasattr(status, 'text'):
|
|
2787
|
+
if status.text == "COMPLETED":
|
|
2788
|
+
return entity.reference
|
|
1441
2789
|
if status.text == "PENDING":
|
|
1442
2790
|
headers = {HEADER_TOKEN: self.manager_token(manager_username, manager_password),
|
|
1443
2791
|
'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -1448,19 +2796,30 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1448
2796
|
xml.etree.ElementTree.SubElement(approval_el, "Comment").text = supervisor_comment
|
|
1449
2797
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
1450
2798
|
logger.debug(xml_request)
|
|
1451
|
-
approve = self.session.put(
|
|
1452
|
-
|
|
2799
|
+
approve = self.session.put(
|
|
2800
|
+
f"{self.protocol}://{self.server}/api/entity/actions/deletions/{progress}",
|
|
2801
|
+
data=xml_request, headers=headers)
|
|
1453
2802
|
if approve.status_code == requests.codes.accepted:
|
|
1454
2803
|
return entity.reference
|
|
1455
2804
|
else:
|
|
2805
|
+
logger.error(approve.content.decode('utf-8'))
|
|
1456
2806
|
raise RuntimeError(approve.status_code, "delete_asset failed during approval")
|
|
1457
2807
|
sleep(2.0)
|
|
2808
|
+
req = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{progress}",
|
|
2809
|
+
headers=headers)
|
|
1458
2810
|
elif request.status_code == requests.codes.unauthorized:
|
|
1459
2811
|
self.token = self.__token__()
|
|
1460
|
-
return self.
|
|
2812
|
+
return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
|
|
1461
2813
|
if request.status_code == requests.codes.unprocessable:
|
|
2814
|
+
logger.error(request.content.decode('utf-8'))
|
|
1462
2815
|
raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
|
|
1463
2816
|
if request.status_code == requests.codes.forbidden:
|
|
2817
|
+
logger.error(request.content.decode('utf-8'))
|
|
1464
2818
|
raise RuntimeError(request.status_code, "User doesn't have deletion rights on the "
|
|
1465
2819
|
"entity or the required operator role to evaluate a deletion")
|
|
1466
|
-
|
|
2820
|
+
|
|
2821
|
+
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2822
|
+
"_delete_entity", request.content.decode('utf-8'))
|
|
2823
|
+
logger.error(exception)
|
|
2824
|
+
raise exception
|
|
2825
|
+
|