pyPreservica 3.1.1__tar.gz → 3.2.1__tar.gz
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-3.1.1 → pypreservica-3.2.1}/PKG-INFO +3 -3
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/__init__.py +2 -1
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/common.py +27 -2
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/contentAPI.py +5 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/entityAPI.py +100 -13
- pypreservica-3.2.1/pyPreservica/settingsAPI.py +295 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica.egg-info/PKG-INFO +3 -3
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica.egg-info/SOURCES.txt +2 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica.egg-info/requires.txt +2 -2
- {pypreservica-3.1.1 → pypreservica-3.2.1}/setup.py +2 -2
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_identifier.py +12 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_metadata.py +6 -2
- pypreservica-3.2.1/tests/test_settings.py +28 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_workflow.py +1 -1
- {pypreservica-3.1.1 → pypreservica-3.2.1}/LICENSE.txt +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/README.md +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/adminAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/authorityAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/mdformsAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/monitorAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/opex.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/parAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/retentionAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/uploadAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/webHooksAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica/workflowAPI.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica.egg-info/dependency_links.txt +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/pyPreservica.egg-info/top_level.txt +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/setup.cfg +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_authority_records.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_bitstream.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_children.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_content_api.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_crawl_fs.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_delete.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_download.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_entity.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_export_opex.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_groups.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_ingest.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_integrity_check.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_par.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_replace.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_retention.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_schema.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_security.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_thumbnail.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_upload.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_users.py +0 -0
- {pypreservica-3.1.1 → pypreservica-3.2.1}/tests/test_xml_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyPreservica
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: Python library for the Preservica API
|
|
5
5
|
Home-page: https://pypreservica.readthedocs.io/
|
|
6
6
|
Author: James Carr
|
|
@@ -23,8 +23,8 @@ License-File: LICENSE.txt
|
|
|
23
23
|
Requires-Dist: requests
|
|
24
24
|
Requires-Dist: urllib3
|
|
25
25
|
Requires-Dist: certifi
|
|
26
|
-
Requires-Dist: boto3
|
|
27
|
-
Requires-Dist: botocore
|
|
26
|
+
Requires-Dist: boto3>=1.36.0
|
|
27
|
+
Requires-Dist: botocore>=1.36.0
|
|
28
28
|
Requires-Dist: s3transfer
|
|
29
29
|
Requires-Dist: azure-storage-blob
|
|
30
30
|
Requires-Dist: tqdm
|
|
@@ -30,10 +30,11 @@ from .monitorAPI import MonitorAPI, MonitorCategory, MonitorStatus, MessageStatu
|
|
|
30
30
|
from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler
|
|
31
31
|
from .authorityAPI import AuthorityAPI, Table
|
|
32
32
|
from .mdformsAPI import MetadataGroupsAPI, Group, GroupField, GroupFieldType
|
|
33
|
+
from .settingsAPI import SettingsAPI
|
|
33
34
|
|
|
34
35
|
__author__ = "James Carr (drjamescarr@gmail.com)"
|
|
35
36
|
|
|
36
37
|
# Version of the pyPreservica package
|
|
37
|
-
__version__ = "3.
|
|
38
|
+
__version__ = "3.2.1"
|
|
38
39
|
|
|
39
40
|
__license__ = "Apache License Version 2.0"
|
|
@@ -27,6 +27,7 @@ from requests import Session
|
|
|
27
27
|
from urllib3.util import Retry
|
|
28
28
|
import requests
|
|
29
29
|
from requests.adapters import HTTPAdapter
|
|
30
|
+
from typing import TypeVar
|
|
30
31
|
|
|
31
32
|
import pyPreservica
|
|
32
33
|
|
|
@@ -420,6 +421,26 @@ class Bitstream:
|
|
|
420
421
|
return self.__str__()
|
|
421
422
|
|
|
422
423
|
|
|
424
|
+
class ExternIdentifier:
|
|
425
|
+
"""
|
|
426
|
+
Class to represent the External Identifier Object in the Preservica data model
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
def __init__(self, identifier_type: str, identifier_value: str):
|
|
430
|
+
self.type = identifier_type
|
|
431
|
+
self.value = identifier_value
|
|
432
|
+
self.id = None
|
|
433
|
+
|
|
434
|
+
def __str__(self):
|
|
435
|
+
return f"""
|
|
436
|
+
Identifier: {self.id}
|
|
437
|
+
Identifier Type: {self.type}
|
|
438
|
+
Identifier Value: {self.value}
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
def __repr__(self):
|
|
442
|
+
return self.__str__()
|
|
443
|
+
|
|
423
444
|
class Generation:
|
|
424
445
|
"""
|
|
425
446
|
Class to represent the Generation Object in the Preservica data model
|
|
@@ -528,6 +549,9 @@ class ContentObject(Entity):
|
|
|
528
549
|
self.tag = "ContentObject"
|
|
529
550
|
|
|
530
551
|
|
|
552
|
+
EntityT = TypeVar("EntityT", Folder, Asset, ContentObject, None)
|
|
553
|
+
|
|
554
|
+
|
|
531
555
|
class Representation:
|
|
532
556
|
"""
|
|
533
557
|
Class to represent the Representation Object in the Preservica data model
|
|
@@ -738,7 +762,7 @@ class AuthenticatedAPI:
|
|
|
738
762
|
Return the edition of this tenancy
|
|
739
763
|
"""
|
|
740
764
|
if self.major_version < 8 and self.minor_version < 3:
|
|
741
|
-
raise RuntimeError("Entitlement
|
|
765
|
+
raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
|
|
742
766
|
|
|
743
767
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
744
768
|
|
|
@@ -777,6 +801,7 @@ class AuthenticatedAPI:
|
|
|
777
801
|
self.sec_ns = f"{NS_SEC_ROOT}/v{self.major_version}.{self.minor_version}"
|
|
778
802
|
self.admin_ns = f"{NS_ADMIN}/v{self.major_version}.{self.minor_version}"
|
|
779
803
|
|
|
804
|
+
|
|
780
805
|
def __version_number__(self):
|
|
781
806
|
"""
|
|
782
807
|
Determine the version number of the server
|
|
@@ -801,7 +826,7 @@ class AuthenticatedAPI:
|
|
|
801
826
|
RuntimeError(request.status_code, "version number failed")
|
|
802
827
|
|
|
803
828
|
def __str__(self):
|
|
804
|
-
return f"pyPreservica version: {pyPreservica.__version__} (Preservica
|
|
829
|
+
return f"pyPreservica version: {pyPreservica.__version__} (Preservica 8.0 Compatible) " \
|
|
805
830
|
f"Connected to: {self.server} Preservica version: {self.version} as {self.username} " \
|
|
806
831
|
f"in tenancy {self.tenant}"
|
|
807
832
|
|
|
@@ -34,6 +34,11 @@ class Field:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class ContentAPI(AuthenticatedAPI):
|
|
37
|
+
"""
|
|
38
|
+
The ContentAPI class provides the search interface to the Preservica repository.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
39
44
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
@@ -486,6 +486,54 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
486
486
|
logger.error(request)
|
|
487
487
|
raise RuntimeError(request.status_code, "delete_identifier failed")
|
|
488
488
|
|
|
489
|
+
def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
|
|
490
|
+
"""
|
|
491
|
+
Get all external identifiers on an entity
|
|
492
|
+
|
|
493
|
+
Returns the set of external identifiers on the entity
|
|
494
|
+
|
|
495
|
+
:param entity: The Entity (Asset or Folder)
|
|
496
|
+
:param external_identifier_type: Optional identifier type to filter the results
|
|
497
|
+
:type entity: Entity
|
|
498
|
+
"""
|
|
499
|
+
headers = {HEADER_TOKEN: self.token}
|
|
500
|
+
request = self.session.get(
|
|
501
|
+
f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
|
|
502
|
+
headers=headers)
|
|
503
|
+
if request.status_code == requests.codes.ok:
|
|
504
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
505
|
+
logger.debug(xml_response)
|
|
506
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
507
|
+
identifier_list = entity_response.findall(f'.//{{{self.xip_ns}}}Identifier')
|
|
508
|
+
result = set()
|
|
509
|
+
for identifier in identifier_list:
|
|
510
|
+
identifier_value = identifier_type = identifier_id = ""
|
|
511
|
+
for child in identifier:
|
|
512
|
+
if child.tag.endswith("Type"):
|
|
513
|
+
identifier_type = child.text
|
|
514
|
+
if child.tag.endswith("Value"):
|
|
515
|
+
identifier_value = child.text
|
|
516
|
+
if child.tag.endswith("ApiId"):
|
|
517
|
+
identifier_id = child.text
|
|
518
|
+
if external_identifier_type is None:
|
|
519
|
+
external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
|
|
520
|
+
external_id.identifier_id = identifier_id
|
|
521
|
+
result.add(external_id)
|
|
522
|
+
else:
|
|
523
|
+
if identifier_type == external_identifier_type:
|
|
524
|
+
external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
|
|
525
|
+
external_id.identifier_id = identifier_id
|
|
526
|
+
result.add(external_id)
|
|
527
|
+
return result
|
|
528
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
529
|
+
self.token = self.__token__()
|
|
530
|
+
return self.entity_identifiers(entity)
|
|
531
|
+
else:
|
|
532
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "identifiers_for_entity",
|
|
533
|
+
request.content.decode('utf-8'))
|
|
534
|
+
logger.error(exception)
|
|
535
|
+
raise exception
|
|
536
|
+
|
|
489
537
|
|
|
490
538
|
def identifiers_for_entity(self, entity: Entity) -> set[Tuple]:
|
|
491
539
|
"""
|
|
@@ -526,14 +574,14 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
526
574
|
|
|
527
575
|
|
|
528
576
|
|
|
529
|
-
def identifier(self, identifier_type: str, identifier_value: str) -> set[
|
|
577
|
+
def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
|
|
530
578
|
"""
|
|
531
|
-
|
|
579
|
+
Get all entities which have the external identifier
|
|
532
580
|
|
|
533
|
-
|
|
581
|
+
Returns the set of entities which have the external identifier
|
|
534
582
|
|
|
535
|
-
|
|
536
|
-
|
|
583
|
+
:param identifier_type: The identifier type
|
|
584
|
+
:param identifier_value: The identifier value
|
|
537
585
|
"""
|
|
538
586
|
headers = {HEADER_TOKEN: self.token}
|
|
539
587
|
payload = {'type': identifier_type, 'value': identifier_value}
|
|
@@ -861,7 +909,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
861
909
|
logger.error(exception)
|
|
862
910
|
raise exception
|
|
863
911
|
|
|
864
|
-
def delete_metadata(self, entity:
|
|
912
|
+
def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
|
|
865
913
|
"""
|
|
866
914
|
Deletes all the metadata fragments on an entity which match the schema URI
|
|
867
915
|
|
|
@@ -887,7 +935,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
887
935
|
|
|
888
936
|
return self.entity(entity.entity_type, entity.reference)
|
|
889
937
|
|
|
890
|
-
def update_metadata(self, entity:
|
|
938
|
+
def update_metadata(self, entity: EntityT, schema: str, data: Any) -> EntityT:
|
|
891
939
|
"""
|
|
892
940
|
Update all existing metadata fragments which match the schema
|
|
893
941
|
|
|
@@ -933,7 +981,45 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
933
981
|
raise exception
|
|
934
982
|
return self.entity(entity.entity_type, entity.reference)
|
|
935
983
|
|
|
936
|
-
def
|
|
984
|
+
def add_metadata_as_fragment(
|
|
985
|
+
self, entity: EntityT, schema: str, xml_fragment: str
|
|
986
|
+
) -> EntityT:
|
|
987
|
+
"""
|
|
988
|
+
Add a metadata fragment with a given namespace URI to an Entity
|
|
989
|
+
|
|
990
|
+
Don't parse the xml fragment which may add extra namespaces etc
|
|
991
|
+
|
|
992
|
+
Returns The updated Entity
|
|
993
|
+
|
|
994
|
+
:param xml_fragment: The new XML as a string
|
|
995
|
+
:param entity: The Entity to update
|
|
996
|
+
:param schema: The schema URI of the XML document
|
|
997
|
+
"""
|
|
998
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
999
|
+
|
|
1000
|
+
xml_doc = f"""<xip:MetadataContainer xmlns="{schema}" schemaUri="{schema}" xmlns:xip="{self.xip_ns}">
|
|
1001
|
+
<xip:Entity>{entity.reference}</xip:Entity>
|
|
1002
|
+
<xip:Content>
|
|
1003
|
+
{xml_fragment}
|
|
1004
|
+
</xip:Content>
|
|
1005
|
+
</xip:MetadataContainer>"""
|
|
1006
|
+
|
|
1007
|
+
end_point = f"/{entity.path}/{entity.reference}/metadata"
|
|
1008
|
+
logger.debug(xml_doc)
|
|
1009
|
+
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_doc,
|
|
1010
|
+
headers=headers)
|
|
1011
|
+
if request.status_code == requests.codes.ok:
|
|
1012
|
+
return self.entity(entity_type=entity.entity_type, reference=entity.reference)
|
|
1013
|
+
elif request.status_code == requests.codes.unauthorized:
|
|
1014
|
+
self.token = self.__token__()
|
|
1015
|
+
return self.add_metadata(entity, schema, xml_fragment)
|
|
1016
|
+
else:
|
|
1017
|
+
exception = HTTPException(entity.reference, request.status_code, request.url, "add_metadata",
|
|
1018
|
+
request.content.decode('utf-8'))
|
|
1019
|
+
logger.error(exception)
|
|
1020
|
+
raise exception
|
|
1021
|
+
|
|
1022
|
+
def add_metadata(self, entity: EntityT, schema: str, data) -> EntityT:
|
|
937
1023
|
"""
|
|
938
1024
|
Add a metadata fragment with a given namespace URI
|
|
939
1025
|
|
|
@@ -960,6 +1046,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
960
1046
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
961
1047
|
end_point = f"/{entity.path}/{entity.reference}/metadata"
|
|
962
1048
|
logger.debug(xml_request)
|
|
1049
|
+
print(xml_request)
|
|
963
1050
|
request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
|
|
964
1051
|
headers=headers)
|
|
965
1052
|
if request.status_code == requests.codes.ok:
|
|
@@ -973,7 +1060,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
973
1060
|
logger.error(exception)
|
|
974
1061
|
raise exception
|
|
975
1062
|
|
|
976
|
-
def save(self, entity:
|
|
1063
|
+
def save(self, entity: EntityT) -> EntityT:
|
|
977
1064
|
"""
|
|
978
1065
|
Save the title and description of an entity
|
|
979
1066
|
|
|
@@ -1091,7 +1178,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1091
1178
|
logger.error(exception)
|
|
1092
1179
|
raise exception
|
|
1093
1180
|
|
|
1094
|
-
def move_sync(self, entity:
|
|
1181
|
+
def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
1095
1182
|
"""
|
|
1096
1183
|
Move an Entity (Asset or Folder) to a new Folder
|
|
1097
1184
|
If dest_folder is None then the entity must be a Folder and will be moved to the root of the repository
|
|
@@ -1131,7 +1218,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1131
1218
|
logger.error(exception)
|
|
1132
1219
|
raise exception
|
|
1133
1220
|
|
|
1134
|
-
def move(self, entity:
|
|
1221
|
+
def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
|
|
1135
1222
|
"""
|
|
1136
1223
|
Move an Entity (Asset or Folder) to a new Folder
|
|
1137
1224
|
If dest_folder is None then the entity must be a Folder and will be moved to the root of the repository
|
|
@@ -1236,7 +1323,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1236
1323
|
else:
|
|
1237
1324
|
return xml_object.find(tag).text
|
|
1238
1325
|
|
|
1239
|
-
def security_tag_sync(self, entity:
|
|
1326
|
+
def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
|
|
1240
1327
|
"""
|
|
1241
1328
|
Change the security tag for a folder or asset
|
|
1242
1329
|
|
|
@@ -1316,7 +1403,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1316
1403
|
logger.error(exception)
|
|
1317
1404
|
raise exception
|
|
1318
1405
|
|
|
1319
|
-
def entity(self, entity_type: EntityType, reference: str) ->
|
|
1406
|
+
def entity(self, entity_type: EntityType, reference: str) -> EntityT:
|
|
1320
1407
|
"""
|
|
1321
1408
|
Retrieve an entity by its type and reference
|
|
1322
1409
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyPreservica Settings API module definition
|
|
3
|
+
|
|
4
|
+
API for retrieving information about configuration settings.
|
|
5
|
+
|
|
6
|
+
author: James Carr
|
|
7
|
+
licence: Apache License 2.0
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from pyPreservica.common import *
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SettingsAPI(AuthenticatedAPI):
|
|
19
|
+
"""
|
|
20
|
+
API for retrieving information about configuration settings.
|
|
21
|
+
|
|
22
|
+
Includes methods for:
|
|
23
|
+
|
|
24
|
+
* metadata-enrichment
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
username=None,
|
|
31
|
+
password=None,
|
|
32
|
+
tenant=None,
|
|
33
|
+
server=None,
|
|
34
|
+
use_shared_secret=False,
|
|
35
|
+
two_fa_secret_key: str = None,
|
|
36
|
+
protocol: str = "https",
|
|
37
|
+
request_hook: Callable = None,
|
|
38
|
+
credentials_path: str = "credentials.properties",
|
|
39
|
+
):
|
|
40
|
+
super().__init__(
|
|
41
|
+
username,
|
|
42
|
+
password,
|
|
43
|
+
tenant,
|
|
44
|
+
server,
|
|
45
|
+
use_shared_secret,
|
|
46
|
+
two_fa_secret_key,
|
|
47
|
+
protocol,
|
|
48
|
+
request_hook,
|
|
49
|
+
credentials_path,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if self.major_version < 7 and self.minor_version < 7:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"Settings API is only available when connected to a v7.7 System or higher"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self.base_url = "api/settings"
|
|
58
|
+
|
|
59
|
+
def metadata_enrichment_rules(self, profile_id: str = None) -> dict:
|
|
60
|
+
"""
|
|
61
|
+
Returns a list of metadata enrichment rules.
|
|
62
|
+
An empty selection implies that the rule is applied to all content.
|
|
63
|
+
Rules define where particular behaviours, defined by profiles, will be applied.
|
|
64
|
+
Rules are evaluated in order, with the first matching rule being applied.
|
|
65
|
+
|
|
66
|
+
:param profile_id: The rules for a specific profile id, Set to None for all rules
|
|
67
|
+
:type profile_id: str
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
headers = {
|
|
71
|
+
HEADER_TOKEN: self.token,
|
|
72
|
+
"Accept": "application/json",
|
|
73
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
endpoint: str = "/metadata-enrichment/config/rules"
|
|
77
|
+
|
|
78
|
+
request = self.session.get(
|
|
79
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}",
|
|
80
|
+
headers=headers)
|
|
81
|
+
|
|
82
|
+
if request.status_code == requests.codes.ok:
|
|
83
|
+
rules: dict = json.loads(request.content.decode("utf-8"))
|
|
84
|
+
if profile_id is None:
|
|
85
|
+
return rules
|
|
86
|
+
else:
|
|
87
|
+
profile_rules = []
|
|
88
|
+
for rule in rules["rules"]:
|
|
89
|
+
if rule["profileId"] == profile_id:
|
|
90
|
+
profile_rules.append(rule)
|
|
91
|
+
return {"rules": profile_rules}
|
|
92
|
+
else:
|
|
93
|
+
logger.debug(request.content.decode("utf-8"))
|
|
94
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_rules failed")
|
|
95
|
+
|
|
96
|
+
def metadata_enrichment_delete_rule(self, rule_id: str):
|
|
97
|
+
"""
|
|
98
|
+
Deletes a metadata enrichment rule.
|
|
99
|
+
|
|
100
|
+
:param rule_id: The rule id
|
|
101
|
+
:type rule_id: str
|
|
102
|
+
|
|
103
|
+
:return: No return value
|
|
104
|
+
:rtype: None
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
headers = {
|
|
108
|
+
HEADER_TOKEN: self.token,
|
|
109
|
+
"Accept": "application/json",
|
|
110
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
endpoint: str = f"/metadata-enrichment/config/rules/{rule_id}"
|
|
114
|
+
|
|
115
|
+
request = self.session.delete(
|
|
116
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}", headers=headers)
|
|
117
|
+
|
|
118
|
+
if request.status_code == requests.codes.no_content:
|
|
119
|
+
return
|
|
120
|
+
else:
|
|
121
|
+
logger.debug(request.content.decode("utf-8"))
|
|
122
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_delete_rule failed")
|
|
123
|
+
|
|
124
|
+
def metadata_enrichment_add_rule(self, profile_id: str, priority: int = 1):
|
|
125
|
+
"""
|
|
126
|
+
Create a metadata enrichment rule to control when metadata enrichment profiles are applied and return it.
|
|
127
|
+
Rules define where particular behaviours, defined by profiles, will be applied.
|
|
128
|
+
Rules are evaluated in order, with the first matching rule being applied.
|
|
129
|
+
Note that not specifying, or specifying an empty selection implies that the rule will be applied to all content.
|
|
130
|
+
Currently only securityDescriptorSelector, representationSelector and hierarchySelector are supported selectors.
|
|
131
|
+
If a rule already exists for the requested priority, existing rules will be shifted down priority to accommodate the new entry.
|
|
132
|
+
|
|
133
|
+
:param profile_id: The profile id
|
|
134
|
+
:type profile_id: str
|
|
135
|
+
|
|
136
|
+
:param priority: The rule priority
|
|
137
|
+
:type priority: int
|
|
138
|
+
|
|
139
|
+
:return: The metadata enrichment rule
|
|
140
|
+
:rtype: dict
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
headers = {
|
|
144
|
+
HEADER_TOKEN: self.token,
|
|
145
|
+
"Accept": "application/json",
|
|
146
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
endpoint: str = "/metadata-enrichment/config/rules"
|
|
150
|
+
|
|
151
|
+
rule: dict = {
|
|
152
|
+
"profileId": profile_id,
|
|
153
|
+
"priority": str(priority),
|
|
154
|
+
"selectorSettings": {},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
request = self.session.post(
|
|
158
|
+
f"{self.protocol}://{self.server}/{self.base_url}/{endpoint}",
|
|
159
|
+
headers=headers,
|
|
160
|
+
json=rule,
|
|
161
|
+
)
|
|
162
|
+
if request.status_code == requests.codes.created:
|
|
163
|
+
return json.loads(request.content.decode("utf-8"))
|
|
164
|
+
else:
|
|
165
|
+
logger.debug(request.content.decode("utf-8"))
|
|
166
|
+
raise RuntimeError(
|
|
167
|
+
request.status_code, f"metadata_enrichment_add_rule failed"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def metadata_enrichment_add_profile(self, name: str, active: bool = True):
|
|
171
|
+
"""
|
|
172
|
+
Create a metadata enrichment profile to control automatic metadata enrichment of content and return it.
|
|
173
|
+
Profiles define a set of behaviours that will be applied when the profile is selected by a rule.
|
|
174
|
+
A profile has no effect if it is not used by a rule. Includes settings for PII identification.
|
|
175
|
+
PII detection tools may be run against the full text extracted from content.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
:param name: The profile name
|
|
179
|
+
:type name: str
|
|
180
|
+
|
|
181
|
+
:param active: The profile active status
|
|
182
|
+
:type active: bool
|
|
183
|
+
|
|
184
|
+
:return: The metadata enrichment profile
|
|
185
|
+
:rtype: dict
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
headers = {
|
|
190
|
+
HEADER_TOKEN: self.token,
|
|
191
|
+
"Accept": "application/json",
|
|
192
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
endpoint: str = "/metadata-enrichment/config/profiles"
|
|
196
|
+
|
|
197
|
+
profile: dict = {"name": name, "piiSettings": {"active": str(active).lower()}}
|
|
198
|
+
|
|
199
|
+
request = self.session.post(
|
|
200
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}",
|
|
201
|
+
headers=headers, json=profile)
|
|
202
|
+
|
|
203
|
+
if request.status_code == requests.codes.created:
|
|
204
|
+
return json.loads(request.content.decode("utf-8"))
|
|
205
|
+
else:
|
|
206
|
+
logger.debug(request.content.decode("utf-8"))
|
|
207
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_add_profile failed")
|
|
208
|
+
|
|
209
|
+
def metadata_enrichment_profile(self, profile_id: str) -> dict:
|
|
210
|
+
"""
|
|
211
|
+
Returns a single profile by its ID
|
|
212
|
+
Profiles define a set of behaviours that will be applied when the profile is selected by a rule.
|
|
213
|
+
A profile has no effect if it is not used by a rule. Includes settings for PII identification.
|
|
214
|
+
PII detection tools may be run against the full text extracted from content.
|
|
215
|
+
|
|
216
|
+
:param profile_id: The profile name
|
|
217
|
+
:type profile_id: str
|
|
218
|
+
|
|
219
|
+
:return: The metadata enrichment profile
|
|
220
|
+
:rtype: dict
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
headers = {
|
|
224
|
+
HEADER_TOKEN: self.token,
|
|
225
|
+
"Accept": "application/json",
|
|
226
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
endpoint: str = f"/metadata-enrichment/config/profiles/{profile_id}"
|
|
230
|
+
|
|
231
|
+
request = self.session.get(
|
|
232
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}", headers=headers)
|
|
233
|
+
|
|
234
|
+
if request.status_code == requests.codes.ok:
|
|
235
|
+
return json.loads(request.content.decode("utf-8"))
|
|
236
|
+
else:
|
|
237
|
+
logger.debug(request.content.decode("utf-8"))
|
|
238
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_profile failed")
|
|
239
|
+
|
|
240
|
+
def metadata_enrichment_delete_profile(self, profile_id: str) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Deletes a metadata enrichment profile
|
|
243
|
+
|
|
244
|
+
:param profile_id: The profile name
|
|
245
|
+
:type profile_id: str
|
|
246
|
+
|
|
247
|
+
:return: No return value
|
|
248
|
+
:rtype: None
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
headers = {
|
|
252
|
+
HEADER_TOKEN: self.token,
|
|
253
|
+
"Accept": "application/json",
|
|
254
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
endpoint: str = f"/metadata-enrichment/config/profiles/{profile_id}"
|
|
258
|
+
|
|
259
|
+
request = self.session.delete(
|
|
260
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}", headers=headers)
|
|
261
|
+
|
|
262
|
+
if request.status_code == requests.codes.forbidden:
|
|
263
|
+
logger.debug(request.content.decode("utf-8"))
|
|
264
|
+
raise RuntimeError(request.status_code, f"Can't delete a profile with rules assigned")
|
|
265
|
+
|
|
266
|
+
if request.status_code == requests.codes.no_content:
|
|
267
|
+
return
|
|
268
|
+
else:
|
|
269
|
+
logger.debug(request.content.decode("utf-8"))
|
|
270
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_delete_profile failed")
|
|
271
|
+
|
|
272
|
+
def metadata_enrichment_profiles(self) -> dict:
|
|
273
|
+
"""
|
|
274
|
+
Returns the list of all metadata enrichment profiles.
|
|
275
|
+
Profiles define a set of behaviours that will be applied when the profile is selected by a rule.
|
|
276
|
+
A profile has no effect if it is not used by a rule. Includes settings for PII identification.
|
|
277
|
+
PII detection tools may be run against the full text extracted from content.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
headers = {
|
|
281
|
+
HEADER_TOKEN: self.token,
|
|
282
|
+
"Accept": "application/json",
|
|
283
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
endpoint: str = "/metadata-enrichment/config/profiles"
|
|
287
|
+
|
|
288
|
+
request = self.session.get(
|
|
289
|
+
f"{self.protocol}://{self.server}/{self.base_url}{endpoint}", headers=headers)
|
|
290
|
+
|
|
291
|
+
if request.status_code == requests.codes.ok:
|
|
292
|
+
return json.loads(request.content.decode("utf-8"))
|
|
293
|
+
else:
|
|
294
|
+
logger.debug(request.content.decode("utf-8"))
|
|
295
|
+
raise RuntimeError(request.status_code, f"metadata_enrichment_profiles failed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyPreservica
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: Python library for the Preservica API
|
|
5
5
|
Home-page: https://pypreservica.readthedocs.io/
|
|
6
6
|
Author: James Carr
|
|
@@ -23,8 +23,8 @@ License-File: LICENSE.txt
|
|
|
23
23
|
Requires-Dist: requests
|
|
24
24
|
Requires-Dist: urllib3
|
|
25
25
|
Requires-Dist: certifi
|
|
26
|
-
Requires-Dist: boto3
|
|
27
|
-
Requires-Dist: botocore
|
|
26
|
+
Requires-Dist: boto3>=1.36.0
|
|
27
|
+
Requires-Dist: botocore>=1.36.0
|
|
28
28
|
Requires-Dist: s3transfer
|
|
29
29
|
Requires-Dist: azure-storage-blob
|
|
30
30
|
Requires-Dist: tqdm
|
|
@@ -12,6 +12,7 @@ pyPreservica/monitorAPI.py
|
|
|
12
12
|
pyPreservica/opex.py
|
|
13
13
|
pyPreservica/parAPI.py
|
|
14
14
|
pyPreservica/retentionAPI.py
|
|
15
|
+
pyPreservica/settingsAPI.py
|
|
15
16
|
pyPreservica/uploadAPI.py
|
|
16
17
|
pyPreservica/webHooksAPI.py
|
|
17
18
|
pyPreservica/workflowAPI.py
|
|
@@ -39,6 +40,7 @@ tests/test_replace.py
|
|
|
39
40
|
tests/test_retention.py
|
|
40
41
|
tests/test_schema.py
|
|
41
42
|
tests/test_security.py
|
|
43
|
+
tests/test_settings.py
|
|
42
44
|
tests/test_thumbnail.py
|
|
43
45
|
tests/test_upload.py
|
|
44
46
|
tests/test_users.py
|
|
@@ -21,7 +21,7 @@ if sys.argv[-1] == 'publish':
|
|
|
21
21
|
# This call to setup() does all the work
|
|
22
22
|
setup(
|
|
23
23
|
name=PKG,
|
|
24
|
-
version="3.
|
|
24
|
+
version="3.2.1",
|
|
25
25
|
description="Python library for the Preservica API",
|
|
26
26
|
long_description=README,
|
|
27
27
|
long_description_content_type="text/markdown",
|
|
@@ -41,7 +41,7 @@ setup(
|
|
|
41
41
|
"Topic :: System :: Archiving",
|
|
42
42
|
],
|
|
43
43
|
keywords='Preservica API Preservation',
|
|
44
|
-
install_requires=["requests", "urllib3", "certifi", "boto3", "botocore", "s3transfer", "azure-storage-blob", "tqdm", "pyotp", "python-dateutil"],
|
|
44
|
+
install_requires=["requests", "urllib3", "certifi", "boto3>=1.36.0", "botocore>=1.36.0", "s3transfer", "azure-storage-blob", "tqdm", "pyotp", "python-dateutil"],
|
|
45
45
|
project_urls={
|
|
46
46
|
'Documentation': 'https://pypreservica.readthedocs.io',
|
|
47
47
|
'Source': 'https://github.com/carj/pyPreservica',
|
|
@@ -113,6 +113,18 @@ def test_get_identifier_asset(setup_data):
|
|
|
113
113
|
assert identifier[1] == "1234567890"
|
|
114
114
|
assert test_pass
|
|
115
115
|
|
|
116
|
+
def test_get_identifier_asset2(setup_data):
|
|
117
|
+
client = EntityAPI()
|
|
118
|
+
asset = client.asset(ASSET_ID)
|
|
119
|
+
|
|
120
|
+
client.add_identifier(asset, "ARK", "1234567890")
|
|
121
|
+
|
|
122
|
+
identifiers = client.entity_identifiers(asset)
|
|
123
|
+
assert len(identifiers) == 1
|
|
124
|
+
identifier = identifiers.pop()
|
|
125
|
+
assert identifier.type == "ARK"
|
|
126
|
+
assert identifier.value == "1234567890"
|
|
127
|
+
|
|
116
128
|
|
|
117
129
|
def test_delete_identifier_folder(setup_data):
|
|
118
130
|
client = EntityAPI()
|
|
@@ -65,21 +65,25 @@ def test_add_folder_metadata_no_prefix():
|
|
|
65
65
|
client = EntityAPI()
|
|
66
66
|
entity = client.entity(EntityType.FOLDER, FOLDER_ID)
|
|
67
67
|
folder = client.add_metadata(entity, "https://www.person.com/person", XML_DOCUMENT_NO_PREFIX)
|
|
68
|
+
client.delete_metadata(folder, "https://www.person.com/person")
|
|
68
69
|
|
|
69
70
|
def test_add_folder_metadata_with_ns():
|
|
70
71
|
client = EntityAPI()
|
|
71
72
|
entity = client.entity(EntityType.FOLDER, FOLDER_ID)
|
|
72
73
|
folder = client.add_metadata(entity, "https://www.person.com/person", XML_DOCUMENT)
|
|
74
|
+
client.delete_metadata(folder, "https://www.person.com/person")
|
|
73
75
|
|
|
74
76
|
def test_add_asset_metadata_no_prefix():
|
|
75
77
|
client = EntityAPI()
|
|
76
78
|
entity = client.entity(EntityType.ASSET, ASSET_ID)
|
|
77
|
-
|
|
79
|
+
asset = client.add_metadata(entity, "https://www.person.com/person", XML_DOCUMENT_NO_PREFIX)
|
|
80
|
+
client.delete_metadata(asset, "https://www.person.com/person")
|
|
78
81
|
|
|
79
82
|
def test_add_asset_metadata_with_ns():
|
|
80
83
|
client = EntityAPI()
|
|
81
84
|
entity = client.entity(EntityType.ASSET, ASSET_ID)
|
|
82
|
-
|
|
85
|
+
asset = client.add_metadata(entity, "https://www.person.com/person", XML_DOCUMENT)
|
|
86
|
+
client.delete_metadata(asset, "https://www.person.com/person")
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pyPreservica.settingsAPI import SettingsAPI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup():
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tear_down():
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def setup_data():
|
|
16
|
+
setup()
|
|
17
|
+
yield
|
|
18
|
+
tear_down()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_get_enrichment_profiles(setup_data):
|
|
23
|
+
client = SettingsAPI()
|
|
24
|
+
profiles = client.metadata_enrichment_profiles()
|
|
25
|
+
assert profiles is not None
|
|
26
|
+
|
|
27
|
+
def test_can_add_enrichment_profile(setup_data):
|
|
28
|
+
client = SettingsAPI()
|
|
@@ -24,7 +24,7 @@ def test_get_workflow_contexts_type():
|
|
|
24
24
|
workflow = WorkflowAPI()
|
|
25
25
|
|
|
26
26
|
workflows = workflow.get_workflow_contexts_by_type("Ingest")
|
|
27
|
-
assert len(workflows) ==
|
|
27
|
+
assert len(workflows) == 9
|
|
28
28
|
|
|
29
29
|
workflows = workflow.get_workflow_contexts_by_type("Access")
|
|
30
30
|
assert len(workflows) == 5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|