pyPreservica 2.0.3__py3-none-any.whl → 3.3.3__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.
Potentially problematic release.
This version of pyPreservica might be problematic. Click here for more details.
- pyPreservica/__init__.py +19 -7
- pyPreservica/adminAPI.py +43 -33
- pyPreservica/authorityAPI.py +9 -9
- pyPreservica/common.py +198 -54
- pyPreservica/contentAPI.py +199 -18
- pyPreservica/entityAPI.py +944 -250
- pyPreservica/mdformsAPI.py +572 -0
- pyPreservica/monitorAPI.py +3 -3
- pyPreservica/parAPI.py +7 -40
- pyPreservica/retentionAPI.py +58 -26
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +426 -609
- pyPreservica/webHooksAPI.py +3 -1
- pyPreservica/workflowAPI.py +21 -37
- {pyPreservica-2.0.3.dist-info → pypreservica-3.3.3.dist-info}/METADATA +93 -84
- pypreservica-3.3.3.dist-info/RECORD +20 -0
- {pyPreservica-2.0.3.dist-info → pypreservica-3.3.3.dist-info}/WHEEL +1 -1
- pyPreservica/vocabularyAPI.py +0 -141
- pyPreservica-2.0.3.dist-info/RECORD +0 -19
- {pyPreservica-2.0.3.dist-info → pypreservica-3.3.3.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-2.0.3.dist-info → pypreservica-3.3.3.dist-info}/top_level.txt +0 -0
pyPreservica/entityAPI.py
CHANGED
|
@@ -8,11 +8,15 @@ author: James Carr
|
|
|
8
8
|
licence: Apache License 2.0
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
+
|
|
12
|
+
import os.path
|
|
11
13
|
import uuid
|
|
12
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
|
-
from typing import Any, Generator, Tuple, Iterable, Union
|
|
18
|
+
from typing import Any, Generator, Tuple, Iterable, Union, Callable
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
from pyPreservica.common import *
|
|
18
22
|
|
|
@@ -31,8 +35,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
31
35
|
"""
|
|
32
36
|
|
|
33
37
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
34
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
35
|
-
|
|
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'):
|
|
40
|
+
|
|
41
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
42
|
+
protocol, request_hook, credentials_path)
|
|
43
|
+
|
|
36
44
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
37
45
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
38
46
|
|
|
@@ -40,19 +48,127 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
40
48
|
"""
|
|
41
49
|
Return security tags available for the current user
|
|
42
50
|
|
|
51
|
+
:param with_permissions: Return the permissions for each security tag
|
|
52
|
+
:type with_permissions: bool
|
|
53
|
+
|
|
43
54
|
:return: dict of security tags
|
|
44
55
|
:rtype: dict
|
|
45
56
|
"""
|
|
46
57
|
|
|
47
58
|
return self.security_tags_base(with_permissions=with_permissions)
|
|
48
59
|
|
|
49
|
-
def
|
|
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
|
|
87
|
+
|
|
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
|
|
91
|
+
|
|
92
|
+
Returns the byteIO
|
|
93
|
+
Returns None if the file does not contain the correct number of bytes (default 2k)
|
|
94
|
+
|
|
95
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
96
|
+
:param bitstream: A Bitstream object
|
|
97
|
+
:type bitstream: Bitstream
|
|
98
|
+
|
|
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
|
|
125
|
+
|
|
126
|
+
def bitstream_location(self, bitstream: Bitstream) -> list:
|
|
127
|
+
""""
|
|
128
|
+
Retrieves information about a bitstreams storage locations
|
|
129
|
+
|
|
130
|
+
:param Bitstream bitstream: The bitstream object
|
|
131
|
+
:return: A list of strings containing all the storage locations of this bitstream
|
|
132
|
+
:rtype: list
|
|
133
|
+
|
|
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")
|
|
138
|
+
|
|
139
|
+
storage_locations = []
|
|
140
|
+
|
|
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'
|
|
142
|
+
|
|
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
|
|
152
|
+
|
|
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
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
|
|
50
165
|
"""
|
|
51
166
|
Download a file represented as a Bitstream to a local filename
|
|
52
167
|
|
|
53
168
|
Returns the number of bytes written to the file
|
|
54
169
|
Returns None if the file does not contain the correct number of bytes
|
|
55
170
|
|
|
171
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
56
172
|
:param bitstream: A Bitstream object
|
|
57
173
|
:type bitstream: Bitstream
|
|
58
174
|
|
|
@@ -73,7 +189,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
73
189
|
return self.bitstream_content(bitstream, filename)
|
|
74
190
|
elif request.status_code == requests.codes.ok:
|
|
75
191
|
with open(filename, 'wb') as file:
|
|
76
|
-
for chunk in request.iter_content(chunk_size=
|
|
192
|
+
for chunk in request.iter_content(chunk_size=chunk_size):
|
|
77
193
|
file.write(chunk)
|
|
78
194
|
file.flush()
|
|
79
195
|
if os.path.getsize(filename) == bitstream.length:
|
|
@@ -178,8 +294,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
178
294
|
|
|
179
295
|
logger.debug(xml_request)
|
|
180
296
|
|
|
181
|
-
request = self.session.post(
|
|
182
|
-
|
|
297
|
+
request = self.session.post(
|
|
298
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/exports',
|
|
299
|
+
headers=headers, data=xml_request)
|
|
183
300
|
|
|
184
301
|
if request.status_code == requests.codes.accepted:
|
|
185
302
|
return str(request.content.decode('utf-8'))
|
|
@@ -205,7 +322,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
205
322
|
Initiates export of the entity and downloads the opex package
|
|
206
323
|
Blocks until the package is downloaded
|
|
207
324
|
|
|
208
|
-
By default includes content, metadata with the latest active generations
|
|
325
|
+
By default, includes content, metadata with the latest active generations
|
|
209
326
|
and the parent hierarchy.
|
|
210
327
|
|
|
211
328
|
Arguments are kwargs map
|
|
@@ -233,6 +350,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
233
350
|
IncludedGenerations
|
|
234
351
|
IncludeParentHierarchy
|
|
235
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
|
+
|
|
236
362
|
"""
|
|
237
363
|
status = "ACTIVE"
|
|
238
364
|
pid = self.__export_opex_start__(entity, **kwargs)
|
|
@@ -247,12 +373,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
247
373
|
|
|
248
374
|
def download(self, entity: Entity, filename: str) -> str:
|
|
249
375
|
"""
|
|
250
|
-
|
|
376
|
+
Download the first generation of the access representation of an asset
|
|
251
377
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
378
|
+
:param Entity entity: The entity
|
|
379
|
+
:param str filename: The file the image is written to
|
|
380
|
+
:return: The filename
|
|
381
|
+
:rtype: str
|
|
256
382
|
"""
|
|
257
383
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
258
384
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}'}
|
|
@@ -299,13 +425,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
299
425
|
|
|
300
426
|
def thumbnail(self, entity: Entity, filename: str, size=Thumbnail.LARGE):
|
|
301
427
|
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Returns the filename of the new thumbnail file or None if the entity has no thumbnail
|
|
428
|
+
Get the thumbnail image for an asset or folder
|
|
305
429
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
309
435
|
"""
|
|
310
436
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
311
437
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}', 'size': f'{size.value}'}
|
|
@@ -330,21 +456,22 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
330
456
|
|
|
331
457
|
def delete_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
332
458
|
"""
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
Returns the entity
|
|
459
|
+
Delete identifiers on an Entity object
|
|
336
460
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
"""
|
|
341
467
|
|
|
342
468
|
if (self.major_version < 7) and (self.minor_version < 1):
|
|
343
469
|
raise RuntimeError("delete_identifiers API call is not available when connected to a v6.0 System")
|
|
344
470
|
|
|
345
471
|
headers = {HEADER_TOKEN: self.token}
|
|
346
|
-
request = self.session.get(
|
|
347
|
-
|
|
472
|
+
request = self.session.get(
|
|
473
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
474
|
+
headers=headers)
|
|
348
475
|
if request.status_code == requests.codes.ok:
|
|
349
476
|
xml_response = str(request.content.decode('utf-8'))
|
|
350
477
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
@@ -379,18 +506,66 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
379
506
|
logger.error(request)
|
|
380
507
|
raise RuntimeError(request.status_code, "delete_identifier failed")
|
|
381
508
|
|
|
382
|
-
def
|
|
509
|
+
def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
|
|
383
510
|
"""
|
|
384
|
-
|
|
511
|
+
Get all external identifiers on an entity
|
|
385
512
|
|
|
386
|
-
|
|
513
|
+
Returns the set of external identifiers on the entity
|
|
387
514
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
556
|
+
|
|
557
|
+
def identifiers_for_entity(self, entity: Entity) -> set[Tuple]:
|
|
558
|
+
"""
|
|
559
|
+
Return a set of identifiers which belong to the entity
|
|
560
|
+
|
|
561
|
+
:param Entity entity: The entity
|
|
562
|
+
:return: Set of identifiers as tuples
|
|
563
|
+
:rtype: set(Tuple)
|
|
390
564
|
"""
|
|
391
565
|
headers = {HEADER_TOKEN: self.token}
|
|
392
|
-
request = self.session.get(
|
|
393
|
-
|
|
566
|
+
request = self.session.get(
|
|
567
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
568
|
+
headers=headers)
|
|
394
569
|
if request.status_code == requests.codes.ok:
|
|
395
570
|
xml_response = str(request.content.decode('utf-8'))
|
|
396
571
|
logger.debug(xml_response)
|
|
@@ -415,14 +590,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
415
590
|
logger.error(exception)
|
|
416
591
|
raise exception
|
|
417
592
|
|
|
418
|
-
def identifier(self, identifier_type: str, identifier_value: str) -> set:
|
|
593
|
+
def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
|
|
419
594
|
"""
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
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
|
|
423
596
|
|
|
424
|
-
|
|
425
|
-
|
|
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)
|
|
426
601
|
"""
|
|
427
602
|
headers = {HEADER_TOKEN: self.token}
|
|
428
603
|
payload = {'type': identifier_type, 'value': identifier_value}
|
|
@@ -456,14 +631,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
456
631
|
|
|
457
632
|
def add_identifier(self, entity: Entity, identifier_type: str, identifier_value: str):
|
|
458
633
|
"""
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
Returns the internal identifier DB key
|
|
634
|
+
Add a new external identifier to an Entity object
|
|
462
635
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
"""
|
|
467
642
|
|
|
468
643
|
if self.major_version < 7 and self.minor_version < 1:
|
|
469
644
|
raise RuntimeError("add_identifier API call is not available when connected to a v6.0 System")
|
|
@@ -477,7 +652,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
477
652
|
end_point = f"/{entity.path}/{entity.reference}/identifiers"
|
|
478
653
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
479
654
|
logger.debug(xml_request)
|
|
480
|
-
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
655
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
656
|
+
headers=headers)
|
|
481
657
|
if request.status_code == requests.codes.ok:
|
|
482
658
|
xml_string = str(request.content.decode("utf-8"))
|
|
483
659
|
identifier_response = xml.etree.ElementTree.fromstring(xml_string)
|
|
@@ -495,6 +671,76 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
495
671
|
logger.error(exception)
|
|
496
672
|
raise exception
|
|
497
673
|
|
|
674
|
+
def update_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
675
|
+
"""
|
|
676
|
+
Update external identifiers based on Entity and Type
|
|
677
|
+
|
|
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
|
+
"""
|
|
684
|
+
|
|
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
|
+
|
|
498
744
|
def delete_relationships(self, entity: Entity, relationship_type: str = None):
|
|
499
745
|
"""
|
|
500
746
|
Delete a relationship between two entities by its internal id
|
|
@@ -534,7 +780,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
534
780
|
end_point = f"{entity.path}/{entity.reference}/links/{relationship.api_id}"
|
|
535
781
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers)
|
|
536
782
|
if request.status_code == requests.codes.no_content:
|
|
537
|
-
|
|
783
|
+
return None
|
|
538
784
|
elif request.status_code == requests.codes.unauthorized:
|
|
539
785
|
self.token = self.__token__()
|
|
540
786
|
return self.__delete_relationship(relationship)
|
|
@@ -544,7 +790,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
544
790
|
logger.error(exception)
|
|
545
791
|
raise exception
|
|
546
792
|
|
|
547
|
-
def relationships(self, entity: Entity, page_size: int = 25) -> Generator:
|
|
793
|
+
def relationships(self, entity: Entity, page_size: int = 25) -> Generator[Relationship, None, None]:
|
|
548
794
|
"""
|
|
549
795
|
List the relationship links between entities
|
|
550
796
|
|
|
@@ -553,7 +799,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
553
799
|
:type: page_size: int
|
|
554
800
|
|
|
555
801
|
:param entity: The Source Entity
|
|
556
|
-
:type: entity: Entity
|
|
802
|
+
:type: entity: An Entity type such as Asset, Folder etc
|
|
557
803
|
|
|
558
804
|
:return: Generator
|
|
559
805
|
:rtype: Relationship
|
|
@@ -589,7 +835,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
589
835
|
|
|
590
836
|
if next_page is None:
|
|
591
837
|
params = {'start': '0', 'max': str(maximum)}
|
|
592
|
-
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers,
|
|
838
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers,
|
|
839
|
+
params=params)
|
|
593
840
|
else:
|
|
594
841
|
request = self.session.get(next_page, headers=headers)
|
|
595
842
|
|
|
@@ -620,7 +867,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
620
867
|
|
|
621
868
|
return PagedSet(results, has_more, int(total_hits.text), url)
|
|
622
869
|
elif request.status_code == requests.codes.unauthorized:
|
|
623
|
-
self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
870
|
+
return self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
624
871
|
else:
|
|
625
872
|
exception = HTTPException(entity.reference, request.status_code, request.url, "relationships",
|
|
626
873
|
request.content.decode('utf-8'))
|
|
@@ -631,10 +878,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
631
878
|
"""
|
|
632
879
|
Add a new relationship link between two Assets or Folders
|
|
633
880
|
|
|
634
|
-
:param from_entity: The Source
|
|
881
|
+
:param from_entity: The Source entity to link from
|
|
635
882
|
:type from_entity: Entity
|
|
636
883
|
|
|
637
|
-
:param to_entity: The Target
|
|
884
|
+
:param to_entity: The Target entity
|
|
638
885
|
:type to_entity: Entity
|
|
639
886
|
|
|
640
887
|
:param relationship_type: The Relationship type
|
|
@@ -660,7 +907,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
660
907
|
end_point = f"/{from_entity.path}/{from_entity.reference}/links"
|
|
661
908
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
662
909
|
logger.debug(xml_request)
|
|
663
|
-
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
910
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
911
|
+
headers=headers)
|
|
664
912
|
if request.status_code == requests.codes.ok:
|
|
665
913
|
xml_string = str(request.content.decode("utf-8"))
|
|
666
914
|
logger.debug(xml_string)
|
|
@@ -677,15 +925,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
677
925
|
logger.error(exception)
|
|
678
926
|
raise exception
|
|
679
927
|
|
|
680
|
-
def delete_metadata(self, entity:
|
|
928
|
+
def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
|
|
681
929
|
"""
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
Returns The updated Entity
|
|
930
|
+
Delete an existing descriptive XML document on an entity by its schema
|
|
931
|
+
This call will delete all fragments with the same schema
|
|
685
932
|
|
|
686
|
-
:param entity: The
|
|
687
|
-
:param schema: The schema URI
|
|
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
|
|
688
937
|
"""
|
|
938
|
+
|
|
689
939
|
headers = {HEADER_TOKEN: self.token}
|
|
690
940
|
for url in entity.metadata:
|
|
691
941
|
if schema == entity.metadata[url]:
|
|
@@ -703,15 +953,45 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
703
953
|
|
|
704
954
|
return self.entity(entity.entity_type, entity.reference)
|
|
705
955
|
|
|
706
|
-
|
|
956
|
+
|
|
957
|
+
def add_group_metadata(self, csv_file: str) -> str:
|
|
707
958
|
"""
|
|
708
|
-
|
|
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
|
|
709
963
|
|
|
710
|
-
|
|
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'}
|
|
711
969
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
984
|
+
|
|
985
|
+
|
|
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
|
|
715
995
|
"""
|
|
716
996
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
717
997
|
|
|
@@ -721,11 +1001,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
721
1001
|
for url in entity.metadata:
|
|
722
1002
|
if schema == entity.metadata[url]:
|
|
723
1003
|
mref = url[url.rfind(f"{entity.reference}/metadata/") + len(f"{entity.reference}/metadata/"):]
|
|
724
|
-
xml_object = xml.etree.ElementTree.Element('MetadataContainer',
|
|
725
|
-
{"schemaUri": schema, "xmlns": self.xip_ns})
|
|
726
|
-
xml.etree.ElementTree.SubElement(xml_object, "Ref").text = mref
|
|
727
|
-
xml.etree.ElementTree.SubElement(xml_object, "Entity").text = entity.reference
|
|
728
|
-
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")
|
|
729
1009
|
if isinstance(data, str):
|
|
730
1010
|
ob = xml.etree.ElementTree.fromstring(data)
|
|
731
1011
|
content.append(ob)
|
|
@@ -734,7 +1014,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
734
1014
|
content.append(tree.getroot())
|
|
735
1015
|
else:
|
|
736
1016
|
raise RuntimeError("Unknown data type")
|
|
737
|
-
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")
|
|
738
1018
|
logger.debug(xml_request)
|
|
739
1019
|
request = self.session.put(url, data=xml_request, headers=headers)
|
|
740
1020
|
if request.status_code == requests.codes.ok:
|
|
@@ -749,21 +1029,58 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
749
1029
|
raise exception
|
|
750
1030
|
return self.entity(entity.entity_type, entity.reference)
|
|
751
1031
|
|
|
752
|
-
def
|
|
1032
|
+
def add_metadata_as_fragment(self, entity: EntityT, schema: str, xml_fragment: str) -> EntityT:
|
|
753
1033
|
"""
|
|
754
|
-
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
|
|
755
1036
|
|
|
756
1037
|
Returns The updated Entity
|
|
757
1038
|
|
|
758
|
-
:param
|
|
759
|
-
:param entity: The
|
|
760
|
-
: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
|
|
761
1043
|
"""
|
|
762
1044
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
763
1045
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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")
|
|
767
1084
|
if isinstance(data, str):
|
|
768
1085
|
ob = xml.etree.ElementTree.fromstring(data)
|
|
769
1086
|
content.append(ob)
|
|
@@ -775,7 +1092,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
775
1092
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
776
1093
|
end_point = f"/{entity.path}/{entity.reference}/metadata"
|
|
777
1094
|
logger.debug(xml_request)
|
|
778
|
-
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
1095
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
1096
|
+
headers=headers)
|
|
779
1097
|
if request.status_code == requests.codes.ok:
|
|
780
1098
|
return self.entity(entity_type=entity.entity_type, reference=entity.reference)
|
|
781
1099
|
elif request.status_code == requests.codes.unauthorized:
|
|
@@ -787,14 +1105,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
787
1105
|
logger.error(exception)
|
|
788
1106
|
raise exception
|
|
789
1107
|
|
|
790
|
-
def save(self, entity:
|
|
1108
|
+
def save(self, entity: EntityT) -> EntityT:
|
|
791
1109
|
"""
|
|
792
|
-
|
|
1110
|
+
Updates the title and description of an entity
|
|
1111
|
+
The security tag and parent are not saved via this method call
|
|
793
1112
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
:
|
|
797
|
-
:
|
|
1113
|
+
:param entity: The entity (asset, folder, content_object) to be updated
|
|
1114
|
+
:type entity: Entity
|
|
1115
|
+
:return: The updated entity
|
|
1116
|
+
:rtype: Entity
|
|
798
1117
|
"""
|
|
799
1118
|
|
|
800
1119
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -840,6 +1159,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
840
1159
|
if 'CustomType' in response:
|
|
841
1160
|
content_object.custom_type = response['CustomType']
|
|
842
1161
|
return content_object
|
|
1162
|
+
return None
|
|
843
1163
|
elif request.status_code == requests.codes.unauthorized:
|
|
844
1164
|
self.token = self.__token__()
|
|
845
1165
|
return self.save(entity)
|
|
@@ -859,8 +1179,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
859
1179
|
|
|
860
1180
|
Returns The updated Entity
|
|
861
1181
|
|
|
862
|
-
:param entity:
|
|
863
|
-
: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
|
|
864
1186
|
"""
|
|
865
1187
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
866
1188
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -869,8 +1191,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
869
1191
|
data = dest_folder.reference
|
|
870
1192
|
else:
|
|
871
1193
|
data = "@root@"
|
|
872
|
-
request = self.session.put(
|
|
873
|
-
|
|
1194
|
+
request = self.session.put(
|
|
1195
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/parent-ref',
|
|
1196
|
+
data=data, headers=headers)
|
|
874
1197
|
if request.status_code == requests.codes.accepted:
|
|
875
1198
|
return request.content.decode()
|
|
876
1199
|
elif request.status_code == requests.codes.unauthorized:
|
|
@@ -882,7 +1205,18 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
882
1205
|
logger.error(exception)
|
|
883
1206
|
raise exception
|
|
884
1207
|
|
|
1208
|
+
def get_progress(self, pid: str) -> AsyncProgress:
|
|
1209
|
+
return AsyncProgress[self.get_async_progress(pid)]
|
|
1210
|
+
|
|
885
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
|
+
"""
|
|
886
1220
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
887
1221
|
request = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{pid}", headers=headers)
|
|
888
1222
|
if request.status_code == requests.codes.ok:
|
|
@@ -901,16 +1235,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
901
1235
|
logger.error(exception)
|
|
902
1236
|
raise exception
|
|
903
1237
|
|
|
904
|
-
def move_sync(self, entity:
|
|
1238
|
+
def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
905
1239
|
"""
|
|
906
|
-
Move an
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
Returns The updated Entity.
|
|
910
|
-
Blocks until the move is complete.
|
|
1240
|
+
Move an entity (asset or folder) to a new folder
|
|
1241
|
+
This call blocks until the move is complete
|
|
911
1242
|
|
|
912
|
-
:param entity:
|
|
913
|
-
:param dest_folder: The
|
|
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
|
|
914
1247
|
"""
|
|
915
1248
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
916
1249
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -919,8 +1252,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
919
1252
|
data = dest_folder.reference
|
|
920
1253
|
else:
|
|
921
1254
|
data = "@root@"
|
|
922
|
-
request = self.session.put(
|
|
923
|
-
|
|
1255
|
+
request = self.session.put(
|
|
1256
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/parent-ref',
|
|
1257
|
+
data=data, headers=headers)
|
|
924
1258
|
if request.status_code == requests.codes.accepted:
|
|
925
1259
|
sleep_sec = 1
|
|
926
1260
|
while True:
|
|
@@ -940,15 +1274,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
940
1274
|
logger.error(exception)
|
|
941
1275
|
raise exception
|
|
942
1276
|
|
|
943
|
-
def move(self, entity:
|
|
1277
|
+
def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
944
1278
|
"""
|
|
945
|
-
Move an
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
Returns The updated Entity
|
|
1279
|
+
Move an entity (asset or folder) to a new folder
|
|
1280
|
+
This call is an alias for the move_sync (blocking) method.
|
|
949
1281
|
|
|
950
|
-
:param entity:
|
|
951
|
-
:param dest_folder: The
|
|
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
|
|
952
1286
|
"""
|
|
953
1287
|
return self.move_sync(entity, dest_folder)
|
|
954
1288
|
|
|
@@ -994,26 +1328,28 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
994
1328
|
logger.error(exception)
|
|
995
1329
|
raise exception
|
|
996
1330
|
|
|
997
|
-
def all_metadata(self, entity: Entity) -> Tuple:
|
|
1331
|
+
def all_metadata(self, entity: Entity) -> Generator[Tuple[str, str], None, None]:
|
|
998
1332
|
"""
|
|
999
1333
|
Retrieve all metadata fragments on an entity
|
|
1000
1334
|
|
|
1001
1335
|
Returns XML documents in a tuple
|
|
1002
1336
|
|
|
1003
|
-
:param entity: The entity with the metadata
|
|
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]]
|
|
1004
1340
|
"""
|
|
1005
1341
|
|
|
1006
1342
|
for uri, schema in entity.metadata.items():
|
|
1007
1343
|
yield tuple((str(schema), self.metadata(uri)))
|
|
1008
1344
|
|
|
1009
|
-
def metadata_for_entity(self, entity: Entity, schema: str) -> str:
|
|
1345
|
+
def metadata_for_entity(self, entity: Entity, schema: str) -> Union[str, None]:
|
|
1010
1346
|
"""
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
Returns XML document as a string
|
|
1347
|
+
Fetch the first metadata document which matches the schema URI from an entity
|
|
1014
1348
|
|
|
1015
|
-
:param entity:
|
|
1016
|
-
:param schema:
|
|
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
|
|
1017
1353
|
"""
|
|
1018
1354
|
|
|
1019
1355
|
# if the entity is a lightweight enum version request the full object
|
|
@@ -1025,7 +1361,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1025
1361
|
return self.metadata(uri)
|
|
1026
1362
|
return None
|
|
1027
1363
|
|
|
1028
|
-
def metadata_tag_for_entity(self, entity: Entity, schema: str, tag: str, isXpath: bool = False) -> str:
|
|
1364
|
+
def metadata_tag_for_entity(self, entity: Entity, schema: str, tag: str, isXpath: bool = False) -> Union[str, None]:
|
|
1029
1365
|
"""
|
|
1030
1366
|
Retrieve the first value of the tag from a metadata template given by schema
|
|
1031
1367
|
|
|
@@ -1038,20 +1374,25 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1038
1374
|
"""
|
|
1039
1375
|
|
|
1040
1376
|
xml_doc = self.metadata_for_entity(entity, schema)
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
1383
|
+
return None
|
|
1046
1384
|
|
|
1047
|
-
def security_tag_sync(self, entity:
|
|
1385
|
+
def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
|
|
1048
1386
|
"""
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
Returns the updated entity after the security tag has been updated.
|
|
1387
|
+
Change the security tag of an asset or folder
|
|
1388
|
+
This is a blocking call which returns after all entities have been updated.
|
|
1052
1389
|
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1055
1396
|
"""
|
|
1056
1397
|
self.token = self.__token__()
|
|
1057
1398
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
@@ -1078,12 +1419,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1078
1419
|
|
|
1079
1420
|
def security_tag_async(self, entity: Entity, new_tag: str):
|
|
1080
1421
|
"""
|
|
1081
|
-
|
|
1422
|
+
Change the security tag of an asset or folder
|
|
1423
|
+
This is a non blocking call which returns immediately.
|
|
1082
1424
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
|
1087
1431
|
"""
|
|
1088
1432
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1089
1433
|
end_point = f"/{entity.path}/{entity.reference}/security-descriptor"
|
|
@@ -1102,11 +1446,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1102
1446
|
|
|
1103
1447
|
def metadata(self, uri: str) -> str:
|
|
1104
1448
|
"""
|
|
1105
|
-
|
|
1449
|
+
Fetch the metadata document by its identifier, this is the key from the entity metadata map
|
|
1106
1450
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
:
|
|
1451
|
+
:param str uri: The metadata identifier
|
|
1452
|
+
:return: An XML document as a string
|
|
1453
|
+
:rtype: str
|
|
1110
1454
|
"""
|
|
1111
1455
|
request = self.session.get(uri, headers={HEADER_TOKEN: self.token})
|
|
1112
1456
|
if request.status_code == requests.codes.ok:
|
|
@@ -1124,14 +1468,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1124
1468
|
logger.error(exception)
|
|
1125
1469
|
raise exception
|
|
1126
1470
|
|
|
1127
|
-
def entity(self, entity_type: EntityType, reference: str) ->
|
|
1471
|
+
def entity(self, entity_type: EntityType, reference: str) -> EntityT:
|
|
1128
1472
|
"""
|
|
1129
|
-
|
|
1473
|
+
Returns a generic entity based on its reference identifier
|
|
1130
1474
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
:param
|
|
1134
|
-
:
|
|
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
|
|
1135
1482
|
"""
|
|
1136
1483
|
if entity_type is EntityType.CONTENT_OBJECT:
|
|
1137
1484
|
return self.content_object(reference)
|
|
@@ -1139,6 +1486,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1139
1486
|
return self.folder(reference)
|
|
1140
1487
|
if entity_type is EntityType.ASSET:
|
|
1141
1488
|
return self.asset(reference)
|
|
1489
|
+
return None
|
|
1142
1490
|
|
|
1143
1491
|
def add_physical_asset(self, title: str, description: str, parent: Folder, security_tag: str = "open") -> Asset:
|
|
1144
1492
|
"""
|
|
@@ -1146,10 +1494,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1146
1494
|
|
|
1147
1495
|
Returns Asset
|
|
1148
1496
|
|
|
1149
|
-
:param title: The title of the new Asset
|
|
1150
|
-
:param description: The description of the new Asset
|
|
1151
|
-
:param parent: The parent folder
|
|
1152
|
-
:param security_tag: The security
|
|
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
|
|
1153
1503
|
"""
|
|
1154
1504
|
|
|
1155
1505
|
if (self.major_version < 7) and (self.minor_version < 4):
|
|
@@ -1169,7 +1519,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1169
1519
|
|
|
1170
1520
|
xml_request = xml.etree.ElementTree.tostring(xip_object, encoding='utf-8')
|
|
1171
1521
|
|
|
1172
|
-
request = self.session.post(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}', data=xml_request,
|
|
1522
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}', data=xml_request,
|
|
1523
|
+
headers=headers)
|
|
1173
1524
|
if request.status_code == requests.codes.ok:
|
|
1174
1525
|
xml_string = str(request.content.decode("utf-8"))
|
|
1175
1526
|
entity = self.entity_from_string(xml_string)
|
|
@@ -1185,15 +1536,134 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1185
1536
|
logger.error(exception)
|
|
1186
1537
|
raise exception
|
|
1187
1538
|
|
|
1188
|
-
def
|
|
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')
|
|
1611
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1612
|
+
self.token = self.__token__()
|
|
1613
|
+
return self.merge_folder(folder)
|
|
1614
|
+
else:
|
|
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:
|
|
1189
1627
|
"""
|
|
1190
1628
|
Retrieve an Asset by its reference
|
|
1191
1629
|
|
|
1192
|
-
Returns Asset
|
|
1630
|
+
Returns an XML document of the full Asset
|
|
1193
1631
|
|
|
1194
1632
|
:param reference: The unique identifier of the entity
|
|
1195
1633
|
"""
|
|
1196
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)
|
|
1637
|
+
if request.status_code == requests.codes.ok:
|
|
1638
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
1639
|
+
return xml_response
|
|
1640
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1641
|
+
self.token = self.__token__()
|
|
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
|
|
1647
|
+
else:
|
|
1648
|
+
exception = HTTPException(reference, request.status_code, request.url, "xml_asset",
|
|
1649
|
+
request.content.decode('utf-8'))
|
|
1650
|
+
logger.error(exception)
|
|
1651
|
+
raise exception
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def asset(self, reference: str) -> Asset:
|
|
1655
|
+
|
|
1656
|
+
"""
|
|
1657
|
+
Returns an asset object back by its internal reference identifier
|
|
1658
|
+
|
|
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
|
|
1664
|
+
|
|
1665
|
+
"""
|
|
1666
|
+
headers = {HEADER_TOKEN: self.token}
|
|
1197
1667
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}/{reference}', headers=headers)
|
|
1198
1668
|
if request.status_code == requests.codes.ok:
|
|
1199
1669
|
xml_response = str(request.content.decode('utf-8'))
|
|
@@ -1219,11 +1689,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1219
1689
|
|
|
1220
1690
|
def folder(self, reference: str) -> Folder:
|
|
1221
1691
|
"""
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
Returns Folder
|
|
1692
|
+
Returns a folder object back by its internal reference identifier
|
|
1225
1693
|
|
|
1226
|
-
|
|
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
|
|
1227
1699
|
"""
|
|
1228
1700
|
headers = {HEADER_TOKEN: self.token}
|
|
1229
1701
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{SO_PATH}/{reference}', headers=headers)
|
|
@@ -1251,11 +1723,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1251
1723
|
|
|
1252
1724
|
def content_object(self, reference: str) -> ContentObject:
|
|
1253
1725
|
"""
|
|
1254
|
-
|
|
1726
|
+
Returns a content object back by its internal reference identifier
|
|
1255
1727
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
1259
1733
|
"""
|
|
1260
1734
|
headers = {HEADER_TOKEN: self.token}
|
|
1261
1735
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{reference}', headers=headers)
|
|
@@ -1281,17 +1755,20 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1281
1755
|
logger.error(exception)
|
|
1282
1756
|
raise exception
|
|
1283
1757
|
|
|
1284
|
-
def content_objects(self, representation: Representation) -> list:
|
|
1758
|
+
def content_objects(self, representation: Representation) -> list[ContentObject]:
|
|
1285
1759
|
"""
|
|
1286
|
-
|
|
1760
|
+
Return a list of content objects for a representation
|
|
1761
|
+
|
|
1762
|
+
:param representation: The representation
|
|
1763
|
+
:type representation: Representation
|
|
1764
|
+
:return: List of content objects
|
|
1765
|
+
:rtype: list(ContentObject)
|
|
1287
1766
|
|
|
1288
|
-
:param representation:
|
|
1289
|
-
:returns list[ContentObject]
|
|
1290
1767
|
"""
|
|
1291
1768
|
headers = {HEADER_TOKEN: self.token}
|
|
1292
1769
|
if not isinstance(representation, Representation):
|
|
1293
1770
|
logger.warning("representation is not of type Representation")
|
|
1294
|
-
return
|
|
1771
|
+
return []
|
|
1295
1772
|
request = self.session.get(f'{representation.url}', headers=headers)
|
|
1296
1773
|
if request.status_code == requests.codes.ok:
|
|
1297
1774
|
results = []
|
|
@@ -1315,13 +1792,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1315
1792
|
logger.error(exception)
|
|
1316
1793
|
raise exception
|
|
1317
1794
|
|
|
1318
|
-
def generation(self, url: str) -> Generation:
|
|
1795
|
+
def generation(self, url: str, content_ref: str = None) -> Generation:
|
|
1796
|
+
"""
|
|
1797
|
+
Retrieve a list of generation objects
|
|
1798
|
+
|
|
1799
|
+
:param url:
|
|
1800
|
+
:param content_ref:
|
|
1801
|
+
|
|
1802
|
+
:return: Generation
|
|
1803
|
+
:rtype: Generation
|
|
1319
1804
|
"""
|
|
1320
|
-
Retrieve a list of generation objects
|
|
1321
1805
|
|
|
1322
|
-
:param url:
|
|
1323
|
-
:returns Generation
|
|
1324
|
-
"""
|
|
1325
1806
|
headers = {HEADER_TOKEN: self.token}
|
|
1326
1807
|
request = self.session.get(url, headers=headers)
|
|
1327
1808
|
if request.status_code == requests.codes.ok:
|
|
@@ -1331,14 +1812,53 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1331
1812
|
ge = entity_response.find(f'.//{{{self.xip_ns}}}Generation')
|
|
1332
1813
|
format_group = entity_response.find(f'.//{{{self.xip_ns}}}FormatGroup')
|
|
1333
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
|
+
|
|
1334
1846
|
bitstreams = entity_response.findall(f'./{{{self.entity_ns}}}Bitstreams/{{{self.entity_ns}}}Bitstream')
|
|
1335
1847
|
bitstream_list = []
|
|
1336
1848
|
for bit in bitstreams:
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
|
1342
1862
|
elif request.status_code == requests.codes.unauthorized:
|
|
1343
1863
|
self.token = self.__token__()
|
|
1344
1864
|
return self.generation(url)
|
|
@@ -1415,11 +1935,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1415
1935
|
|
|
1416
1936
|
def bitstream(self, url: str) -> Bitstream:
|
|
1417
1937
|
"""
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
Returns Bitstream
|
|
1938
|
+
Fetch a bitstream object from the server using its URL
|
|
1421
1939
|
|
|
1422
|
-
|
|
1940
|
+
:param url: The URL to the bitstream
|
|
1941
|
+
:type url: str
|
|
1942
|
+
:return: a bitstream object
|
|
1943
|
+
:rtype: Bitstream
|
|
1423
1944
|
"""
|
|
1424
1945
|
headers = {HEADER_TOKEN: self.token}
|
|
1425
1946
|
request = self.session.get(url, headers=headers)
|
|
@@ -1431,12 +1952,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1431
1952
|
filesize = entity_response.find(f'.//{{{self.xip_ns}}}FileSize')
|
|
1432
1953
|
fixity_values = entity_response.findall(f'.//{{{self.xip_ns}}}Fixity')
|
|
1433
1954
|
content = entity_response.find(f'.//{{{self.entity_ns}}}Content')
|
|
1955
|
+
|
|
1956
|
+
index = int(url.rsplit("/", 1)[-1])
|
|
1957
|
+
|
|
1434
1958
|
fixity = {}
|
|
1435
1959
|
for f in fixity_values:
|
|
1436
1960
|
fixity[f[0].text] = f[1].text
|
|
1437
1961
|
bitstream = Bitstream(filename.text if hasattr(filename, 'text') else None,
|
|
1438
1962
|
int(filesize.text) if hasattr(filesize, 'text') else None, fixity,
|
|
1439
1963
|
content.text if hasattr(content, 'text') else None)
|
|
1964
|
+
|
|
1965
|
+
bitstream.bs_index = index
|
|
1440
1966
|
return bitstream
|
|
1441
1967
|
elif request.status_code == requests.codes.unauthorized:
|
|
1442
1968
|
self.token = self.__token__()
|
|
@@ -1450,9 +1976,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1450
1976
|
def replace_generation_sync(self, content_object: ContentObject, file_name, fixity_algorithm=None,
|
|
1451
1977
|
fixity_value=None) -> str:
|
|
1452
1978
|
"""
|
|
1453
|
-
|
|
1979
|
+
Replace the last active generation of a content object with a new digital file.
|
|
1980
|
+
|
|
1981
|
+
Starts the workflow and blocks until the workflow completes.
|
|
1454
1982
|
|
|
1455
|
-
|
|
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
|
|
1456
1989
|
|
|
1457
1990
|
"""
|
|
1458
1991
|
|
|
@@ -1471,7 +2004,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1471
2004
|
"""
|
|
1472
2005
|
Replace the last active generation of a content object with a new digital file.
|
|
1473
2006
|
|
|
1474
|
-
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
|
|
1475
2015
|
|
|
1476
2016
|
"""
|
|
1477
2017
|
if (self.major_version < 7) and (self.minor_version < 2) and (self.patch_version < 1):
|
|
@@ -1485,7 +2025,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1485
2025
|
bitstream = generation.bitstreams.pop()
|
|
1486
2026
|
for algo, value in bitstream.fixity.items():
|
|
1487
2027
|
fixity_algorithm = algo
|
|
1488
|
-
|
|
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)
|
|
1489
2036
|
|
|
1490
2037
|
if fixity_algorithm and fixity_value:
|
|
1491
2038
|
if "MD5" in fixity_algorithm.upper():
|
|
@@ -1516,17 +2063,19 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1516
2063
|
logger.error(exception)
|
|
1517
2064
|
raise exception
|
|
1518
2065
|
|
|
1519
|
-
def generations(self, content_object: ContentObject) -> list:
|
|
2066
|
+
def generations(self, content_object: ContentObject) -> list[Generation]:
|
|
1520
2067
|
"""
|
|
1521
|
-
|
|
2068
|
+
Return a list of Generation objects for a content object
|
|
1522
2069
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
:
|
|
2070
|
+
:param content_object: The content object
|
|
2071
|
+
:type content_object: ContentObject
|
|
2072
|
+
:return: list of generations
|
|
2073
|
+
:rtype: list(Generation)
|
|
1526
2074
|
"""
|
|
1527
2075
|
headers = {HEADER_TOKEN: self.token}
|
|
1528
2076
|
request = self.session.get(
|
|
1529
|
-
f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{content_object.reference}/generations',
|
|
2077
|
+
f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{content_object.reference}/generations',
|
|
2078
|
+
headers=headers)
|
|
1530
2079
|
if request.status_code == requests.codes.ok:
|
|
1531
2080
|
xml_response = str(request.content.decode('utf-8'))
|
|
1532
2081
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
@@ -1534,7 +2083,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1534
2083
|
result = []
|
|
1535
2084
|
for g in generations:
|
|
1536
2085
|
if hasattr(g, 'text'):
|
|
1537
|
-
generation = self.generation(g.text)
|
|
2086
|
+
generation = self.generation(g.text, content_object.reference)
|
|
1538
2087
|
generation.asset = content_object.asset
|
|
1539
2088
|
generation.content_object = content_object
|
|
1540
2089
|
generation.representation_type = content_object.representation_type
|
|
@@ -1549,15 +2098,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1549
2098
|
logger.error(exception)
|
|
1550
2099
|
raise exception
|
|
1551
2100
|
|
|
1552
|
-
def bitstreams_for_asset(self, asset: Asset) -> Iterable:
|
|
2101
|
+
def bitstreams_for_asset(self, asset: Union[Asset, Entity]) -> Iterable[Bitstream]:
|
|
1553
2102
|
"""
|
|
1554
|
-
|
|
1555
|
-
Return all the bitstreams within an asset.
|
|
2103
|
+
Return all the active bitstreams within an asset.
|
|
1556
2104
|
This includes all the representations and content objects
|
|
1557
2105
|
|
|
1558
|
-
|
|
1559
2106
|
:param asset: The asset
|
|
1560
|
-
:return:
|
|
2107
|
+
:return: Iterable
|
|
1561
2108
|
"""
|
|
1562
2109
|
|
|
1563
2110
|
for representation in self.representations(asset):
|
|
@@ -1570,18 +2117,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1570
2117
|
bitstream.generation = generation
|
|
1571
2118
|
yield bitstream
|
|
1572
2119
|
|
|
1573
|
-
def representations(self, asset: Asset) -> set:
|
|
2120
|
+
def representations(self, asset: Asset) -> set[Representation]:
|
|
1574
2121
|
"""
|
|
1575
|
-
|
|
2122
|
+
Return a set of representations for the asset
|
|
2123
|
+
|
|
2124
|
+
Representations are used to define how the information object are composed in terms of technology and structure.
|
|
1576
2125
|
|
|
1577
|
-
:param asset:
|
|
1578
|
-
:
|
|
2126
|
+
:param asset: The asset containing the required representations
|
|
2127
|
+
:type asset: Asset
|
|
2128
|
+
:return: Set of Representation objects
|
|
2129
|
+
:rtype: set(Representation)
|
|
1579
2130
|
"""
|
|
1580
2131
|
headers = {HEADER_TOKEN: self.token}
|
|
1581
2132
|
if not isinstance(asset, Asset):
|
|
1582
|
-
return
|
|
1583
|
-
request = self.session.get(
|
|
1584
|
-
|
|
2133
|
+
return set()
|
|
2134
|
+
request = self.session.get(
|
|
2135
|
+
f'{self.protocol}://{self.server}/api/entity/{asset.path}/{asset.reference}/representations',
|
|
2136
|
+
headers=headers)
|
|
1585
2137
|
if request.status_code == requests.codes.ok:
|
|
1586
2138
|
xml_response = str(request.content.decode('utf-8'))
|
|
1587
2139
|
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
@@ -1602,11 +2154,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1602
2154
|
|
|
1603
2155
|
def remove_thumbnail(self, entity: Entity):
|
|
1604
2156
|
"""
|
|
1605
|
-
|
|
1606
|
-
|
|
2157
|
+
Remove the thumbnail for the entity to the uploaded image
|
|
1607
2158
|
|
|
1608
|
-
|
|
1609
|
-
|
|
2159
|
+
:param entity: The entity with the thumbnail
|
|
2160
|
+
:type entity: Entity
|
|
2161
|
+
"""
|
|
1610
2162
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1611
2163
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1612
2164
|
|
|
@@ -1615,8 +2167,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1615
2167
|
|
|
1616
2168
|
headers = {HEADER_TOKEN: self.token}
|
|
1617
2169
|
|
|
1618
|
-
request = self.session.delete(
|
|
1619
|
-
|
|
2170
|
+
request = self.session.delete(
|
|
2171
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/preview',
|
|
2172
|
+
headers=headers)
|
|
1620
2173
|
if request.status_code == requests.codes.no_content:
|
|
1621
2174
|
return str(request.content.decode('utf-8'))
|
|
1622
2175
|
elif request.status_code == requests.codes.unauthorized:
|
|
@@ -1628,25 +2181,66 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1628
2181
|
logger.error(exception)
|
|
1629
2182
|
raise exception
|
|
1630
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
|
|
2221
|
+
|
|
1631
2222
|
def add_thumbnail(self, entity: Entity, image_file: str):
|
|
1632
2223
|
"""
|
|
1633
|
-
|
|
2224
|
+
Set the thumbnail for the entity to the uploaded image
|
|
1634
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
|
+
"""
|
|
1635
2231
|
|
|
1636
|
-
:param entity: The Entity
|
|
1637
|
-
:param image_file: Path to image file
|
|
1638
|
-
"""
|
|
1639
2232
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1640
2233
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1641
2234
|
|
|
1642
2235
|
if isinstance(entity, ContentObject):
|
|
1643
2236
|
raise RuntimeError("Thumbnails cannot be added to Content Objects")
|
|
1644
2237
|
|
|
1645
|
-
headers = {HEADER_TOKEN: self.token
|
|
2238
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
1646
2239
|
|
|
1647
2240
|
with open(image_file, 'rb') as fd:
|
|
1648
|
-
request = self.session.put(
|
|
1649
|
-
|
|
2241
|
+
request = self.session.put(
|
|
2242
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/preview',
|
|
2243
|
+
data=fd, headers=headers)
|
|
1650
2244
|
|
|
1651
2245
|
if request.status_code == requests.codes.no_content:
|
|
1652
2246
|
return str(request.content.decode('utf-8'))
|
|
@@ -1670,11 +2264,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1670
2264
|
headers = {HEADER_TOKEN: self.token}
|
|
1671
2265
|
params = {'start': str(0), 'max': str(maximum)}
|
|
1672
2266
|
|
|
1673
|
-
request = self.session.get(
|
|
1674
|
-
|
|
2267
|
+
request = self.session.get(
|
|
2268
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/event-actions',
|
|
2269
|
+
params=params, headers=headers)
|
|
1675
2270
|
|
|
1676
2271
|
if request.status_code == requests.codes.ok:
|
|
1677
|
-
|
|
2272
|
+
return None
|
|
1678
2273
|
elif request.status_code == requests.codes.unauthorized:
|
|
1679
2274
|
self.token = self.__token__()
|
|
1680
2275
|
return self._event_actions(entity, maximum=maximum)
|
|
@@ -1684,20 +2279,34 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1684
2279
|
logger.error(exception)
|
|
1685
2280
|
raise exception
|
|
1686
2281
|
|
|
1687
|
-
def all_descendants(self, folder: Folder = None) -> Generator:
|
|
2282
|
+
def all_descendants(self, folder: Union[Folder, Entity] = None) -> Generator[Entity, None, None]:
|
|
1688
2283
|
"""
|
|
1689
|
-
|
|
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.
|
|
1690
2287
|
|
|
1691
|
-
|
|
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)
|
|
1692
2291
|
|
|
1693
|
-
:param folder: The folder to find children of
|
|
1694
2292
|
"""
|
|
1695
2293
|
for entity in self.descendants(folder=folder):
|
|
1696
2294
|
yield entity
|
|
1697
2295
|
if entity.entity_type == EntityType.FOLDER:
|
|
1698
2296
|
yield from self.all_descendants(folder=entity)
|
|
1699
2297
|
|
|
1700
|
-
def descendants(self, folder: Union[str, Folder] = None) -> Generator:
|
|
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)
|
|
2307
|
+
|
|
2308
|
+
"""
|
|
2309
|
+
|
|
1701
2310
|
maximum = 100
|
|
1702
2311
|
paged_set = self.children(folder, maximum=maximum, next_page=None)
|
|
1703
2312
|
for entity in paged_set.results:
|
|
@@ -1707,10 +2316,30 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1707
2316
|
for entity in paged_set.results:
|
|
1708
2317
|
yield entity
|
|
1709
2318
|
|
|
2319
|
+
|
|
2320
|
+
|
|
1710
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
|
+
|
|
1711
2336
|
headers = {HEADER_TOKEN: self.token}
|
|
1712
2337
|
data = {'start': str(0), 'max': str(maximum)}
|
|
1713
|
-
|
|
2338
|
+
|
|
2339
|
+
if isinstance(folder, Folder):
|
|
2340
|
+
folder_reference = folder.reference
|
|
2341
|
+
else:
|
|
2342
|
+
folder_reference = folder
|
|
1714
2343
|
if next_page is None:
|
|
1715
2344
|
if folder_reference is None:
|
|
1716
2345
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/root/children', params=data,
|
|
@@ -1750,12 +2379,22 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1750
2379
|
self.token = self.__token__()
|
|
1751
2380
|
return self.children(folder_reference, maximum=maximum, next_page=next_page)
|
|
1752
2381
|
else:
|
|
1753
|
-
exception = HTTPException(
|
|
2382
|
+
exception = HTTPException(folder_reference, request.status_code, request.url,
|
|
1754
2383
|
"children", request.content.decode('utf-8'))
|
|
1755
2384
|
logger.error(exception)
|
|
1756
2385
|
raise exception
|
|
1757
2386
|
|
|
1758
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
|
+
|
|
1759
2398
|
self.token = self.__token__()
|
|
1760
2399
|
previous = datetime.utcnow() - timedelta(days=previous_days)
|
|
1761
2400
|
from_date = previous.replace(tzinfo=timezone.utc).isoformat()
|
|
@@ -1771,6 +2410,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1771
2410
|
yield entity
|
|
1772
2411
|
|
|
1773
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
|
+
"""
|
|
1774
2421
|
self.token = self.__token__()
|
|
1775
2422
|
paged_set = self._all_events_page()
|
|
1776
2423
|
for entity in paged_set.results:
|
|
@@ -1796,9 +2443,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1796
2443
|
actions = entity_response.findall(f'.//{{{self.xip_ns}}}EventAction')
|
|
1797
2444
|
result_list = []
|
|
1798
2445
|
for action in actions:
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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)
|
|
1802
2455
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
1803
2456
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
1804
2457
|
has_more = True
|
|
@@ -1808,8 +2461,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1808
2461
|
else:
|
|
1809
2462
|
url = next_url.text
|
|
1810
2463
|
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2464
|
+
return None
|
|
2465
|
+
|
|
2466
|
+
|
|
1811
2467
|
|
|
1812
|
-
|
|
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
|
+
"""
|
|
1813
2475
|
self.token = self.__token__()
|
|
1814
2476
|
paged_set = self._entity_from_event_page(event_id, 25, None)
|
|
1815
2477
|
for entity in paged_set.results:
|
|
@@ -1832,9 +2494,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1832
2494
|
params["from"] = kwargs.get("from_date")
|
|
1833
2495
|
if "to_date" in kwargs:
|
|
1834
2496
|
params["to"] = kwargs.get("to_date")
|
|
2497
|
+
if "username" in kwargs:
|
|
2498
|
+
params["username"] = kwargs.get("username")
|
|
1835
2499
|
|
|
1836
2500
|
if next_page is None:
|
|
1837
|
-
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
|
|
2501
|
+
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
|
|
2502
|
+
headers=headers)
|
|
1838
2503
|
else:
|
|
1839
2504
|
request = self.session.get(next_page, headers=headers)
|
|
1840
2505
|
|
|
@@ -1961,6 +2626,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1961
2626
|
raise exception
|
|
1962
2627
|
|
|
1963
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
|
+
"""
|
|
1964
2639
|
self.token = self.__token__()
|
|
1965
2640
|
paged_set = self._entity_events_page(entity)
|
|
1966
2641
|
for entity in paged_set.results:
|
|
@@ -1971,6 +2646,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1971
2646
|
yield entity
|
|
1972
2647
|
|
|
1973
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
|
+
"""
|
|
2659
|
+
|
|
1974
2660
|
self.token = self.__token__()
|
|
1975
2661
|
maximum = 25
|
|
1976
2662
|
paged_set = self._updated_entities_page(previous_days=previous_days, maximum=maximum, next_page=None)
|
|
@@ -2028,34 +2714,37 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2028
2714
|
logger.error(exception)
|
|
2029
2715
|
raise exception
|
|
2030
2716
|
|
|
2031
|
-
def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str):
|
|
2717
|
+
def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2032
2718
|
"""
|
|
2033
|
-
|
|
2719
|
+
Initiate and approve the deletion of an asset.
|
|
2034
2720
|
|
|
2035
|
-
:param asset:
|
|
2036
|
-
:param operator_comment:
|
|
2037
|
-
:param supervisor_comment:
|
|
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
|
|
2038
2726
|
"""
|
|
2039
2727
|
if isinstance(asset, Asset):
|
|
2040
|
-
return self._delete_entity(asset, operator_comment, supervisor_comment)
|
|
2728
|
+
return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
|
|
2041
2729
|
else:
|
|
2042
2730
|
raise RuntimeError("delete_asset only deletes assets")
|
|
2043
2731
|
|
|
2044
|
-
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"):
|
|
2045
2733
|
"""
|
|
2046
|
-
|
|
2047
|
-
|
|
2734
|
+
Initiate and approve the deletion of a folder.
|
|
2048
2735
|
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
|
2052
2741
|
"""
|
|
2053
2742
|
if isinstance(folder, Folder):
|
|
2054
|
-
return self._delete_entity(folder, operator_comment, supervisor_comment)
|
|
2743
|
+
return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
|
|
2055
2744
|
else:
|
|
2056
2745
|
raise RuntimeError("delete_folder only deletes folders")
|
|
2057
2746
|
|
|
2058
|
-
def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str):
|
|
2747
|
+
def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2059
2748
|
"""
|
|
2060
2749
|
Delete an asset from the repository
|
|
2061
2750
|
|
|
@@ -2066,7 +2755,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2066
2755
|
|
|
2067
2756
|
# check manager password is available:
|
|
2068
2757
|
config = configparser.ConfigParser()
|
|
2069
|
-
config.read(
|
|
2758
|
+
config.read(credentials_path, encoding='utf-8')
|
|
2070
2759
|
try:
|
|
2071
2760
|
manager_username = config['credentials']['manager.username']
|
|
2072
2761
|
manager_password = config['credentials']['manager.password']
|
|
@@ -2095,6 +2784,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2095
2784
|
entity_response = xml.etree.ElementTree.fromstring(req.content.decode("utf-8"))
|
|
2096
2785
|
status = entity_response.find(".//{http://status.preservica.com}Status")
|
|
2097
2786
|
if hasattr(status, 'text'):
|
|
2787
|
+
if status.text == "COMPLETED":
|
|
2788
|
+
return entity.reference
|
|
2098
2789
|
if status.text == "PENDING":
|
|
2099
2790
|
headers = {HEADER_TOKEN: self.manager_token(manager_username, manager_password),
|
|
2100
2791
|
'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -2105,18 +2796,20 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2105
2796
|
xml.etree.ElementTree.SubElement(approval_el, "Comment").text = supervisor_comment
|
|
2106
2797
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
2107
2798
|
logger.debug(xml_request)
|
|
2108
|
-
approve = self.session.put(
|
|
2109
|
-
|
|
2799
|
+
approve = self.session.put(
|
|
2800
|
+
f"{self.protocol}://{self.server}/api/entity/actions/deletions/{progress}",
|
|
2801
|
+
data=xml_request, headers=headers)
|
|
2110
2802
|
if approve.status_code == requests.codes.accepted:
|
|
2111
2803
|
return entity.reference
|
|
2112
2804
|
else:
|
|
2113
2805
|
logger.error(approve.content.decode('utf-8'))
|
|
2114
2806
|
raise RuntimeError(approve.status_code, "delete_asset failed during approval")
|
|
2115
2807
|
sleep(2.0)
|
|
2116
|
-
req = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{progress}",
|
|
2808
|
+
req = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{progress}",
|
|
2809
|
+
headers=headers)
|
|
2117
2810
|
elif request.status_code == requests.codes.unauthorized:
|
|
2118
2811
|
self.token = self.__token__()
|
|
2119
|
-
return self._delete_entity(entity, operator_comment, supervisor_comment)
|
|
2812
|
+
return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
|
|
2120
2813
|
if request.status_code == requests.codes.unprocessable:
|
|
2121
2814
|
logger.error(request.content.decode('utf-8'))
|
|
2122
2815
|
raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
|
|
@@ -2129,3 +2822,4 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2129
2822
|
"_delete_entity", request.content.decode('utf-8'))
|
|
2130
2823
|
logger.error(exception)
|
|
2131
2824
|
raise exception
|
|
2825
|
+
|