pyPreservica 2.7.2__py3-none-any.whl → 3.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyPreservica/__init__.py +18 -6
- pyPreservica/adminAPI.py +29 -22
- pyPreservica/authorityAPI.py +6 -7
- pyPreservica/common.py +116 -19
- pyPreservica/contentAPI.py +179 -8
- pyPreservica/entityAPI.py +730 -214
- pyPreservica/mdformsAPI.py +501 -29
- pyPreservica/monitorAPI.py +2 -2
- pyPreservica/parAPI.py +1 -37
- pyPreservica/retentionAPI.py +58 -26
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +298 -480
- pyPreservica/webHooksAPI.py +42 -1
- pyPreservica/workflowAPI.py +17 -13
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/METADATA +20 -9
- pypreservica-3.3.4.dist-info/RECORD +20 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/WHEEL +1 -1
- pyPreservica/vocabularyAPI.py +0 -141
- pyPreservica-2.7.2.dist-info/RECORD +0 -20
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/top_level.txt +0 -0
pyPreservica/entityAPI.py
CHANGED
|
@@ -8,14 +8,15 @@ author: James Carr
|
|
|
8
8
|
licence: Apache License 2.0
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import os.path
|
|
13
13
|
import uuid
|
|
14
14
|
import xml.etree.ElementTree
|
|
15
15
|
from datetime import datetime, timedelta, timezone
|
|
16
16
|
from io import BytesIO
|
|
17
17
|
from time import sleep
|
|
18
|
-
from typing import Any, Generator, Tuple, Iterable, Union
|
|
18
|
+
from typing import Any, Generator, Tuple, Iterable, Union, Callable
|
|
19
|
+
|
|
19
20
|
|
|
20
21
|
from pyPreservica.common import *
|
|
21
22
|
|
|
@@ -34,8 +35,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
37
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
37
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
38
|
-
|
|
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
|
+
|
|
39
44
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
40
45
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
41
46
|
|
|
@@ -54,11 +59,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
54
59
|
|
|
55
60
|
def bitstream_chunks(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Generator:
|
|
56
61
|
"""
|
|
57
|
-
Generator function to return bitstream chunks
|
|
62
|
+
Generator function to return bitstream chunks, allows the clients to
|
|
63
|
+
process chunks as they are downloaded.
|
|
58
64
|
|
|
59
|
-
:param
|
|
60
|
-
:
|
|
61
|
-
:
|
|
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
|
|
62
71
|
"""
|
|
63
72
|
if not isinstance(bitstream, Bitstream):
|
|
64
73
|
logger.error("bitstream_content argument is not a Bitstream object")
|
|
@@ -66,40 +75,40 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
66
75
|
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
67
76
|
if request.status_code == requests.codes.unauthorized:
|
|
68
77
|
self.token = self.__token__()
|
|
69
|
-
|
|
78
|
+
yield from self.bitstream_chunks(bitstream)
|
|
70
79
|
elif request.status_code == requests.codes.ok:
|
|
71
80
|
for chunk in request.iter_content(chunk_size=chunk_size):
|
|
72
81
|
yield chunk
|
|
73
82
|
else:
|
|
74
|
-
exception = HTTPException(bitstream.filename, request.status_code, request.url, "
|
|
83
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_chunks",
|
|
75
84
|
request.content.decode('utf-8'))
|
|
76
85
|
logger.error(exception)
|
|
77
86
|
raise exception
|
|
78
87
|
|
|
79
88
|
def bitstream_bytes(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Union[BytesIO, None]:
|
|
80
89
|
"""
|
|
81
|
-
|
|
90
|
+
Download a file represented as a Bitstream to a byteIO array
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
Returns the byteIO
|
|
93
|
+
Returns None if the file does not contain the correct number of bytes (default 2k)
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
96
|
+
:param bitstream: A Bitstream object
|
|
97
|
+
:type bitstream: Bitstream
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
:return: The file in bytes
|
|
100
|
+
:rtype: byteIO
|
|
92
101
|
"""
|
|
93
102
|
if not isinstance(bitstream, Bitstream):
|
|
94
103
|
logger.error("bitstream_content argument is not a Bitstream object")
|
|
95
104
|
raise RuntimeError("bitstream_bytes argument is not a Bitstream object")
|
|
96
|
-
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as
|
|
97
|
-
if
|
|
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:
|
|
98
107
|
self.token = self.__token__()
|
|
99
108
|
return self.bitstream_bytes(bitstream)
|
|
100
|
-
elif
|
|
109
|
+
elif response.status_code == requests.codes.ok:
|
|
101
110
|
file_bytes = BytesIO()
|
|
102
|
-
for chunk in
|
|
111
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
103
112
|
file_bytes.write(chunk)
|
|
104
113
|
file_bytes.seek(0)
|
|
105
114
|
if file_bytes.getbuffer().nbytes == bitstream.length:
|
|
@@ -109,11 +118,49 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
109
118
|
logger.error("Downloaded file size did not match the Preservica held value")
|
|
110
119
|
return None
|
|
111
120
|
else:
|
|
112
|
-
exception = HTTPException(bitstream.filename,
|
|
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",
|
|
113
158
|
request.content.decode('utf-8'))
|
|
114
159
|
logger.error(exception)
|
|
115
160
|
raise exception
|
|
116
161
|
|
|
162
|
+
|
|
163
|
+
|
|
117
164
|
def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
|
|
118
165
|
"""
|
|
119
166
|
Download a file represented as a Bitstream to a local filename
|
|
@@ -303,6 +350,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
303
350
|
IncludedGenerations
|
|
304
351
|
IncludeParentHierarchy
|
|
305
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
|
+
|
|
306
362
|
"""
|
|
307
363
|
status = "ACTIVE"
|
|
308
364
|
pid = self.__export_opex_start__(entity, **kwargs)
|
|
@@ -317,12 +373,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
317
373
|
|
|
318
374
|
def download(self, entity: Entity, filename: str) -> str:
|
|
319
375
|
"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
Returns the filename of the new file
|
|
376
|
+
Download the first generation of the access representation of an asset
|
|
323
377
|
|
|
324
|
-
|
|
325
|
-
|
|
378
|
+
:param Entity entity: The entity
|
|
379
|
+
:param str filename: The file the image is written to
|
|
380
|
+
:return: The filename
|
|
381
|
+
:rtype: str
|
|
326
382
|
"""
|
|
327
383
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
328
384
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}'}
|
|
@@ -369,13 +425,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
369
425
|
|
|
370
426
|
def thumbnail(self, entity: Entity, filename: str, size=Thumbnail.LARGE):
|
|
371
427
|
"""
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
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
|
|
375
429
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
379
435
|
"""
|
|
380
436
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
381
437
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}', 'size': f'{size.value}'}
|
|
@@ -400,14 +456,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
400
456
|
|
|
401
457
|
def delete_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
402
458
|
"""
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
Returns the entity
|
|
459
|
+
Delete identifiers on an Entity object
|
|
406
460
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
"""
|
|
411
467
|
|
|
412
468
|
if (self.major_version < 7) and (self.minor_version < 1):
|
|
413
469
|
raise RuntimeError("delete_identifiers API call is not available when connected to a v6.0 System")
|
|
@@ -450,14 +506,61 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
450
506
|
logger.error(request)
|
|
451
507
|
raise RuntimeError(request.status_code, "delete_identifier failed")
|
|
452
508
|
|
|
453
|
-
def
|
|
509
|
+
def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
|
|
454
510
|
"""
|
|
455
|
-
|
|
511
|
+
Get all external identifiers on an entity
|
|
456
512
|
|
|
457
|
-
|
|
513
|
+
Returns the set of external identifiers on the entity
|
|
458
514
|
|
|
459
|
-
|
|
460
|
-
|
|
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)
|
|
461
564
|
"""
|
|
462
565
|
headers = {HEADER_TOKEN: self.token}
|
|
463
566
|
request = self.session.get(
|
|
@@ -487,14 +590,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
487
590
|
logger.error(exception)
|
|
488
591
|
raise exception
|
|
489
592
|
|
|
490
|
-
def identifier(self, identifier_type: str, identifier_value: str) -> set[
|
|
593
|
+
def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
|
|
491
594
|
"""
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
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
|
|
495
596
|
|
|
496
|
-
|
|
497
|
-
|
|
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)
|
|
498
601
|
"""
|
|
499
602
|
headers = {HEADER_TOKEN: self.token}
|
|
500
603
|
payload = {'type': identifier_type, 'value': identifier_value}
|
|
@@ -528,14 +631,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
528
631
|
|
|
529
632
|
def add_identifier(self, entity: Entity, identifier_type: str, identifier_value: str):
|
|
530
633
|
"""
|
|
531
|
-
|
|
634
|
+
Add a new external identifier to an Entity object
|
|
532
635
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
"""
|
|
539
642
|
|
|
540
643
|
if self.major_version < 7 and self.minor_version < 1:
|
|
541
644
|
raise RuntimeError("add_identifier API call is not available when connected to a v6.0 System")
|
|
@@ -568,6 +671,76 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
568
671
|
logger.error(exception)
|
|
569
672
|
raise exception
|
|
570
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
|
+
|
|
571
744
|
def delete_relationships(self, entity: Entity, relationship_type: str = None):
|
|
572
745
|
"""
|
|
573
746
|
Delete a relationship between two entities by its internal id
|
|
@@ -607,7 +780,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
607
780
|
end_point = f"{entity.path}/{entity.reference}/links/{relationship.api_id}"
|
|
608
781
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers)
|
|
609
782
|
if request.status_code == requests.codes.no_content:
|
|
610
|
-
|
|
783
|
+
return None
|
|
611
784
|
elif request.status_code == requests.codes.unauthorized:
|
|
612
785
|
self.token = self.__token__()
|
|
613
786
|
return self.__delete_relationship(relationship)
|
|
@@ -626,7 +799,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
626
799
|
:type: page_size: int
|
|
627
800
|
|
|
628
801
|
:param entity: The Source Entity
|
|
629
|
-
:type: entity: Entity
|
|
802
|
+
:type: entity: An Entity type such as Asset, Folder etc
|
|
630
803
|
|
|
631
804
|
:return: Generator
|
|
632
805
|
:rtype: Relationship
|
|
@@ -694,7 +867,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
694
867
|
|
|
695
868
|
return PagedSet(results, has_more, int(total_hits.text), url)
|
|
696
869
|
elif request.status_code == requests.codes.unauthorized:
|
|
697
|
-
self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
870
|
+
return self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
698
871
|
else:
|
|
699
872
|
exception = HTTPException(entity.reference, request.status_code, request.url, "relationships",
|
|
700
873
|
request.content.decode('utf-8'))
|
|
@@ -705,10 +878,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
705
878
|
"""
|
|
706
879
|
Add a new relationship link between two Assets or Folders
|
|
707
880
|
|
|
708
|
-
:param from_entity: The Source
|
|
881
|
+
:param from_entity: The Source entity to link from
|
|
709
882
|
:type from_entity: Entity
|
|
710
883
|
|
|
711
|
-
:param to_entity: The Target
|
|
884
|
+
:param to_entity: The Target entity
|
|
712
885
|
:type to_entity: Entity
|
|
713
886
|
|
|
714
887
|
:param relationship_type: The Relationship type
|
|
@@ -752,15 +925,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
752
925
|
logger.error(exception)
|
|
753
926
|
raise exception
|
|
754
927
|
|
|
755
|
-
def delete_metadata(self, entity:
|
|
928
|
+
def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
|
|
756
929
|
"""
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
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
|
|
760
932
|
|
|
761
|
-
:param entity: The
|
|
762
|
-
: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
|
|
763
937
|
"""
|
|
938
|
+
|
|
764
939
|
headers = {HEADER_TOKEN: self.token}
|
|
765
940
|
for url in entity.metadata:
|
|
766
941
|
if schema == entity.metadata[url]:
|
|
@@ -778,15 +953,45 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
778
953
|
|
|
779
954
|
return self.entity(entity.entity_type, entity.reference)
|
|
780
955
|
|
|
781
|
-
|
|
956
|
+
|
|
957
|
+
def add_group_metadata(self, csv_file: str) -> str:
|
|
782
958
|
"""
|
|
783
|
-
|
|
959
|
+
Perform bulk additions of metadata with a CSV file.
|
|
960
|
+
This is designed for metadata which populates a New Gen Metadata Group
|
|
961
|
+
Returns The process ID which will track the updates
|
|
962
|
+
Requires DataManagement permission
|
|
963
|
+
|
|
964
|
+
:param str csv_file: The path of the CSV metadata file
|
|
965
|
+
:return: The process ID
|
|
966
|
+
:rtype: str
|
|
967
|
+
"""
|
|
968
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/csv;charset=UTF-8'}
|
|
969
|
+
|
|
970
|
+
url = f'{self.protocol}://{self.server}/api/entity/actions/metadata-csv-edits'
|
|
971
|
+
|
|
972
|
+
with open(csv_file, 'rb') as fd:
|
|
973
|
+
with self.session.post(url, headers=headers, data=fd) as request:
|
|
974
|
+
if request.status_code == requests.codes.unauthorized:
|
|
975
|
+
self.token = self.__token__()
|
|
976
|
+
return self.add_group_metadata(csv_file)
|
|
977
|
+
elif request.status_code == requests.codes.accepted:
|
|
978
|
+
return str(request.content.decode('utf-8'))
|
|
979
|
+
else:
|
|
980
|
+
exception = HTTPException(None, request.status_code, request.url, "add_group_metadata",
|
|
981
|
+
request.content.decode('utf-8'))
|
|
982
|
+
logger.error(exception)
|
|
983
|
+
raise exception
|
|
784
984
|
|
|
785
|
-
Returns The updated Entity
|
|
786
985
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
|
790
995
|
"""
|
|
791
996
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
792
997
|
|
|
@@ -809,7 +1014,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
809
1014
|
content.append(tree.getroot())
|
|
810
1015
|
else:
|
|
811
1016
|
raise RuntimeError("Unknown data type")
|
|
812
|
-
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")
|
|
813
1018
|
logger.debug(xml_request)
|
|
814
1019
|
request = self.session.put(url, data=xml_request, headers=headers)
|
|
815
1020
|
if request.status_code == requests.codes.ok:
|
|
@@ -824,15 +1029,51 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
824
1029
|
raise exception
|
|
825
1030
|
return self.entity(entity.entity_type, entity.reference)
|
|
826
1031
|
|
|
827
|
-
def
|
|
1032
|
+
def add_metadata_as_fragment(self, entity: EntityT, schema: str, xml_fragment: str) -> EntityT:
|
|
828
1033
|
"""
|
|
829
|
-
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
|
|
830
1036
|
|
|
831
1037
|
Returns The updated Entity
|
|
832
1038
|
|
|
833
|
-
:param
|
|
834
|
-
:param entity: The
|
|
835
|
-
: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
|
|
1043
|
+
"""
|
|
1044
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
1045
|
+
|
|
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
|
|
836
1077
|
"""
|
|
837
1078
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
838
1079
|
|
|
@@ -864,13 +1105,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
864
1105
|
logger.error(exception)
|
|
865
1106
|
raise exception
|
|
866
1107
|
|
|
867
|
-
def save(self, entity:
|
|
1108
|
+
def save(self, entity: EntityT) -> EntityT:
|
|
868
1109
|
"""
|
|
869
|
-
|
|
1110
|
+
Updates the title and description of an entity
|
|
1111
|
+
The security tag and parent are not saved via this method call
|
|
870
1112
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
:
|
|
1113
|
+
:param entity: The entity (asset, folder, content_object) to be updated
|
|
1114
|
+
:type entity: Entity
|
|
1115
|
+
:return: The updated entity
|
|
1116
|
+
:rtype: Entity
|
|
874
1117
|
"""
|
|
875
1118
|
|
|
876
1119
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -916,6 +1159,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
916
1159
|
if 'CustomType' in response:
|
|
917
1160
|
content_object.custom_type = response['CustomType']
|
|
918
1161
|
return content_object
|
|
1162
|
+
return None
|
|
919
1163
|
elif request.status_code == requests.codes.unauthorized:
|
|
920
1164
|
self.token = self.__token__()
|
|
921
1165
|
return self.save(entity)
|
|
@@ -935,8 +1179,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
935
1179
|
|
|
936
1180
|
Returns The updated Entity
|
|
937
1181
|
|
|
938
|
-
:param entity:
|
|
939
|
-
: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
|
|
940
1186
|
"""
|
|
941
1187
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
942
1188
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -959,7 +1205,18 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
959
1205
|
logger.error(exception)
|
|
960
1206
|
raise exception
|
|
961
1207
|
|
|
1208
|
+
def get_progress(self, pid: str) -> AsyncProgress:
|
|
1209
|
+
return AsyncProgress[self.get_async_progress(pid)]
|
|
1210
|
+
|
|
962
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
|
+
"""
|
|
963
1220
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
964
1221
|
request = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{pid}", headers=headers)
|
|
965
1222
|
if request.status_code == requests.codes.ok:
|
|
@@ -978,16 +1235,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
978
1235
|
logger.error(exception)
|
|
979
1236
|
raise exception
|
|
980
1237
|
|
|
981
|
-
def move_sync(self, entity:
|
|
1238
|
+
def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
982
1239
|
"""
|
|
983
|
-
Move an
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
Returns The updated Entity.
|
|
987
|
-
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
|
|
988
1242
|
|
|
989
|
-
:param entity:
|
|
990
|
-
: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
|
|
991
1247
|
"""
|
|
992
1248
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
993
1249
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -1018,15 +1274,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1018
1274
|
logger.error(exception)
|
|
1019
1275
|
raise exception
|
|
1020
1276
|
|
|
1021
|
-
def move(self, entity:
|
|
1277
|
+
def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
1022
1278
|
"""
|
|
1023
|
-
Move an
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
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.
|
|
1027
1281
|
|
|
1028
|
-
:param entity:
|
|
1029
|
-
: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
|
|
1030
1286
|
"""
|
|
1031
1287
|
return self.move_sync(entity, dest_folder)
|
|
1032
1288
|
|
|
@@ -1072,13 +1328,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1072
1328
|
logger.error(exception)
|
|
1073
1329
|
raise exception
|
|
1074
1330
|
|
|
1075
|
-
def all_metadata(self, entity: Entity) -> Tuple:
|
|
1331
|
+
def all_metadata(self, entity: Entity) -> Generator[Tuple[str, str], None, None]:
|
|
1076
1332
|
"""
|
|
1077
1333
|
Retrieve all metadata fragments on an entity
|
|
1078
1334
|
|
|
1079
1335
|
Returns XML documents in a tuple
|
|
1080
1336
|
|
|
1081
|
-
: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]]
|
|
1082
1340
|
"""
|
|
1083
1341
|
|
|
1084
1342
|
for uri, schema in entity.metadata.items():
|
|
@@ -1086,12 +1344,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1086
1344
|
|
|
1087
1345
|
def metadata_for_entity(self, entity: Entity, schema: str) -> Union[str, None]:
|
|
1088
1346
|
"""
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
Returns XML document as a string
|
|
1347
|
+
Fetch the first metadata document which matches the schema URI from an entity
|
|
1092
1348
|
|
|
1093
|
-
:param entity:
|
|
1094
|
-
: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
|
|
1095
1353
|
"""
|
|
1096
1354
|
|
|
1097
1355
|
# if the entity is a lightweight enum version request the full object
|
|
@@ -1101,9 +1359,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1101
1359
|
for uri, schema_name in entity.metadata.items():
|
|
1102
1360
|
if schema == schema_name:
|
|
1103
1361
|
return self.metadata(uri)
|
|
1104
|
-
return
|
|
1362
|
+
return None
|
|
1105
1363
|
|
|
1106
|
-
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]:
|
|
1107
1365
|
"""
|
|
1108
1366
|
Retrieve the first value of the tag from a metadata template given by schema
|
|
1109
1367
|
|
|
@@ -1118,19 +1376,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1118
1376
|
xml_doc = self.metadata_for_entity(entity, schema)
|
|
1119
1377
|
if xml_doc:
|
|
1120
1378
|
xml_object = xml.etree.ElementTree.fromstring(xml_doc)
|
|
1121
|
-
if isXpath
|
|
1379
|
+
if not isXpath:
|
|
1122
1380
|
return xml_object.find(f'.//{{*}}{tag}').text
|
|
1123
1381
|
else:
|
|
1124
1382
|
return xml_object.find(tag).text
|
|
1383
|
+
return None
|
|
1125
1384
|
|
|
1126
|
-
def security_tag_sync(self, entity:
|
|
1385
|
+
def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
|
|
1127
1386
|
"""
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
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.
|
|
1131
1389
|
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
1134
1396
|
"""
|
|
1135
1397
|
self.token = self.__token__()
|
|
1136
1398
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
@@ -1157,12 +1419,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1157
1419
|
|
|
1158
1420
|
def security_tag_async(self, entity: Entity, new_tag: str):
|
|
1159
1421
|
"""
|
|
1160
|
-
|
|
1422
|
+
Change the security tag of an asset or folder
|
|
1423
|
+
This is a non blocking call which returns immediately.
|
|
1161
1424
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
|
1166
1431
|
"""
|
|
1167
1432
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1168
1433
|
end_point = f"/{entity.path}/{entity.reference}/security-descriptor"
|
|
@@ -1181,11 +1446,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1181
1446
|
|
|
1182
1447
|
def metadata(self, uri: str) -> str:
|
|
1183
1448
|
"""
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
Returns XML document as a string
|
|
1449
|
+
Fetch the metadata document by its identifier, this is the key from the entity metadata map
|
|
1187
1450
|
|
|
1188
|
-
:param uri:
|
|
1451
|
+
:param str uri: The metadata identifier
|
|
1452
|
+
:return: An XML document as a string
|
|
1453
|
+
:rtype: str
|
|
1189
1454
|
"""
|
|
1190
1455
|
request = self.session.get(uri, headers={HEADER_TOKEN: self.token})
|
|
1191
1456
|
if request.status_code == requests.codes.ok:
|
|
@@ -1203,14 +1468,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1203
1468
|
logger.error(exception)
|
|
1204
1469
|
raise exception
|
|
1205
1470
|
|
|
1206
|
-
def entity(self, entity_type: EntityType, reference: str) ->
|
|
1471
|
+
def entity(self, entity_type: EntityType, reference: str) -> EntityT:
|
|
1207
1472
|
"""
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
Returns Entity (Asset, Folder, ContentObject)
|
|
1473
|
+
Returns a generic entity based on its reference identifier
|
|
1211
1474
|
|
|
1212
|
-
:param
|
|
1213
|
-
:
|
|
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
|
|
1214
1482
|
"""
|
|
1215
1483
|
if entity_type is EntityType.CONTENT_OBJECT:
|
|
1216
1484
|
return self.content_object(reference)
|
|
@@ -1218,6 +1486,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1218
1486
|
return self.folder(reference)
|
|
1219
1487
|
if entity_type is EntityType.ASSET:
|
|
1220
1488
|
return self.asset(reference)
|
|
1489
|
+
return None
|
|
1221
1490
|
|
|
1222
1491
|
def add_physical_asset(self, title: str, description: str, parent: Folder, security_tag: str = "open") -> Asset:
|
|
1223
1492
|
"""
|
|
@@ -1225,10 +1494,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1225
1494
|
|
|
1226
1495
|
Returns Asset
|
|
1227
1496
|
|
|
1228
|
-
:param title: The title of the new Asset
|
|
1229
|
-
:param description: The description of the new Asset
|
|
1230
|
-
:param parent: The parent folder
|
|
1231
|
-
: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
|
|
1232
1503
|
"""
|
|
1233
1504
|
|
|
1234
1505
|
if (self.major_version < 7) and (self.minor_version < 4):
|
|
@@ -1265,15 +1536,134 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1265
1536
|
logger.error(exception)
|
|
1266
1537
|
raise exception
|
|
1267
1538
|
|
|
1268
|
-
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:
|
|
1269
1627
|
"""
|
|
1270
1628
|
Retrieve an Asset by its reference
|
|
1271
1629
|
|
|
1272
|
-
Returns Asset
|
|
1630
|
+
Returns an XML document of the full Asset
|
|
1273
1631
|
|
|
1274
1632
|
:param reference: The unique identifier of the entity
|
|
1275
1633
|
"""
|
|
1276
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}
|
|
1277
1667
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}/{reference}', headers=headers)
|
|
1278
1668
|
if request.status_code == requests.codes.ok:
|
|
1279
1669
|
xml_response = str(request.content.decode('utf-8'))
|
|
@@ -1299,11 +1689,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1299
1689
|
|
|
1300
1690
|
def folder(self, reference: str) -> Folder:
|
|
1301
1691
|
"""
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
Returns Folder
|
|
1692
|
+
Returns a folder object back by its internal reference identifier
|
|
1305
1693
|
|
|
1306
|
-
|
|
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
|
|
1307
1699
|
"""
|
|
1308
1700
|
headers = {HEADER_TOKEN: self.token}
|
|
1309
1701
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{SO_PATH}/{reference}', headers=headers)
|
|
@@ -1331,11 +1723,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1331
1723
|
|
|
1332
1724
|
def content_object(self, reference: str) -> ContentObject:
|
|
1333
1725
|
"""
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
Returns ContentObject
|
|
1726
|
+
Returns a content object back by its internal reference identifier
|
|
1337
1727
|
|
|
1338
|
-
|
|
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
|
|
1339
1733
|
"""
|
|
1340
1734
|
headers = {HEADER_TOKEN: self.token}
|
|
1341
1735
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{reference}', headers=headers)
|
|
@@ -1363,11 +1757,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1363
1757
|
|
|
1364
1758
|
def content_objects(self, representation: Representation) -> list[ContentObject]:
|
|
1365
1759
|
"""
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
:param representation:
|
|
1760
|
+
Return a list of content objects for a representation
|
|
1369
1761
|
|
|
1370
|
-
|
|
1762
|
+
:param representation: The representation
|
|
1763
|
+
:type representation: Representation
|
|
1764
|
+
:return: List of content objects
|
|
1765
|
+
:rtype: list(ContentObject)
|
|
1371
1766
|
|
|
1372
1767
|
"""
|
|
1373
1768
|
headers = {HEADER_TOKEN: self.token}
|
|
@@ -1397,13 +1792,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1397
1792
|
logger.error(exception)
|
|
1398
1793
|
raise exception
|
|
1399
1794
|
|
|
1400
|
-
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
|
|
1401
1804
|
"""
|
|
1402
|
-
Retrieve a list of generation objects
|
|
1403
1805
|
|
|
1404
|
-
:param url:
|
|
1405
|
-
:returns Generation
|
|
1406
|
-
"""
|
|
1407
1806
|
headers = {HEADER_TOKEN: self.token}
|
|
1408
1807
|
request = self.session.get(url, headers=headers)
|
|
1409
1808
|
if request.status_code == requests.codes.ok:
|
|
@@ -1447,7 +1846,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1447
1846
|
bitstreams = entity_response.findall(f'./{{{self.entity_ns}}}Bitstreams/{{{self.entity_ns}}}Bitstream')
|
|
1448
1847
|
bitstream_list = []
|
|
1449
1848
|
for bit in bitstreams:
|
|
1450
|
-
|
|
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)
|
|
1451
1854
|
generation = Generation(strtobool(ge.attrib['original']), strtobool(ge.attrib['active']),
|
|
1452
1855
|
format_group.text if hasattr(format_group, 'text') else None,
|
|
1453
1856
|
effective_date.text if hasattr(effective_date, 'text') else None,
|
|
@@ -1532,11 +1935,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1532
1935
|
|
|
1533
1936
|
def bitstream(self, url: str) -> Bitstream:
|
|
1534
1937
|
"""
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
Returns Bitstream
|
|
1938
|
+
Fetch a bitstream object from the server using its URL
|
|
1538
1939
|
|
|
1539
|
-
|
|
1940
|
+
:param url: The URL to the bitstream
|
|
1941
|
+
:type url: str
|
|
1942
|
+
:return: a bitstream object
|
|
1943
|
+
:rtype: Bitstream
|
|
1540
1944
|
"""
|
|
1541
1945
|
headers = {HEADER_TOKEN: self.token}
|
|
1542
1946
|
request = self.session.get(url, headers=headers)
|
|
@@ -1572,9 +1976,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1572
1976
|
def replace_generation_sync(self, content_object: ContentObject, file_name, fixity_algorithm=None,
|
|
1573
1977
|
fixity_value=None) -> str:
|
|
1574
1978
|
"""
|
|
1575
|
-
|
|
1979
|
+
Replace the last active generation of a content object with a new digital file.
|
|
1576
1980
|
|
|
1577
|
-
|
|
1981
|
+
Starts the workflow and blocks until the workflow completes.
|
|
1982
|
+
|
|
1983
|
+
:param ContentObject content_object: The content object to replace
|
|
1984
|
+
:param str file_name: The path to the new content object
|
|
1985
|
+
:param str fixity_algorithm: Optional fixity algorithm
|
|
1986
|
+
:param str fixity_value: Optional fixity value
|
|
1987
|
+
:return: Completed workflow status
|
|
1988
|
+
:rtype: str
|
|
1578
1989
|
|
|
1579
1990
|
"""
|
|
1580
1991
|
|
|
@@ -1593,7 +2004,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1593
2004
|
"""
|
|
1594
2005
|
Replace the last active generation of a content object with a new digital file.
|
|
1595
2006
|
|
|
1596
|
-
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
|
|
1597
2015
|
|
|
1598
2016
|
"""
|
|
1599
2017
|
if (self.major_version < 7) and (self.minor_version < 2) and (self.patch_version < 1):
|
|
@@ -1647,11 +2065,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1647
2065
|
|
|
1648
2066
|
def generations(self, content_object: ContentObject) -> list[Generation]:
|
|
1649
2067
|
"""
|
|
1650
|
-
|
|
2068
|
+
Return a list of Generation objects for a content object
|
|
1651
2069
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
:
|
|
2070
|
+
:param content_object: The content object
|
|
2071
|
+
:type content_object: ContentObject
|
|
2072
|
+
:return: list of generations
|
|
2073
|
+
:rtype: list(Generation)
|
|
1655
2074
|
"""
|
|
1656
2075
|
headers = {HEADER_TOKEN: self.token}
|
|
1657
2076
|
request = self.session.get(
|
|
@@ -1664,7 +2083,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1664
2083
|
result = []
|
|
1665
2084
|
for g in generations:
|
|
1666
2085
|
if hasattr(g, 'text'):
|
|
1667
|
-
generation = self.generation(g.text)
|
|
2086
|
+
generation = self.generation(g.text, content_object.reference)
|
|
1668
2087
|
generation.asset = content_object.asset
|
|
1669
2088
|
generation.content_object = content_object
|
|
1670
2089
|
generation.representation_type = content_object.representation_type
|
|
@@ -1681,13 +2100,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1681
2100
|
|
|
1682
2101
|
def bitstreams_for_asset(self, asset: Union[Asset, Entity]) -> Iterable[Bitstream]:
|
|
1683
2102
|
"""
|
|
1684
|
-
|
|
1685
|
-
Return all the bitstreams within an asset.
|
|
2103
|
+
Return all the active bitstreams within an asset.
|
|
1686
2104
|
This includes all the representations and content objects
|
|
1687
2105
|
|
|
1688
|
-
|
|
1689
2106
|
:param asset: The asset
|
|
1690
|
-
:return:
|
|
2107
|
+
:return: Iterable
|
|
1691
2108
|
"""
|
|
1692
2109
|
|
|
1693
2110
|
for representation in self.representations(asset):
|
|
@@ -1702,10 +2119,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1702
2119
|
|
|
1703
2120
|
def representations(self, asset: Asset) -> set[Representation]:
|
|
1704
2121
|
"""
|
|
1705
|
-
|
|
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.
|
|
1706
2125
|
|
|
1707
|
-
:param asset:
|
|
1708
|
-
:
|
|
2126
|
+
:param asset: The asset containing the required representations
|
|
2127
|
+
:type asset: Asset
|
|
2128
|
+
:return: Set of Representation objects
|
|
2129
|
+
:rtype: set(Representation)
|
|
1709
2130
|
"""
|
|
1710
2131
|
headers = {HEADER_TOKEN: self.token}
|
|
1711
2132
|
if not isinstance(asset, Asset):
|
|
@@ -1733,11 +2154,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1733
2154
|
|
|
1734
2155
|
def remove_thumbnail(self, entity: Entity):
|
|
1735
2156
|
"""
|
|
1736
|
-
|
|
2157
|
+
Remove the thumbnail for the entity to the uploaded image
|
|
1737
2158
|
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2159
|
+
:param entity: The entity with the thumbnail
|
|
2160
|
+
:type entity: Entity
|
|
2161
|
+
"""
|
|
1741
2162
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1742
2163
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1743
2164
|
|
|
@@ -1760,13 +2181,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1760
2181
|
logger.error(exception)
|
|
1761
2182
|
raise exception
|
|
1762
2183
|
|
|
2184
|
+
|
|
1763
2185
|
def add_access_representation(self, entity: Entity, access_file: str, name: str = "Access"):
|
|
1764
2186
|
"""
|
|
1765
|
-
Add a new representation to an existing asset.
|
|
2187
|
+
Add a new Access representation to an existing asset.
|
|
1766
2188
|
|
|
1767
|
-
:param entity:
|
|
1768
|
-
:param access_file: The new digital file
|
|
1769
|
-
:param name: The name of the new access representation defaults to "Access"
|
|
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"
|
|
1770
2192
|
:return:
|
|
1771
2193
|
"""
|
|
1772
2194
|
|
|
@@ -1799,12 +2221,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1799
2221
|
|
|
1800
2222
|
def add_thumbnail(self, entity: Entity, image_file: str):
|
|
1801
2223
|
"""
|
|
1802
|
-
|
|
2224
|
+
Set the thumbnail for the entity to the uploaded image
|
|
1803
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
|
+
"""
|
|
1804
2231
|
|
|
1805
|
-
:param entity: The Entity
|
|
1806
|
-
:param image_file: Path to image file
|
|
1807
|
-
"""
|
|
1808
2232
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1809
2233
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1810
2234
|
|
|
@@ -1845,7 +2269,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1845
2269
|
params=params, headers=headers)
|
|
1846
2270
|
|
|
1847
2271
|
if request.status_code == requests.codes.ok:
|
|
1848
|
-
|
|
2272
|
+
return None
|
|
1849
2273
|
elif request.status_code == requests.codes.unauthorized:
|
|
1850
2274
|
self.token = self.__token__()
|
|
1851
2275
|
return self._event_actions(entity, maximum=maximum)
|
|
@@ -1857,11 +2281,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1857
2281
|
|
|
1858
2282
|
def all_descendants(self, folder: Union[Folder, Entity] = None) -> Generator[Entity, None, None]:
|
|
1859
2283
|
"""
|
|
1860
|
-
|
|
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.
|
|
1861
2287
|
|
|
1862
|
-
|
|
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)
|
|
1863
2291
|
|
|
1864
|
-
:param folder: The folder to find children of
|
|
1865
2292
|
"""
|
|
1866
2293
|
for entity in self.descendants(folder=folder):
|
|
1867
2294
|
yield entity
|
|
@@ -1869,6 +2296,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1869
2296
|
yield from self.all_descendants(folder=entity)
|
|
1870
2297
|
|
|
1871
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
|
+
|
|
1872
2310
|
maximum = 100
|
|
1873
2311
|
paged_set = self.children(folder, maximum=maximum, next_page=None)
|
|
1874
2312
|
for entity in paged_set.results:
|
|
@@ -1878,7 +2316,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1878
2316
|
for entity in paged_set.results:
|
|
1879
2317
|
yield entity
|
|
1880
2318
|
|
|
2319
|
+
|
|
2320
|
+
|
|
1881
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
|
+
|
|
1882
2336
|
headers = {HEADER_TOKEN: self.token}
|
|
1883
2337
|
data = {'start': str(0), 'max': str(maximum)}
|
|
1884
2338
|
|
|
@@ -1925,12 +2379,22 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1925
2379
|
self.token = self.__token__()
|
|
1926
2380
|
return self.children(folder_reference, maximum=maximum, next_page=next_page)
|
|
1927
2381
|
else:
|
|
1928
|
-
exception = HTTPException(
|
|
2382
|
+
exception = HTTPException(folder_reference, request.status_code, request.url,
|
|
1929
2383
|
"children", request.content.decode('utf-8'))
|
|
1930
2384
|
logger.error(exception)
|
|
1931
2385
|
raise exception
|
|
1932
2386
|
|
|
1933
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
|
+
|
|
1934
2398
|
self.token = self.__token__()
|
|
1935
2399
|
previous = datetime.utcnow() - timedelta(days=previous_days)
|
|
1936
2400
|
from_date = previous.replace(tzinfo=timezone.utc).isoformat()
|
|
@@ -1946,6 +2410,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1946
2410
|
yield entity
|
|
1947
2411
|
|
|
1948
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
|
+
"""
|
|
1949
2421
|
self.token = self.__token__()
|
|
1950
2422
|
paged_set = self._all_events_page()
|
|
1951
2423
|
for entity in paged_set.results:
|
|
@@ -1971,9 +2443,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1971
2443
|
actions = entity_response.findall(f'.//{{{self.xip_ns}}}EventAction')
|
|
1972
2444
|
result_list = []
|
|
1973
2445
|
for action in actions:
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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)
|
|
1977
2455
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
1978
2456
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
1979
2457
|
has_more = True
|
|
@@ -1983,8 +2461,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1983
2461
|
else:
|
|
1984
2462
|
url = next_url.text
|
|
1985
2463
|
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2464
|
+
return None
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
|
|
1986
2468
|
|
|
1987
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
|
+
"""
|
|
1988
2475
|
self.token = self.__token__()
|
|
1989
2476
|
paged_set = self._entity_from_event_page(event_id, 25, None)
|
|
1990
2477
|
for entity in paged_set.results:
|
|
@@ -2007,6 +2494,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2007
2494
|
params["from"] = kwargs.get("from_date")
|
|
2008
2495
|
if "to_date" in kwargs:
|
|
2009
2496
|
params["to"] = kwargs.get("to_date")
|
|
2497
|
+
if "username" in kwargs:
|
|
2498
|
+
params["username"] = kwargs.get("username")
|
|
2010
2499
|
|
|
2011
2500
|
if next_page is None:
|
|
2012
2501
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
|
|
@@ -2137,6 +2626,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2137
2626
|
raise exception
|
|
2138
2627
|
|
|
2139
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
|
+
"""
|
|
2140
2639
|
self.token = self.__token__()
|
|
2141
2640
|
paged_set = self._entity_events_page(entity)
|
|
2142
2641
|
for entity in paged_set.results:
|
|
@@ -2147,6 +2646,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2147
2646
|
yield entity
|
|
2148
2647
|
|
|
2149
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
|
+
|
|
2150
2660
|
self.token = self.__token__()
|
|
2151
2661
|
maximum = 25
|
|
2152
2662
|
paged_set = self._updated_entities_page(previous_days=previous_days, maximum=maximum, next_page=None)
|
|
@@ -2204,34 +2714,37 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2204
2714
|
logger.error(exception)
|
|
2205
2715
|
raise exception
|
|
2206
2716
|
|
|
2207
|
-
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"):
|
|
2208
2718
|
"""
|
|
2209
|
-
|
|
2719
|
+
Initiate and approve the deletion of an asset.
|
|
2210
2720
|
|
|
2211
|
-
:param asset:
|
|
2212
|
-
:param operator_comment:
|
|
2213
|
-
: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
|
|
2214
2726
|
"""
|
|
2215
2727
|
if isinstance(asset, Asset):
|
|
2216
|
-
return self._delete_entity(asset, operator_comment, supervisor_comment)
|
|
2728
|
+
return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
|
|
2217
2729
|
else:
|
|
2218
2730
|
raise RuntimeError("delete_asset only deletes assets")
|
|
2219
2731
|
|
|
2220
|
-
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"):
|
|
2221
2733
|
"""
|
|
2222
|
-
|
|
2223
|
-
|
|
2734
|
+
Initiate and approve the deletion of a folder.
|
|
2224
2735
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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
|
|
2228
2741
|
"""
|
|
2229
2742
|
if isinstance(folder, Folder):
|
|
2230
|
-
return self._delete_entity(folder, operator_comment, supervisor_comment)
|
|
2743
|
+
return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
|
|
2231
2744
|
else:
|
|
2232
2745
|
raise RuntimeError("delete_folder only deletes folders")
|
|
2233
2746
|
|
|
2234
|
-
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"):
|
|
2235
2748
|
"""
|
|
2236
2749
|
Delete an asset from the repository
|
|
2237
2750
|
|
|
@@ -2242,7 +2755,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2242
2755
|
|
|
2243
2756
|
# check manager password is available:
|
|
2244
2757
|
config = configparser.ConfigParser()
|
|
2245
|
-
config.read(
|
|
2758
|
+
config.read(credentials_path, encoding='utf-8')
|
|
2246
2759
|
try:
|
|
2247
2760
|
manager_username = config['credentials']['manager.username']
|
|
2248
2761
|
manager_password = config['credentials']['manager.password']
|
|
@@ -2271,6 +2784,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2271
2784
|
entity_response = xml.etree.ElementTree.fromstring(req.content.decode("utf-8"))
|
|
2272
2785
|
status = entity_response.find(".//{http://status.preservica.com}Status")
|
|
2273
2786
|
if hasattr(status, 'text'):
|
|
2787
|
+
if status.text == "COMPLETED":
|
|
2788
|
+
return entity.reference
|
|
2274
2789
|
if status.text == "PENDING":
|
|
2275
2790
|
headers = {HEADER_TOKEN: self.manager_token(manager_username, manager_password),
|
|
2276
2791
|
'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -2294,7 +2809,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2294
2809
|
headers=headers)
|
|
2295
2810
|
elif request.status_code == requests.codes.unauthorized:
|
|
2296
2811
|
self.token = self.__token__()
|
|
2297
|
-
return self._delete_entity(entity, operator_comment, supervisor_comment)
|
|
2812
|
+
return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
|
|
2298
2813
|
if request.status_code == requests.codes.unprocessable:
|
|
2299
2814
|
logger.error(request.content.decode('utf-8'))
|
|
2300
2815
|
raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
|
|
@@ -2307,3 +2822,4 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2307
2822
|
"_delete_entity", request.content.decode('utf-8'))
|
|
2308
2823
|
logger.error(exception)
|
|
2309
2824
|
raise exception
|
|
2825
|
+
|