pyPreservica 2.9.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 +15 -3
- pyPreservica/adminAPI.py +29 -22
- pyPreservica/authorityAPI.py +6 -7
- pyPreservica/common.py +85 -14
- pyPreservica/contentAPI.py +56 -5
- pyPreservica/entityAPI.py +652 -215
- pyPreservica/mdformsAPI.py +87 -6
- pyPreservica/monitorAPI.py +2 -2
- pyPreservica/parAPI.py +1 -37
- pyPreservica/retentionAPI.py +5 -4
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +163 -398
- pyPreservica/webHooksAPI.py +1 -1
- pyPreservica/workflowAPI.py +8 -8
- {pyPreservica-2.9.3.dist-info → pypreservica-3.3.3.dist-info}/METADATA +18 -5
- pypreservica-3.3.3.dist-info/RECORD +20 -0
- {pyPreservica-2.9.3.dist-info → pypreservica-3.3.3.dist-info}/WHEEL +1 -1
- pyPreservica-2.9.3.dist-info/RECORD +0 -19
- {pyPreservica-2.9.3.dist-info → pypreservica-3.3.3.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-2.9.3.dist-info → pypreservica-3.3.3.dist-info}/top_level.txt +0 -0
pyPreservica/entityAPI.py
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
|
@@ -17,6 +17,7 @@ from io import BytesIO
|
|
|
17
17
|
from time import sleep
|
|
18
18
|
from typing import Any, Generator, Tuple, Iterable, Union, Callable
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
from pyPreservica.common import *
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
@@ -35,10 +36,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
35
36
|
|
|
36
37
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
37
38
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
38
|
-
protocol: str = "https", request_hook: Callable = None):
|
|
39
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
39
40
|
|
|
40
41
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
41
|
-
protocol, request_hook)
|
|
42
|
+
protocol, request_hook, credentials_path)
|
|
42
43
|
|
|
43
44
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
44
45
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
@@ -58,11 +59,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
58
59
|
|
|
59
60
|
def bitstream_chunks(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Generator:
|
|
60
61
|
"""
|
|
61
|
-
Generator function to return bitstream chunks
|
|
62
|
+
Generator function to return bitstream chunks, allows the clients to
|
|
63
|
+
process chunks as they are downloaded.
|
|
62
64
|
|
|
63
|
-
:param
|
|
64
|
-
:
|
|
65
|
-
:
|
|
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
|
|
66
71
|
"""
|
|
67
72
|
if not isinstance(bitstream, Bitstream):
|
|
68
73
|
logger.error("bitstream_content argument is not a Bitstream object")
|
|
@@ -70,40 +75,40 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
70
75
|
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
71
76
|
if request.status_code == requests.codes.unauthorized:
|
|
72
77
|
self.token = self.__token__()
|
|
73
|
-
|
|
78
|
+
yield from self.bitstream_chunks(bitstream)
|
|
74
79
|
elif request.status_code == requests.codes.ok:
|
|
75
80
|
for chunk in request.iter_content(chunk_size=chunk_size):
|
|
76
81
|
yield chunk
|
|
77
82
|
else:
|
|
78
|
-
exception = HTTPException(bitstream.filename, request.status_code, request.url, "
|
|
83
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_chunks",
|
|
79
84
|
request.content.decode('utf-8'))
|
|
80
85
|
logger.error(exception)
|
|
81
86
|
raise exception
|
|
82
87
|
|
|
83
88
|
def bitstream_bytes(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Union[BytesIO, None]:
|
|
84
89
|
"""
|
|
85
|
-
|
|
90
|
+
Download a file represented as a Bitstream to a byteIO array
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
Returns the byteIO
|
|
93
|
+
Returns None if the file does not contain the correct number of bytes (default 2k)
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
:param chunk_size: The buffer copy chunk size in bytes default
|
|
96
|
+
:param bitstream: A Bitstream object
|
|
97
|
+
:type bitstream: Bitstream
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
:return: The file in bytes
|
|
100
|
+
:rtype: byteIO
|
|
96
101
|
"""
|
|
97
102
|
if not isinstance(bitstream, Bitstream):
|
|
98
103
|
logger.error("bitstream_content argument is not a Bitstream object")
|
|
99
104
|
raise RuntimeError("bitstream_bytes argument is not a Bitstream object")
|
|
100
|
-
with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as
|
|
101
|
-
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:
|
|
102
107
|
self.token = self.__token__()
|
|
103
108
|
return self.bitstream_bytes(bitstream)
|
|
104
|
-
elif
|
|
109
|
+
elif response.status_code == requests.codes.ok:
|
|
105
110
|
file_bytes = BytesIO()
|
|
106
|
-
for chunk in
|
|
111
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
107
112
|
file_bytes.write(chunk)
|
|
108
113
|
file_bytes.seek(0)
|
|
109
114
|
if file_bytes.getbuffer().nbytes == bitstream.length:
|
|
@@ -113,11 +118,49 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
113
118
|
logger.error("Downloaded file size did not match the Preservica held value")
|
|
114
119
|
return None
|
|
115
120
|
else:
|
|
116
|
-
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",
|
|
117
158
|
request.content.decode('utf-8'))
|
|
118
159
|
logger.error(exception)
|
|
119
160
|
raise exception
|
|
120
161
|
|
|
162
|
+
|
|
163
|
+
|
|
121
164
|
def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
|
|
122
165
|
"""
|
|
123
166
|
Download a file represented as a Bitstream to a local filename
|
|
@@ -307,6 +350,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
307
350
|
IncludedGenerations
|
|
308
351
|
IncludeParentHierarchy
|
|
309
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
|
+
|
|
310
362
|
"""
|
|
311
363
|
status = "ACTIVE"
|
|
312
364
|
pid = self.__export_opex_start__(entity, **kwargs)
|
|
@@ -321,12 +373,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
321
373
|
|
|
322
374
|
def download(self, entity: Entity, filename: str) -> str:
|
|
323
375
|
"""
|
|
324
|
-
|
|
376
|
+
Download the first generation of the access representation of an asset
|
|
325
377
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
378
|
+
:param Entity entity: The entity
|
|
379
|
+
:param str filename: The file the image is written to
|
|
380
|
+
:return: The filename
|
|
381
|
+
:rtype: str
|
|
330
382
|
"""
|
|
331
383
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
332
384
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}'}
|
|
@@ -373,13 +425,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
373
425
|
|
|
374
426
|
def thumbnail(self, entity: Entity, filename: str, size=Thumbnail.LARGE):
|
|
375
427
|
"""
|
|
376
|
-
|
|
428
|
+
Get the thumbnail image for an asset or folder
|
|
377
429
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
383
435
|
"""
|
|
384
436
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
385
437
|
params = {'id': f'sdb:{entity.entity_type.value}|{entity.reference}', 'size': f'{size.value}'}
|
|
@@ -404,14 +456,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
404
456
|
|
|
405
457
|
def delete_identifiers(self, entity: Entity, identifier_type: str = None, identifier_value: str = None):
|
|
406
458
|
"""
|
|
407
|
-
|
|
459
|
+
Delete identifiers on an Entity object
|
|
408
460
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
"""
|
|
415
467
|
|
|
416
468
|
if (self.major_version < 7) and (self.minor_version < 1):
|
|
417
469
|
raise RuntimeError("delete_identifiers API call is not available when connected to a v6.0 System")
|
|
@@ -454,15 +506,61 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
454
506
|
logger.error(request)
|
|
455
507
|
raise RuntimeError(request.status_code, "delete_identifier failed")
|
|
456
508
|
|
|
509
|
+
def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
|
|
510
|
+
"""
|
|
511
|
+
Get all external identifiers on an entity
|
|
457
512
|
|
|
458
|
-
|
|
513
|
+
Returns the set of external identifiers on the entity
|
|
514
|
+
|
|
515
|
+
:param entity: The Entity (Asset or Folder)
|
|
516
|
+
:param external_identifier_type: Optional identifier type to filter the results
|
|
517
|
+
:type entity: Entity
|
|
459
518
|
"""
|
|
460
|
-
|
|
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
|
|
461
556
|
|
|
462
|
-
|
|
557
|
+
def identifiers_for_entity(self, entity: Entity) -> set[Tuple]:
|
|
558
|
+
"""
|
|
559
|
+
Return a set of identifiers which belong to the entity
|
|
463
560
|
|
|
464
|
-
|
|
465
|
-
|
|
561
|
+
:param Entity entity: The entity
|
|
562
|
+
:return: Set of identifiers as tuples
|
|
563
|
+
:rtype: set(Tuple)
|
|
466
564
|
"""
|
|
467
565
|
headers = {HEADER_TOKEN: self.token}
|
|
468
566
|
request = self.session.get(
|
|
@@ -492,16 +590,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
492
590
|
logger.error(exception)
|
|
493
591
|
raise exception
|
|
494
592
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def identifier(self, identifier_type: str, identifier_value: str) -> set[Entity]:
|
|
593
|
+
def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
|
|
498
594
|
"""
|
|
499
|
-
|
|
595
|
+
Return a set of entities with external identifiers which match the type and value
|
|
500
596
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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)
|
|
505
601
|
"""
|
|
506
602
|
headers = {HEADER_TOKEN: self.token}
|
|
507
603
|
payload = {'type': identifier_type, 'value': identifier_value}
|
|
@@ -535,14 +631,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
535
631
|
|
|
536
632
|
def add_identifier(self, entity: Entity, identifier_type: str, identifier_value: str):
|
|
537
633
|
"""
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
Returns the internal identifier DB key
|
|
634
|
+
Add a new external identifier to an Entity object
|
|
541
635
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
"""
|
|
546
642
|
|
|
547
643
|
if self.major_version < 7 and self.minor_version < 1:
|
|
548
644
|
raise RuntimeError("add_identifier API call is not available when connected to a v6.0 System")
|
|
@@ -684,7 +780,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
684
780
|
end_point = f"{entity.path}/{entity.reference}/links/{relationship.api_id}"
|
|
685
781
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers)
|
|
686
782
|
if request.status_code == requests.codes.no_content:
|
|
687
|
-
|
|
783
|
+
return None
|
|
688
784
|
elif request.status_code == requests.codes.unauthorized:
|
|
689
785
|
self.token = self.__token__()
|
|
690
786
|
return self.__delete_relationship(relationship)
|
|
@@ -703,7 +799,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
703
799
|
:type: page_size: int
|
|
704
800
|
|
|
705
801
|
:param entity: The Source Entity
|
|
706
|
-
:type: entity: Entity
|
|
802
|
+
:type: entity: An Entity type such as Asset, Folder etc
|
|
707
803
|
|
|
708
804
|
:return: Generator
|
|
709
805
|
:rtype: Relationship
|
|
@@ -771,7 +867,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
771
867
|
|
|
772
868
|
return PagedSet(results, has_more, int(total_hits.text), url)
|
|
773
869
|
elif request.status_code == requests.codes.unauthorized:
|
|
774
|
-
self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
870
|
+
return self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
|
|
775
871
|
else:
|
|
776
872
|
exception = HTTPException(entity.reference, request.status_code, request.url, "relationships",
|
|
777
873
|
request.content.decode('utf-8'))
|
|
@@ -782,10 +878,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
782
878
|
"""
|
|
783
879
|
Add a new relationship link between two Assets or Folders
|
|
784
880
|
|
|
785
|
-
:param from_entity: The Source
|
|
881
|
+
:param from_entity: The Source entity to link from
|
|
786
882
|
:type from_entity: Entity
|
|
787
883
|
|
|
788
|
-
:param to_entity: The Target
|
|
884
|
+
:param to_entity: The Target entity
|
|
789
885
|
:type to_entity: Entity
|
|
790
886
|
|
|
791
887
|
:param relationship_type: The Relationship type
|
|
@@ -829,15 +925,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
829
925
|
logger.error(exception)
|
|
830
926
|
raise exception
|
|
831
927
|
|
|
832
|
-
def delete_metadata(self, entity:
|
|
928
|
+
def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
|
|
833
929
|
"""
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
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
|
|
837
932
|
|
|
838
|
-
:param entity: The
|
|
839
|
-
: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
|
|
840
937
|
"""
|
|
938
|
+
|
|
841
939
|
headers = {HEADER_TOKEN: self.token}
|
|
842
940
|
for url in entity.metadata:
|
|
843
941
|
if schema == entity.metadata[url]:
|
|
@@ -855,15 +953,45 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
855
953
|
|
|
856
954
|
return self.entity(entity.entity_type, entity.reference)
|
|
857
955
|
|
|
858
|
-
|
|
956
|
+
|
|
957
|
+
def add_group_metadata(self, csv_file: str) -> str:
|
|
859
958
|
"""
|
|
860
|
-
|
|
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
|
|
861
963
|
|
|
862
|
-
|
|
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
|
|
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
|
|
863
989
|
|
|
864
|
-
:param
|
|
865
|
-
:param
|
|
866
|
-
:param
|
|
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
|
|
867
995
|
"""
|
|
868
996
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
869
997
|
|
|
@@ -886,7 +1014,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
886
1014
|
content.append(tree.getroot())
|
|
887
1015
|
else:
|
|
888
1016
|
raise RuntimeError("Unknown data type")
|
|
889
|
-
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")
|
|
890
1018
|
logger.debug(xml_request)
|
|
891
1019
|
request = self.session.put(url, data=xml_request, headers=headers)
|
|
892
1020
|
if request.status_code == requests.codes.ok:
|
|
@@ -901,15 +1029,51 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
901
1029
|
raise exception
|
|
902
1030
|
return self.entity(entity.entity_type, entity.reference)
|
|
903
1031
|
|
|
904
|
-
def
|
|
1032
|
+
def add_metadata_as_fragment(self, entity: EntityT, schema: str, xml_fragment: str) -> EntityT:
|
|
905
1033
|
"""
|
|
906
|
-
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
|
|
907
1036
|
|
|
908
1037
|
Returns The updated Entity
|
|
909
1038
|
|
|
910
|
-
:param
|
|
911
|
-
:param entity: The
|
|
912
|
-
: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
|
|
913
1077
|
"""
|
|
914
1078
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
915
1079
|
|
|
@@ -941,13 +1105,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
941
1105
|
logger.error(exception)
|
|
942
1106
|
raise exception
|
|
943
1107
|
|
|
944
|
-
def save(self, entity:
|
|
1108
|
+
def save(self, entity: EntityT) -> EntityT:
|
|
945
1109
|
"""
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
Returns The updated Entity
|
|
1110
|
+
Updates the title and description of an entity
|
|
1111
|
+
The security tag and parent are not saved via this method call
|
|
949
1112
|
|
|
950
|
-
:param entity: The
|
|
1113
|
+
:param entity: The entity (asset, folder, content_object) to be updated
|
|
1114
|
+
:type entity: Entity
|
|
1115
|
+
:return: The updated entity
|
|
1116
|
+
:rtype: Entity
|
|
951
1117
|
"""
|
|
952
1118
|
|
|
953
1119
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
@@ -993,6 +1159,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
993
1159
|
if 'CustomType' in response:
|
|
994
1160
|
content_object.custom_type = response['CustomType']
|
|
995
1161
|
return content_object
|
|
1162
|
+
return None
|
|
996
1163
|
elif request.status_code == requests.codes.unauthorized:
|
|
997
1164
|
self.token = self.__token__()
|
|
998
1165
|
return self.save(entity)
|
|
@@ -1012,8 +1179,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1012
1179
|
|
|
1013
1180
|
Returns The updated Entity
|
|
1014
1181
|
|
|
1015
|
-
:param entity:
|
|
1016
|
-
: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
|
|
1017
1186
|
"""
|
|
1018
1187
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1019
1188
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -1036,7 +1205,18 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1036
1205
|
logger.error(exception)
|
|
1037
1206
|
raise exception
|
|
1038
1207
|
|
|
1208
|
+
def get_progress(self, pid: str) -> AsyncProgress:
|
|
1209
|
+
return AsyncProgress[self.get_async_progress(pid)]
|
|
1210
|
+
|
|
1039
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
|
+
"""
|
|
1040
1220
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1041
1221
|
request = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{pid}", headers=headers)
|
|
1042
1222
|
if request.status_code == requests.codes.ok:
|
|
@@ -1055,16 +1235,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1055
1235
|
logger.error(exception)
|
|
1056
1236
|
raise exception
|
|
1057
1237
|
|
|
1058
|
-
def move_sync(self, entity:
|
|
1238
|
+
def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
1059
1239
|
"""
|
|
1060
|
-
Move an
|
|
1061
|
-
|
|
1240
|
+
Move an entity (asset or folder) to a new folder
|
|
1241
|
+
This call blocks until the move is complete
|
|
1062
1242
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
:
|
|
1067
|
-
:param dest_folder: The Folder which will become the new parent of this entity
|
|
1243
|
+
:param Entity entity: The entity to move either asset or folder
|
|
1244
|
+
:param Entity dest_folder: The new destination folder. This can be None to move a folder to the root of the repository
|
|
1245
|
+
:return: The updated entity
|
|
1246
|
+
:rtype: Entity
|
|
1068
1247
|
"""
|
|
1069
1248
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1070
1249
|
if isinstance(entity, Asset) and dest_folder is None:
|
|
@@ -1095,15 +1274,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1095
1274
|
logger.error(exception)
|
|
1096
1275
|
raise exception
|
|
1097
1276
|
|
|
1098
|
-
def move(self, entity:
|
|
1277
|
+
def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
1099
1278
|
"""
|
|
1100
|
-
Move an
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
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.
|
|
1104
1281
|
|
|
1105
|
-
:param entity:
|
|
1106
|
-
: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
|
|
1107
1286
|
"""
|
|
1108
1287
|
return self.move_sync(entity, dest_folder)
|
|
1109
1288
|
|
|
@@ -1149,13 +1328,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1149
1328
|
logger.error(exception)
|
|
1150
1329
|
raise exception
|
|
1151
1330
|
|
|
1152
|
-
def all_metadata(self, entity: Entity) -> Tuple:
|
|
1331
|
+
def all_metadata(self, entity: Entity) -> Generator[Tuple[str, str], None, None]:
|
|
1153
1332
|
"""
|
|
1154
1333
|
Retrieve all metadata fragments on an entity
|
|
1155
1334
|
|
|
1156
1335
|
Returns XML documents in a tuple
|
|
1157
1336
|
|
|
1158
|
-
: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]]
|
|
1159
1340
|
"""
|
|
1160
1341
|
|
|
1161
1342
|
for uri, schema in entity.metadata.items():
|
|
@@ -1163,12 +1344,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1163
1344
|
|
|
1164
1345
|
def metadata_for_entity(self, entity: Entity, schema: str) -> Union[str, None]:
|
|
1165
1346
|
"""
|
|
1166
|
-
|
|
1347
|
+
Fetch the first metadata document which matches the schema URI from an entity
|
|
1167
1348
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
:
|
|
1171
|
-
:
|
|
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
|
|
1172
1353
|
"""
|
|
1173
1354
|
|
|
1174
1355
|
# if the entity is a lightweight enum version request the full object
|
|
@@ -1178,9 +1359,9 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1178
1359
|
for uri, schema_name in entity.metadata.items():
|
|
1179
1360
|
if schema == schema_name:
|
|
1180
1361
|
return self.metadata(uri)
|
|
1181
|
-
return
|
|
1362
|
+
return None
|
|
1182
1363
|
|
|
1183
|
-
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]:
|
|
1184
1365
|
"""
|
|
1185
1366
|
Retrieve the first value of the tag from a metadata template given by schema
|
|
1186
1367
|
|
|
@@ -1195,19 +1376,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1195
1376
|
xml_doc = self.metadata_for_entity(entity, schema)
|
|
1196
1377
|
if xml_doc:
|
|
1197
1378
|
xml_object = xml.etree.ElementTree.fromstring(xml_doc)
|
|
1198
|
-
if isXpath
|
|
1379
|
+
if not isXpath:
|
|
1199
1380
|
return xml_object.find(f'.//{{*}}{tag}').text
|
|
1200
1381
|
else:
|
|
1201
1382
|
return xml_object.find(tag).text
|
|
1383
|
+
return None
|
|
1202
1384
|
|
|
1203
|
-
def security_tag_sync(self, entity:
|
|
1385
|
+
def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
|
|
1204
1386
|
"""
|
|
1205
|
-
|
|
1387
|
+
Change the security tag of an asset or folder
|
|
1388
|
+
This is a blocking call which returns after all entities have been updated.
|
|
1206
1389
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
|
1211
1396
|
"""
|
|
1212
1397
|
self.token = self.__token__()
|
|
1213
1398
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
@@ -1234,12 +1419,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1234
1419
|
|
|
1235
1420
|
def security_tag_async(self, entity: Entity, new_tag: str):
|
|
1236
1421
|
"""
|
|
1237
|
-
|
|
1422
|
+
Change the security tag of an asset or folder
|
|
1423
|
+
This is a non blocking call which returns immediately.
|
|
1238
1424
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
|
1243
1431
|
"""
|
|
1244
1432
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
|
|
1245
1433
|
end_point = f"/{entity.path}/{entity.reference}/security-descriptor"
|
|
@@ -1258,11 +1446,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1258
1446
|
|
|
1259
1447
|
def metadata(self, uri: str) -> str:
|
|
1260
1448
|
"""
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
Returns XML document as a string
|
|
1449
|
+
Fetch the metadata document by its identifier, this is the key from the entity metadata map
|
|
1264
1450
|
|
|
1265
|
-
:param uri:
|
|
1451
|
+
:param str uri: The metadata identifier
|
|
1452
|
+
:return: An XML document as a string
|
|
1453
|
+
:rtype: str
|
|
1266
1454
|
"""
|
|
1267
1455
|
request = self.session.get(uri, headers={HEADER_TOKEN: self.token})
|
|
1268
1456
|
if request.status_code == requests.codes.ok:
|
|
@@ -1280,14 +1468,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1280
1468
|
logger.error(exception)
|
|
1281
1469
|
raise exception
|
|
1282
1470
|
|
|
1283
|
-
def entity(self, entity_type: EntityType, reference: str) ->
|
|
1471
|
+
def entity(self, entity_type: EntityType, reference: str) -> EntityT:
|
|
1284
1472
|
"""
|
|
1285
|
-
|
|
1473
|
+
Returns a generic entity based on its reference identifier
|
|
1286
1474
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
:param
|
|
1290
|
-
:
|
|
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
|
|
1291
1482
|
"""
|
|
1292
1483
|
if entity_type is EntityType.CONTENT_OBJECT:
|
|
1293
1484
|
return self.content_object(reference)
|
|
@@ -1295,6 +1486,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1295
1486
|
return self.folder(reference)
|
|
1296
1487
|
if entity_type is EntityType.ASSET:
|
|
1297
1488
|
return self.asset(reference)
|
|
1489
|
+
return None
|
|
1298
1490
|
|
|
1299
1491
|
def add_physical_asset(self, title: str, description: str, parent: Folder, security_tag: str = "open") -> Asset:
|
|
1300
1492
|
"""
|
|
@@ -1302,10 +1494,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1302
1494
|
|
|
1303
1495
|
Returns Asset
|
|
1304
1496
|
|
|
1305
|
-
:param title: The title of the new Asset
|
|
1306
|
-
:param description: The description of the new Asset
|
|
1307
|
-
:param parent: The parent folder
|
|
1308
|
-
: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
|
|
1309
1503
|
"""
|
|
1310
1504
|
|
|
1311
1505
|
if (self.major_version < 7) and (self.minor_version < 4):
|
|
@@ -1342,15 +1536,134 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1342
1536
|
logger.error(exception)
|
|
1343
1537
|
raise exception
|
|
1344
1538
|
|
|
1345
|
-
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:
|
|
1346
1627
|
"""
|
|
1347
1628
|
Retrieve an Asset by its reference
|
|
1348
1629
|
|
|
1349
|
-
Returns Asset
|
|
1630
|
+
Returns an XML document of the full Asset
|
|
1350
1631
|
|
|
1351
1632
|
:param reference: The unique identifier of the entity
|
|
1352
1633
|
"""
|
|
1353
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}
|
|
1354
1667
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{IO_PATH}/{reference}', headers=headers)
|
|
1355
1668
|
if request.status_code == requests.codes.ok:
|
|
1356
1669
|
xml_response = str(request.content.decode('utf-8'))
|
|
@@ -1376,11 +1689,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1376
1689
|
|
|
1377
1690
|
def folder(self, reference: str) -> Folder:
|
|
1378
1691
|
"""
|
|
1379
|
-
|
|
1692
|
+
Returns a folder object back by its internal reference identifier
|
|
1380
1693
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
|
1384
1699
|
"""
|
|
1385
1700
|
headers = {HEADER_TOKEN: self.token}
|
|
1386
1701
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{SO_PATH}/{reference}', headers=headers)
|
|
@@ -1408,11 +1723,13 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1408
1723
|
|
|
1409
1724
|
def content_object(self, reference: str) -> ContentObject:
|
|
1410
1725
|
"""
|
|
1411
|
-
|
|
1726
|
+
Returns a content object back by its internal reference identifier
|
|
1412
1727
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
|
1416
1733
|
"""
|
|
1417
1734
|
headers = {HEADER_TOKEN: self.token}
|
|
1418
1735
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/{CO_PATH}/{reference}', headers=headers)
|
|
@@ -1440,11 +1757,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1440
1757
|
|
|
1441
1758
|
def content_objects(self, representation: Representation) -> list[ContentObject]:
|
|
1442
1759
|
"""
|
|
1443
|
-
|
|
1760
|
+
Return a list of content objects for a representation
|
|
1444
1761
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1762
|
+
:param representation: The representation
|
|
1763
|
+
:type representation: Representation
|
|
1764
|
+
:return: List of content objects
|
|
1765
|
+
:rtype: list(ContentObject)
|
|
1448
1766
|
|
|
1449
1767
|
"""
|
|
1450
1768
|
headers = {HEADER_TOKEN: self.token}
|
|
@@ -1474,13 +1792,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1474
1792
|
logger.error(exception)
|
|
1475
1793
|
raise exception
|
|
1476
1794
|
|
|
1477
|
-
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
|
|
1478
1804
|
"""
|
|
1479
|
-
Retrieve a list of generation objects
|
|
1480
1805
|
|
|
1481
|
-
:param url:
|
|
1482
|
-
:returns Generation
|
|
1483
|
-
"""
|
|
1484
1806
|
headers = {HEADER_TOKEN: self.token}
|
|
1485
1807
|
request = self.session.get(url, headers=headers)
|
|
1486
1808
|
if request.status_code == requests.codes.ok:
|
|
@@ -1524,7 +1846,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1524
1846
|
bitstreams = entity_response.findall(f'./{{{self.entity_ns}}}Bitstreams/{{{self.entity_ns}}}Bitstream')
|
|
1525
1847
|
bitstream_list = []
|
|
1526
1848
|
for bit in bitstreams:
|
|
1527
|
-
|
|
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)
|
|
1528
1854
|
generation = Generation(strtobool(ge.attrib['original']), strtobool(ge.attrib['active']),
|
|
1529
1855
|
format_group.text if hasattr(format_group, 'text') else None,
|
|
1530
1856
|
effective_date.text if hasattr(effective_date, 'text') else None,
|
|
@@ -1609,11 +1935,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1609
1935
|
|
|
1610
1936
|
def bitstream(self, url: str) -> Bitstream:
|
|
1611
1937
|
"""
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
Returns Bitstream
|
|
1938
|
+
Fetch a bitstream object from the server using its URL
|
|
1615
1939
|
|
|
1616
|
-
|
|
1940
|
+
:param url: The URL to the bitstream
|
|
1941
|
+
:type url: str
|
|
1942
|
+
:return: a bitstream object
|
|
1943
|
+
:rtype: Bitstream
|
|
1617
1944
|
"""
|
|
1618
1945
|
headers = {HEADER_TOKEN: self.token}
|
|
1619
1946
|
request = self.session.get(url, headers=headers)
|
|
@@ -1649,9 +1976,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1649
1976
|
def replace_generation_sync(self, content_object: ContentObject, file_name, fixity_algorithm=None,
|
|
1650
1977
|
fixity_value=None) -> str:
|
|
1651
1978
|
"""
|
|
1652
|
-
|
|
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.
|
|
1653
1982
|
|
|
1654
|
-
|
|
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
|
|
1655
1989
|
|
|
1656
1990
|
"""
|
|
1657
1991
|
|
|
@@ -1670,7 +2004,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1670
2004
|
"""
|
|
1671
2005
|
Replace the last active generation of a content object with a new digital file.
|
|
1672
2006
|
|
|
1673
|
-
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
|
|
1674
2015
|
|
|
1675
2016
|
"""
|
|
1676
2017
|
if (self.major_version < 7) and (self.minor_version < 2) and (self.patch_version < 1):
|
|
@@ -1724,11 +2065,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1724
2065
|
|
|
1725
2066
|
def generations(self, content_object: ContentObject) -> list[Generation]:
|
|
1726
2067
|
"""
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
Returns list
|
|
2068
|
+
Return a list of Generation objects for a content object
|
|
1730
2069
|
|
|
1731
|
-
:param
|
|
2070
|
+
:param content_object: The content object
|
|
2071
|
+
:type content_object: ContentObject
|
|
2072
|
+
:return: list of generations
|
|
2073
|
+
:rtype: list(Generation)
|
|
1732
2074
|
"""
|
|
1733
2075
|
headers = {HEADER_TOKEN: self.token}
|
|
1734
2076
|
request = self.session.get(
|
|
@@ -1741,7 +2083,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1741
2083
|
result = []
|
|
1742
2084
|
for g in generations:
|
|
1743
2085
|
if hasattr(g, 'text'):
|
|
1744
|
-
generation = self.generation(g.text)
|
|
2086
|
+
generation = self.generation(g.text, content_object.reference)
|
|
1745
2087
|
generation.asset = content_object.asset
|
|
1746
2088
|
generation.content_object = content_object
|
|
1747
2089
|
generation.representation_type = content_object.representation_type
|
|
@@ -1758,13 +2100,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1758
2100
|
|
|
1759
2101
|
def bitstreams_for_asset(self, asset: Union[Asset, Entity]) -> Iterable[Bitstream]:
|
|
1760
2102
|
"""
|
|
1761
|
-
|
|
1762
|
-
Return all the bitstreams within an asset.
|
|
2103
|
+
Return all the active bitstreams within an asset.
|
|
1763
2104
|
This includes all the representations and content objects
|
|
1764
2105
|
|
|
1765
|
-
|
|
1766
2106
|
:param asset: The asset
|
|
1767
|
-
:return:
|
|
2107
|
+
:return: Iterable
|
|
1768
2108
|
"""
|
|
1769
2109
|
|
|
1770
2110
|
for representation in self.representations(asset):
|
|
@@ -1779,10 +2119,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1779
2119
|
|
|
1780
2120
|
def representations(self, asset: Asset) -> set[Representation]:
|
|
1781
2121
|
"""
|
|
1782
|
-
|
|
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.
|
|
1783
2125
|
|
|
1784
|
-
:param asset:
|
|
1785
|
-
:
|
|
2126
|
+
:param asset: The asset containing the required representations
|
|
2127
|
+
:type asset: Asset
|
|
2128
|
+
:return: Set of Representation objects
|
|
2129
|
+
:rtype: set(Representation)
|
|
1786
2130
|
"""
|
|
1787
2131
|
headers = {HEADER_TOKEN: self.token}
|
|
1788
2132
|
if not isinstance(asset, Asset):
|
|
@@ -1810,11 +2154,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1810
2154
|
|
|
1811
2155
|
def remove_thumbnail(self, entity: Entity):
|
|
1812
2156
|
"""
|
|
1813
|
-
|
|
2157
|
+
Remove the thumbnail for the entity to the uploaded image
|
|
1814
2158
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2159
|
+
:param entity: The entity with the thumbnail
|
|
2160
|
+
:type entity: Entity
|
|
2161
|
+
"""
|
|
1818
2162
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1819
2163
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1820
2164
|
|
|
@@ -1837,13 +2181,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1837
2181
|
logger.error(exception)
|
|
1838
2182
|
raise exception
|
|
1839
2183
|
|
|
2184
|
+
|
|
1840
2185
|
def add_access_representation(self, entity: Entity, access_file: str, name: str = "Access"):
|
|
1841
2186
|
"""
|
|
1842
|
-
Add a new representation to an existing asset.
|
|
2187
|
+
Add a new Access representation to an existing asset.
|
|
1843
2188
|
|
|
1844
|
-
:param entity:
|
|
1845
|
-
:param access_file: The new digital file
|
|
1846
|
-
: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"
|
|
1847
2192
|
:return:
|
|
1848
2193
|
"""
|
|
1849
2194
|
|
|
@@ -1876,12 +2221,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1876
2221
|
|
|
1877
2222
|
def add_thumbnail(self, entity: Entity, image_file: str):
|
|
1878
2223
|
"""
|
|
1879
|
-
|
|
2224
|
+
Set the thumbnail for the entity to the uploaded image
|
|
1880
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
|
+
"""
|
|
1881
2231
|
|
|
1882
|
-
:param entity: The Entity
|
|
1883
|
-
:param image_file: Path to image file
|
|
1884
|
-
"""
|
|
1885
2232
|
if self.major_version < 7 and self.minor_version < 2:
|
|
1886
2233
|
raise RuntimeError("Thumbnail API is only available when connected to a v6.2 System")
|
|
1887
2234
|
|
|
@@ -1922,7 +2269,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1922
2269
|
params=params, headers=headers)
|
|
1923
2270
|
|
|
1924
2271
|
if request.status_code == requests.codes.ok:
|
|
1925
|
-
|
|
2272
|
+
return None
|
|
1926
2273
|
elif request.status_code == requests.codes.unauthorized:
|
|
1927
2274
|
self.token = self.__token__()
|
|
1928
2275
|
return self._event_actions(entity, maximum=maximum)
|
|
@@ -1934,11 +2281,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1934
2281
|
|
|
1935
2282
|
def all_descendants(self, folder: Union[Folder, Entity] = None) -> Generator[Entity, None, None]:
|
|
1936
2283
|
"""
|
|
1937
|
-
|
|
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.
|
|
1938
2287
|
|
|
1939
|
-
|
|
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)
|
|
1940
2291
|
|
|
1941
|
-
:param folder: The folder to find children of
|
|
1942
2292
|
"""
|
|
1943
2293
|
for entity in self.descendants(folder=folder):
|
|
1944
2294
|
yield entity
|
|
@@ -1946,6 +2296,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1946
2296
|
yield from self.all_descendants(folder=entity)
|
|
1947
2297
|
|
|
1948
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
|
+
|
|
1949
2310
|
maximum = 100
|
|
1950
2311
|
paged_set = self.children(folder, maximum=maximum, next_page=None)
|
|
1951
2312
|
for entity in paged_set.results:
|
|
@@ -1955,7 +2316,23 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1955
2316
|
for entity in paged_set.results:
|
|
1956
2317
|
yield entity
|
|
1957
2318
|
|
|
2319
|
+
|
|
2320
|
+
|
|
1958
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
|
+
|
|
1959
2336
|
headers = {HEADER_TOKEN: self.token}
|
|
1960
2337
|
data = {'start': str(0), 'max': str(maximum)}
|
|
1961
2338
|
|
|
@@ -2002,12 +2379,22 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2002
2379
|
self.token = self.__token__()
|
|
2003
2380
|
return self.children(folder_reference, maximum=maximum, next_page=next_page)
|
|
2004
2381
|
else:
|
|
2005
|
-
exception = HTTPException(
|
|
2382
|
+
exception = HTTPException(folder_reference, request.status_code, request.url,
|
|
2006
2383
|
"children", request.content.decode('utf-8'))
|
|
2007
2384
|
logger.error(exception)
|
|
2008
2385
|
raise exception
|
|
2009
2386
|
|
|
2010
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
|
+
|
|
2011
2398
|
self.token = self.__token__()
|
|
2012
2399
|
previous = datetime.utcnow() - timedelta(days=previous_days)
|
|
2013
2400
|
from_date = previous.replace(tzinfo=timezone.utc).isoformat()
|
|
@@ -2023,6 +2410,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2023
2410
|
yield entity
|
|
2024
2411
|
|
|
2025
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
|
+
"""
|
|
2026
2421
|
self.token = self.__token__()
|
|
2027
2422
|
paged_set = self._all_events_page()
|
|
2028
2423
|
for entity in paged_set.results:
|
|
@@ -2048,9 +2443,15 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2048
2443
|
actions = entity_response.findall(f'.//{{{self.xip_ns}}}EventAction')
|
|
2049
2444
|
result_list = []
|
|
2050
2445
|
for action in actions:
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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)
|
|
2054
2455
|
next_url = entity_response.find(f'.//{{{self.entity_ns}}}Next')
|
|
2055
2456
|
total_hits = entity_response.find(f'.//{{{self.entity_ns}}}TotalResults')
|
|
2056
2457
|
has_more = True
|
|
@@ -2060,8 +2461,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2060
2461
|
else:
|
|
2061
2462
|
url = next_url.text
|
|
2062
2463
|
return PagedSet(result_list, has_more, int(total_hits.text), url)
|
|
2464
|
+
return None
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
|
|
2063
2468
|
|
|
2064
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
|
+
"""
|
|
2065
2475
|
self.token = self.__token__()
|
|
2066
2476
|
paged_set = self._entity_from_event_page(event_id, 25, None)
|
|
2067
2477
|
for entity in paged_set.results:
|
|
@@ -2084,6 +2494,8 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2084
2494
|
params["from"] = kwargs.get("from_date")
|
|
2085
2495
|
if "to_date" in kwargs:
|
|
2086
2496
|
params["to"] = kwargs.get("to_date")
|
|
2497
|
+
if "username" in kwargs:
|
|
2498
|
+
params["username"] = kwargs.get("username")
|
|
2087
2499
|
|
|
2088
2500
|
if next_page is None:
|
|
2089
2501
|
request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
|
|
@@ -2214,6 +2626,16 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2214
2626
|
raise exception
|
|
2215
2627
|
|
|
2216
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
|
+
"""
|
|
2217
2639
|
self.token = self.__token__()
|
|
2218
2640
|
paged_set = self._entity_events_page(entity)
|
|
2219
2641
|
for entity in paged_set.results:
|
|
@@ -2224,6 +2646,17 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2224
2646
|
yield entity
|
|
2225
2647
|
|
|
2226
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
|
+
|
|
2227
2660
|
self.token = self.__token__()
|
|
2228
2661
|
maximum = 25
|
|
2229
2662
|
paged_set = self._updated_entities_page(previous_days=previous_days, maximum=maximum, next_page=None)
|
|
@@ -2281,34 +2714,37 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2281
2714
|
logger.error(exception)
|
|
2282
2715
|
raise exception
|
|
2283
2716
|
|
|
2284
|
-
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"):
|
|
2285
2718
|
"""
|
|
2286
|
-
|
|
2719
|
+
Initiate and approve the deletion of an asset.
|
|
2287
2720
|
|
|
2288
|
-
:param asset:
|
|
2289
|
-
:param operator_comment:
|
|
2290
|
-
: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
|
|
2291
2726
|
"""
|
|
2292
2727
|
if isinstance(asset, Asset):
|
|
2293
|
-
return self._delete_entity(asset, operator_comment, supervisor_comment)
|
|
2728
|
+
return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
|
|
2294
2729
|
else:
|
|
2295
2730
|
raise RuntimeError("delete_asset only deletes assets")
|
|
2296
2731
|
|
|
2297
|
-
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"):
|
|
2298
2733
|
"""
|
|
2299
|
-
|
|
2300
|
-
|
|
2734
|
+
Initiate and approve the deletion of a folder.
|
|
2301
2735
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
|
2305
2741
|
"""
|
|
2306
2742
|
if isinstance(folder, Folder):
|
|
2307
|
-
return self._delete_entity(folder, operator_comment, supervisor_comment)
|
|
2743
|
+
return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
|
|
2308
2744
|
else:
|
|
2309
2745
|
raise RuntimeError("delete_folder only deletes folders")
|
|
2310
2746
|
|
|
2311
|
-
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"):
|
|
2312
2748
|
"""
|
|
2313
2749
|
Delete an asset from the repository
|
|
2314
2750
|
|
|
@@ -2319,7 +2755,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2319
2755
|
|
|
2320
2756
|
# check manager password is available:
|
|
2321
2757
|
config = configparser.ConfigParser()
|
|
2322
|
-
config.read(
|
|
2758
|
+
config.read(credentials_path, encoding='utf-8')
|
|
2323
2759
|
try:
|
|
2324
2760
|
manager_username = config['credentials']['manager.username']
|
|
2325
2761
|
manager_password = config['credentials']['manager.password']
|
|
@@ -2373,7 +2809,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2373
2809
|
headers=headers)
|
|
2374
2810
|
elif request.status_code == requests.codes.unauthorized:
|
|
2375
2811
|
self.token = self.__token__()
|
|
2376
|
-
return self._delete_entity(entity, operator_comment, supervisor_comment)
|
|
2812
|
+
return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
|
|
2377
2813
|
if request.status_code == requests.codes.unprocessable:
|
|
2378
2814
|
logger.error(request.content.decode('utf-8'))
|
|
2379
2815
|
raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
|
|
@@ -2386,3 +2822,4 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2386
2822
|
"_delete_entity", request.content.decode('utf-8'))
|
|
2387
2823
|
logger.error(exception)
|
|
2388
2824
|
raise exception
|
|
2825
|
+
|