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/__init__.py
CHANGED
|
@@ -6,11 +6,22 @@ author: James Carr
|
|
|
6
6
|
licence: Apache License 2.0
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from .common import *
|
|
10
11
|
from .contentAPI import ContentAPI, Field, SortOrder
|
|
11
12
|
from .entityAPI import EntityAPI
|
|
12
|
-
from .uploadAPI import
|
|
13
|
-
|
|
13
|
+
from .uploadAPI import (
|
|
14
|
+
UploadAPI,
|
|
15
|
+
simple_asset_package,
|
|
16
|
+
complex_asset_package,
|
|
17
|
+
cvs_to_xsd,
|
|
18
|
+
cvs_to_xml,
|
|
19
|
+
cvs_to_cmis_xslt,
|
|
20
|
+
csv_to_search_xml,
|
|
21
|
+
generic_asset_package,
|
|
22
|
+
upload_config,
|
|
23
|
+
multi_asset_package,
|
|
24
|
+
)
|
|
14
25
|
from .workflowAPI import WorkflowAPI, WorkflowContext, WorkflowInstance
|
|
15
26
|
from .retentionAPI import RetentionAPI, RetentionAssignment, RetentionPolicy
|
|
16
27
|
from .parAPI import PreservationActionRegistry
|
|
@@ -19,10 +30,11 @@ from .monitorAPI import MonitorAPI, MonitorCategory, MonitorStatus, MessageStatu
|
|
|
19
30
|
from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler
|
|
20
31
|
from .authorityAPI import AuthorityAPI, Table
|
|
21
32
|
from .mdformsAPI import MetadataGroupsAPI, Group, GroupField, GroupFieldType
|
|
33
|
+
from .settingsAPI import SettingsAPI
|
|
22
34
|
|
|
23
35
|
__author__ = "James Carr (drjamescarr@gmail.com)"
|
|
24
36
|
|
|
25
37
|
# Version of the pyPreservica package
|
|
26
|
-
__version__ = "
|
|
38
|
+
__version__ = "3.3.3"
|
|
27
39
|
|
|
28
40
|
__license__ = "Apache License Version 2.0"
|
pyPreservica/adminAPI.py
CHANGED
|
@@ -10,7 +10,7 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
import csv
|
|
12
12
|
import xml.etree.ElementTree
|
|
13
|
-
from typing import List, Any
|
|
13
|
+
from typing import List, Any, Union
|
|
14
14
|
|
|
15
15
|
from pyPreservica.common import *
|
|
16
16
|
|
|
@@ -36,7 +36,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
36
36
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/security/roles/{role_name}',
|
|
37
37
|
headers=headers)
|
|
38
38
|
if request.status_code == requests.codes.no_content:
|
|
39
|
-
return
|
|
39
|
+
return None
|
|
40
40
|
elif request.status_code == requests.codes.unauthorized:
|
|
41
41
|
self.token = self.__token__()
|
|
42
42
|
return self.delete_system_role(role_name)
|
|
@@ -61,7 +61,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
61
61
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/security/tags/{tag_name}',
|
|
62
62
|
headers=headers)
|
|
63
63
|
if request.status_code == requests.codes.no_content:
|
|
64
|
-
return
|
|
64
|
+
return None
|
|
65
65
|
elif request.status_code == requests.codes.unauthorized:
|
|
66
66
|
self.token = self.__token__()
|
|
67
67
|
return self.delete_security_tag(tag_name)
|
|
@@ -211,7 +211,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
211
211
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
|
|
212
212
|
request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/users/{username}', headers=headers)
|
|
213
213
|
if request.status_code == requests.codes.no_content:
|
|
214
|
-
return
|
|
214
|
+
return None
|
|
215
215
|
elif request.status_code == requests.codes.unauthorized:
|
|
216
216
|
self.token = self.__token__()
|
|
217
217
|
return self.delete_user(username)
|
|
@@ -251,8 +251,9 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
251
251
|
xml.etree.ElementTree.SubElement(xml_roles, "Role").text = role
|
|
252
252
|
xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
|
|
253
253
|
logger.debug(xml_request)
|
|
254
|
+
params = {"source": "UX2"}
|
|
254
255
|
request = self.session.post(f'{self.protocol}://{self.server}/api/admin/users', data=xml_request,
|
|
255
|
-
headers=headers)
|
|
256
|
+
headers=headers, params=params)
|
|
256
257
|
if request.status_code == requests.codes.created:
|
|
257
258
|
return self.user_details(username)
|
|
258
259
|
elif request.status_code == requests.codes.unauthorized:
|
|
@@ -444,7 +445,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
444
445
|
:param xml_data: The xml schema as a UTF-8 string or a file like object
|
|
445
446
|
:type xml_data: Any
|
|
446
447
|
|
|
447
|
-
:return:
|
|
448
|
+
:return:
|
|
448
449
|
:rtype: None
|
|
449
450
|
"""
|
|
450
451
|
|
|
@@ -463,7 +464,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
463
464
|
params=params,
|
|
464
465
|
data=xml_data)
|
|
465
466
|
if request.status_code == requests.codes.created:
|
|
466
|
-
return
|
|
467
|
+
return None
|
|
467
468
|
elif request.status_code == requests.codes.unauthorized:
|
|
468
469
|
self.token = self.__token__()
|
|
469
470
|
return self.add_xml_schema(name, description, originalName, xml_data)
|
|
@@ -493,7 +494,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
493
494
|
:param document_type: The type of the XML document, defaults to descriptive metadata templates
|
|
494
495
|
:type document_type: str
|
|
495
496
|
|
|
496
|
-
:return:
|
|
497
|
+
:return:
|
|
497
498
|
:rtype: None
|
|
498
499
|
|
|
499
500
|
"""
|
|
@@ -513,7 +514,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
513
514
|
params=params,
|
|
514
515
|
data=xml_data)
|
|
515
516
|
if request.status_code == requests.codes.created:
|
|
516
|
-
return
|
|
517
|
+
return None
|
|
517
518
|
elif request.status_code == requests.codes.unauthorized:
|
|
518
519
|
self.token = self.__token__()
|
|
519
520
|
return self.add_xml_document(name, xml_data, document_type)
|
|
@@ -523,12 +524,12 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
523
524
|
|
|
524
525
|
def delete_xml_document(self, uri: str):
|
|
525
526
|
"""
|
|
526
|
-
Delete
|
|
527
|
+
Delete an XML document from Preservica's XML document store
|
|
527
528
|
|
|
528
529
|
:param uri: The URI of the xml document to delete
|
|
529
530
|
:type uri: str
|
|
530
531
|
|
|
531
|
-
:return:
|
|
532
|
+
:return:
|
|
532
533
|
:rtype: None
|
|
533
534
|
|
|
534
535
|
"""
|
|
@@ -543,13 +544,14 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
543
544
|
f"{self.protocol}://{self.server}/api/admin/documents/{document['ApiId']}",
|
|
544
545
|
headers=headers)
|
|
545
546
|
if request.status_code == requests.codes.no_content:
|
|
546
|
-
return
|
|
547
|
+
return None
|
|
547
548
|
elif request.status_code == requests.codes.unauthorized:
|
|
548
549
|
self.token = self.__token__()
|
|
549
550
|
return self.delete_xml_document(uri)
|
|
550
551
|
else:
|
|
551
552
|
logger.error(request.content.decode('utf-8'))
|
|
552
553
|
raise RuntimeError(request.status_code, "delete_xml_document failed")
|
|
554
|
+
return None
|
|
553
555
|
|
|
554
556
|
def delete_xml_schema(self, uri: str):
|
|
555
557
|
"""
|
|
@@ -558,7 +560,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
558
560
|
:param uri: The URI of the xml schema to delete
|
|
559
561
|
:type uri: str
|
|
560
562
|
|
|
561
|
-
:return:
|
|
563
|
+
:return:
|
|
562
564
|
:rtype: None
|
|
563
565
|
|
|
564
566
|
"""
|
|
@@ -572,17 +574,18 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
572
574
|
request = self.session.delete(f"{self.protocol}://{self.server}/api/admin/schemas/{schema['ApiId']}",
|
|
573
575
|
headers=headers)
|
|
574
576
|
if request.status_code == requests.codes.no_content:
|
|
575
|
-
return
|
|
577
|
+
return None
|
|
576
578
|
elif request.status_code == requests.codes.unauthorized:
|
|
577
579
|
self.token = self.__token__()
|
|
578
580
|
return self.delete_xml_schema(uri)
|
|
579
581
|
else:
|
|
580
582
|
logger.error(request.content.decode('utf-8'))
|
|
581
583
|
raise RuntimeError(request.status_code, "delete_xml_schema failed")
|
|
584
|
+
return None
|
|
582
585
|
|
|
583
|
-
def xml_schema(self, uri: str) -> str:
|
|
586
|
+
def xml_schema(self, uri: str) -> Union[str, None]:
|
|
584
587
|
"""
|
|
585
|
-
|
|
588
|
+
Fetch the metadata schema XSD document as a string by its URI
|
|
586
589
|
|
|
587
590
|
:param uri: The URI of the xml schema
|
|
588
591
|
:type uri: str
|
|
@@ -607,8 +610,9 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
607
610
|
else:
|
|
608
611
|
logger.error(request.content.decode('utf-8'))
|
|
609
612
|
raise RuntimeError(request.status_code, "xml_schema failed")
|
|
613
|
+
return None
|
|
610
614
|
|
|
611
|
-
def xml_document(self, uri: str) -> str:
|
|
615
|
+
def xml_document(self, uri: str) -> Union[str, None]:
|
|
612
616
|
"""
|
|
613
617
|
fetch the metadata XML document as a string by its URI
|
|
614
618
|
|
|
@@ -634,6 +638,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
634
638
|
else:
|
|
635
639
|
logger.error(request.content.decode('utf-8'))
|
|
636
640
|
raise RuntimeError(request.status_code, "xml_document failed")
|
|
641
|
+
return None
|
|
637
642
|
|
|
638
643
|
def xml_documents(self) -> List:
|
|
639
644
|
"""
|
|
@@ -754,7 +759,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
754
759
|
logger.error(request.content.decode('utf-8'))
|
|
755
760
|
raise RuntimeError(request.status_code, "xml_transforms failed")
|
|
756
761
|
|
|
757
|
-
def xml_transform(self, input_uri: str, output_uri: str) -> str:
|
|
762
|
+
def xml_transform(self, input_uri: str, output_uri: str) -> Union[str, None]:
|
|
758
763
|
"""
|
|
759
764
|
fetch the XML transform as a string by its URIs
|
|
760
765
|
|
|
@@ -782,6 +787,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
782
787
|
else:
|
|
783
788
|
logger.error(request.content.decode('utf-8'))
|
|
784
789
|
raise RuntimeError(request.status_code, "xml_transform failed")
|
|
790
|
+
return None
|
|
785
791
|
|
|
786
792
|
def delete_xml_transform(self, input_uri: str, output_uri: str):
|
|
787
793
|
"""
|
|
@@ -793,7 +799,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
793
799
|
:param output_uri: The URI of the output XML document
|
|
794
800
|
:type output_uri: str
|
|
795
801
|
|
|
796
|
-
:return:
|
|
802
|
+
:return:
|
|
797
803
|
:rtype: None
|
|
798
804
|
|
|
799
805
|
"""
|
|
@@ -808,13 +814,14 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
808
814
|
f"{self.protocol}://{self.server}/api/admin/transforms/{transform['ApiId']}",
|
|
809
815
|
headers=headers)
|
|
810
816
|
if request.status_code == requests.codes.no_content:
|
|
811
|
-
return
|
|
817
|
+
return None
|
|
812
818
|
elif request.status_code == requests.codes.unauthorized:
|
|
813
819
|
self.token = self.__token__()
|
|
814
820
|
return self.delete_xml_transform(input_uri, output_uri)
|
|
815
821
|
else:
|
|
816
822
|
logger.error(request.content.decode('utf-8'))
|
|
817
823
|
raise RuntimeError(request.status_code, "delete_xml_transform failed")
|
|
824
|
+
return None
|
|
818
825
|
|
|
819
826
|
def add_xml_transform(self, name: str, input_uri: str, output_uri: str, purpose: str, originalName: str,
|
|
820
827
|
xml_data: Any):
|
|
@@ -839,7 +846,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
839
846
|
:param xml_data: The transform xml as a string or file like object
|
|
840
847
|
:type xml_data: Any
|
|
841
848
|
|
|
842
|
-
:return:
|
|
849
|
+
:return:
|
|
843
850
|
:rtype: None
|
|
844
851
|
|
|
845
852
|
"""
|
|
@@ -860,7 +867,7 @@ class AdminAPI(AuthenticatedAPI):
|
|
|
860
867
|
params=params,
|
|
861
868
|
data=xml_data)
|
|
862
869
|
if request.status_code == requests.codes.created:
|
|
863
|
-
return
|
|
870
|
+
return None
|
|
864
871
|
|
|
865
872
|
if request.status_code == requests.codes.unauthorized:
|
|
866
873
|
self.token = self.__token__()
|
pyPreservica/authorityAPI.py
CHANGED
|
@@ -8,9 +8,8 @@ author: James Carr
|
|
|
8
8
|
licence: Apache License 2.0
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import csv
|
|
13
|
-
import requests
|
|
14
13
|
from typing import List, Set
|
|
15
14
|
|
|
16
15
|
from pyPreservica.common import *
|
|
@@ -55,7 +54,7 @@ class AuthorityAPI(AuthenticatedAPI):
|
|
|
55
54
|
self.token = self.__token__()
|
|
56
55
|
return self.delete_record(reference)
|
|
57
56
|
if response.status_code == requests.codes.no_content:
|
|
58
|
-
return
|
|
57
|
+
return None
|
|
59
58
|
else:
|
|
60
59
|
exception = HTTPException("", response.status_code, response.url, "delete_record",
|
|
61
60
|
response.content.decode('utf-8'))
|
|
@@ -92,7 +91,7 @@ class AuthorityAPI(AuthenticatedAPI):
|
|
|
92
91
|
:param table: The Table to add the record to
|
|
93
92
|
:type: table: Table
|
|
94
93
|
|
|
95
|
-
:param record: The record
|
|
94
|
+
:param record: The record as a dictionary
|
|
96
95
|
:type: record: dict
|
|
97
96
|
|
|
98
97
|
:return: A single record
|
|
@@ -123,7 +122,7 @@ class AuthorityAPI(AuthenticatedAPI):
|
|
|
123
122
|
"""
|
|
124
123
|
Return a record by its reference
|
|
125
124
|
|
|
126
|
-
:param reference: The record
|
|
125
|
+
:param reference: The reference of the record
|
|
127
126
|
:type: reference: str
|
|
128
127
|
|
|
129
128
|
:return: A single record
|
|
@@ -149,7 +148,7 @@ class AuthorityAPI(AuthenticatedAPI):
|
|
|
149
148
|
"""
|
|
150
149
|
Return all records from a table
|
|
151
150
|
|
|
152
|
-
:param table: The authority table
|
|
151
|
+
:param table: The authority table to return the records from
|
|
153
152
|
:type: table: Table
|
|
154
153
|
|
|
155
154
|
:return: List of records
|
|
@@ -178,7 +177,7 @@ class AuthorityAPI(AuthenticatedAPI):
|
|
|
178
177
|
:param reference: The reference for the authority table
|
|
179
178
|
:type: reference: str
|
|
180
179
|
|
|
181
|
-
:return: An authority table
|
|
180
|
+
:return: An authority table of interest
|
|
182
181
|
:rtype: Table
|
|
183
182
|
|
|
184
183
|
"""
|
pyPreservica/common.py
CHANGED
|
@@ -23,10 +23,13 @@ import xml.etree.ElementTree
|
|
|
23
23
|
from enum import Enum
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
import pyotp
|
|
26
|
-
from requests import
|
|
26
|
+
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
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
import dateutil
|
|
30
33
|
|
|
31
34
|
import pyPreservica
|
|
32
35
|
|
|
@@ -405,6 +408,9 @@ class Bitstream:
|
|
|
405
408
|
self.length = int(length)
|
|
406
409
|
self.fixity = fixity
|
|
407
410
|
self.content_url = content_url
|
|
411
|
+
self.bs_index = None
|
|
412
|
+
self.gen_index = None
|
|
413
|
+
self.co_ref = None
|
|
408
414
|
|
|
409
415
|
def __str__(self):
|
|
410
416
|
return f"""
|
|
@@ -417,6 +423,26 @@ class Bitstream:
|
|
|
417
423
|
return self.__str__()
|
|
418
424
|
|
|
419
425
|
|
|
426
|
+
class ExternIdentifier:
|
|
427
|
+
"""
|
|
428
|
+
Class to represent the External Identifier Object in the Preservica data model
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, identifier_type: str, identifier_value: str):
|
|
432
|
+
self.type = identifier_type
|
|
433
|
+
self.value = identifier_value
|
|
434
|
+
self.id = None
|
|
435
|
+
|
|
436
|
+
def __str__(self):
|
|
437
|
+
return f"""
|
|
438
|
+
Identifier: {self.id}
|
|
439
|
+
Identifier Type: {self.type}
|
|
440
|
+
Identifier Value: {self.value}
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
def __repr__(self):
|
|
444
|
+
return self.__str__()
|
|
445
|
+
|
|
420
446
|
class Generation:
|
|
421
447
|
"""
|
|
422
448
|
Class to represent the Generation Object in the Preservica data model
|
|
@@ -525,6 +551,9 @@ class ContentObject(Entity):
|
|
|
525
551
|
self.tag = "ContentObject"
|
|
526
552
|
|
|
527
553
|
|
|
554
|
+
EntityT = TypeVar("EntityT", Folder, Asset, ContentObject, None)
|
|
555
|
+
|
|
556
|
+
|
|
528
557
|
class Representation:
|
|
529
558
|
"""
|
|
530
559
|
Class to represent the Representation Object in the Preservica data model
|
|
@@ -565,12 +594,28 @@ class Thumbnail(Enum):
|
|
|
565
594
|
LARGE = "large"
|
|
566
595
|
|
|
567
596
|
|
|
597
|
+
class AsyncProgress(Enum):
|
|
598
|
+
"""
|
|
599
|
+
Enumeration of the possible status of an asynchronous process
|
|
600
|
+
"""
|
|
601
|
+
ABORTED = "ABORTED"
|
|
602
|
+
ACTIVE = "ACTIVE"
|
|
603
|
+
COMPLETED = "COMPLETED"
|
|
604
|
+
PENDING = "PENDING"
|
|
605
|
+
SUSPENDING = "SUSPENDING"
|
|
606
|
+
SUSPENDED = "SUSPENDED"
|
|
607
|
+
UNKNOWN = "UNKNOWN"
|
|
608
|
+
FAILED = "FAILED"
|
|
609
|
+
FINISHED_MIXED_OUTCOME = "FINISHED_MIXED_OUTCOME"
|
|
610
|
+
CANCELLED = "CANCELLED"
|
|
611
|
+
|
|
612
|
+
|
|
568
613
|
def sanitize(filename) -> str:
|
|
569
614
|
"""
|
|
570
615
|
Return a fairly safe version of the filename.
|
|
571
616
|
|
|
572
617
|
We don't limit ourselves to ascii, because we want to keep municipality
|
|
573
|
-
names, etc
|
|
618
|
+
names, etc., but we do want to get rid of anything potentially harmful,
|
|
574
619
|
and make sure we do not exceed Windows filename length limits.
|
|
575
620
|
Hence, a less safe blacklist, rather than a whitelist.
|
|
576
621
|
"""
|
|
@@ -630,19 +675,24 @@ class AuthenticatedAPI:
|
|
|
630
675
|
logger.error(f"The AdminAPI requires the user to have ROLE_SDB_MANAGER_USER")
|
|
631
676
|
raise RuntimeError(f"The API requires the user to have at least the ROLE_SDB_MANAGER_USER")
|
|
632
677
|
|
|
633
|
-
def _find_user_roles_(self) -> list:
|
|
678
|
+
def _find_user_roles_(self) -> list[str]:
|
|
634
679
|
"""
|
|
635
680
|
Get a list of roles for the user
|
|
636
681
|
:return list of roles:
|
|
637
682
|
"""
|
|
638
|
-
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/
|
|
683
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
639
684
|
request = self.session.get(f"{self.protocol}://{self.server}/api/user/details", headers=headers)
|
|
685
|
+
logger.debug(request.headers)
|
|
640
686
|
if request.status_code == requests.codes.ok:
|
|
641
|
-
|
|
687
|
+
json_document = str(request.content.decode('utf-8'))
|
|
688
|
+
logger.debug(json_document)
|
|
689
|
+
roles: list[str] = json.loads(json_document)['roles']
|
|
642
690
|
return roles
|
|
643
691
|
elif request.status_code == requests.codes.unauthorized:
|
|
644
692
|
self.token = self.__token__()
|
|
645
693
|
return self._find_user_roles_()
|
|
694
|
+
return []
|
|
695
|
+
|
|
646
696
|
|
|
647
697
|
def security_tags_base(self, with_permissions: bool = False) -> dict:
|
|
648
698
|
"""
|
|
@@ -719,7 +769,7 @@ class AuthenticatedAPI:
|
|
|
719
769
|
Return the edition of this tenancy
|
|
720
770
|
"""
|
|
721
771
|
if self.major_version < 8 and self.minor_version < 3:
|
|
722
|
-
raise RuntimeError("Entitlement
|
|
772
|
+
raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
|
|
723
773
|
|
|
724
774
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
725
775
|
|
|
@@ -758,6 +808,8 @@ class AuthenticatedAPI:
|
|
|
758
808
|
self.sec_ns = f"{NS_SEC_ROOT}/v{self.major_version}.{self.minor_version}"
|
|
759
809
|
self.admin_ns = f"{NS_ADMIN}/v{self.major_version}.{self.minor_version}"
|
|
760
810
|
|
|
811
|
+
xml.etree.ElementTree.register_namespace("xip", f"{self.xip_ns}")
|
|
812
|
+
|
|
761
813
|
def __version_number__(self):
|
|
762
814
|
"""
|
|
763
815
|
Determine the version number of the server
|
|
@@ -772,6 +824,7 @@ class AuthenticatedAPI:
|
|
|
772
824
|
self.major_version = int(version_numbers[0])
|
|
773
825
|
self.minor_version = int(version_numbers[1])
|
|
774
826
|
self.patch_version = int(version_numbers[2])
|
|
827
|
+
|
|
775
828
|
return version
|
|
776
829
|
elif request.status_code == requests.codes.unauthorized:
|
|
777
830
|
self.token = self.__token__()
|
|
@@ -780,9 +833,12 @@ class AuthenticatedAPI:
|
|
|
780
833
|
logger.error(f"version number failed with http response {request.status_code}")
|
|
781
834
|
logger.error(str(request.content))
|
|
782
835
|
RuntimeError(request.status_code, "version number failed")
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
|
|
783
839
|
|
|
784
840
|
def __str__(self):
|
|
785
|
-
return f"pyPreservica version: {pyPreservica.__version__} (Preservica
|
|
841
|
+
return f"pyPreservica version: {pyPreservica.__version__} (Preservica 8.0 Compatible) " \
|
|
786
842
|
f"Connected to: {self.server} Preservica version: {self.version} as {self.username} " \
|
|
787
843
|
f"in tenancy {self.tenant}"
|
|
788
844
|
|
|
@@ -799,7 +855,7 @@ class AuthenticatedAPI:
|
|
|
799
855
|
with open('credentials.properties', 'wt', encoding="utf-8") as configfile:
|
|
800
856
|
config.write(configfile)
|
|
801
857
|
|
|
802
|
-
def manager_token(self, username: str, password: str):
|
|
858
|
+
def manager_token(self, username: str, password: str) -> str:
|
|
803
859
|
data = {'username': username, 'password': password, 'tenant': self.tenant}
|
|
804
860
|
response = self.session.post(f'{self.protocol}://{self.server}/api/accesstoken/login', data=data)
|
|
805
861
|
if response.status_code == requests.codes.ok:
|
|
@@ -811,7 +867,7 @@ class AuthenticatedAPI:
|
|
|
811
867
|
logger.error(str(response.content))
|
|
812
868
|
RuntimeError(response.status_code, "Could not generate valid manager approval token")
|
|
813
869
|
|
|
814
|
-
def __token__(self):
|
|
870
|
+
def __token__(self) -> str:
|
|
815
871
|
"""
|
|
816
872
|
Generate am API token to use to authenticate calls
|
|
817
873
|
:return: API Token
|
|
@@ -834,20 +890,23 @@ class AuthenticatedAPI:
|
|
|
834
890
|
if self.tenant is None:
|
|
835
891
|
self.tenant = response.json()['tenant']
|
|
836
892
|
if self.two_fa_secret_key:
|
|
893
|
+
logger.debug("Found Two Factor Token")
|
|
837
894
|
totp = pyotp.TOTP(self.two_fa_secret_key)
|
|
838
895
|
data = {'username': self.username,
|
|
839
896
|
'continuationToken': response.json()['continuationToken'],
|
|
840
897
|
'tenant': self.tenant, 'twoFactorToken': totp.now()}
|
|
898
|
+
|
|
899
|
+
header = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
841
900
|
response_2fa = self.session.post(
|
|
842
901
|
f'{self.protocol}://{self.server}/api/accesstoken/complete-2fa',
|
|
843
|
-
data=data)
|
|
902
|
+
data=data, headers=header)
|
|
844
903
|
if response_2fa.status_code == requests.codes.ok:
|
|
845
904
|
return response_2fa.json()['token']
|
|
846
905
|
else:
|
|
847
906
|
msg = "Failed to create a 2FA authentication token. Check your credentials are correct"
|
|
848
907
|
logger.error(msg)
|
|
849
|
-
logger.error(str(
|
|
850
|
-
raise RuntimeError(
|
|
908
|
+
logger.error(str(response_2fa.content))
|
|
909
|
+
raise RuntimeError(response_2fa.status_code, msg)
|
|
851
910
|
else:
|
|
852
911
|
msg = "2FA twoFactorToken required to authenticate against this account using 2FA"
|
|
853
912
|
logger.error(msg)
|
|
@@ -880,10 +939,10 @@ class AuthenticatedAPI:
|
|
|
880
939
|
|
|
881
940
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
882
941
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
883
|
-
protocol: str = "https", request_hook=None):
|
|
942
|
+
protocol: str = "https", request_hook=None, credentials_path: str = 'credentials.properties'):
|
|
884
943
|
|
|
885
944
|
config = configparser.ConfigParser(interpolation=configparser.Interpolation())
|
|
886
|
-
config.read(
|
|
945
|
+
config.read(os.path.relpath(credentials_path), encoding='utf-8')
|
|
887
946
|
self.session: Session = requests.Session()
|
|
888
947
|
|
|
889
948
|
if request_hook is not None:
|
|
@@ -976,3 +1035,15 @@ class AuthenticatedAPI:
|
|
|
976
1035
|
|
|
977
1036
|
logger.debug(self.xip_ns)
|
|
978
1037
|
logger.debug(self.entity_ns)
|
|
1038
|
+
|
|
1039
|
+
def parse_date_to_iso(date):
|
|
1040
|
+
try:
|
|
1041
|
+
date = datetime.datetime.fromisoformat(date.replace('Z','+0000'))
|
|
1042
|
+
if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
|
|
1043
|
+
date = date.replace(tzinfo=datetime.timezone.utc)
|
|
1044
|
+
date = date.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
|
|
1045
|
+
except ValueError:
|
|
1046
|
+
date = dateutil.parser.parse(date)
|
|
1047
|
+
if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
|
|
1048
|
+
date = date.replace(tzinfo=datetime.timezone.utc)
|
|
1049
|
+
date = date.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
|
pyPreservica/contentAPI.py
CHANGED
|
@@ -10,7 +10,8 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
-
from
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from typing import Generator, Callable, Optional, Union
|
|
14
15
|
from pyPreservica.common import *
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
@@ -34,12 +35,18 @@ class Field:
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class ContentAPI(AuthenticatedAPI):
|
|
38
|
+
"""
|
|
39
|
+
The ContentAPI class provides the search interface to the Preservica repository.
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
45
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
46
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
40
47
|
|
|
41
48
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
42
|
-
protocol, request_hook)
|
|
49
|
+
protocol, request_hook, credentials_path)
|
|
43
50
|
self.callback = None
|
|
44
51
|
|
|
45
52
|
class SearchResult:
|
|
@@ -89,6 +96,29 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
89
96
|
logger.error(f"object_details failed with error code: {request.status_code}")
|
|
90
97
|
raise RuntimeError(request.status_code, f"object_details failed with error code: {request.status_code}")
|
|
91
98
|
|
|
99
|
+
|
|
100
|
+
def download_bytes(self, reference):
|
|
101
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
102
|
+
params = {'id': f'sdb:IO|{reference}'}
|
|
103
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/download', params=params, headers=headers,
|
|
104
|
+
stream=True) as req:
|
|
105
|
+
if req.status_code == requests.codes.ok:
|
|
106
|
+
file_bytes = BytesIO()
|
|
107
|
+
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
108
|
+
file_bytes.write(chunk)
|
|
109
|
+
file_bytes.seek(0)
|
|
110
|
+
return file_bytes
|
|
111
|
+
elif req.status_code == requests.codes.unauthorized:
|
|
112
|
+
self.token = self.__token__()
|
|
113
|
+
return self.download_bytes(reference)
|
|
114
|
+
elif req.status_code == requests.codes.not_found:
|
|
115
|
+
logger.error(f"The requested asset reference is not found in the repository: {reference}")
|
|
116
|
+
raise RuntimeError(reference, "The requested reference is not found in the repository")
|
|
117
|
+
else:
|
|
118
|
+
logger.error(f"download failed with error code: {req.status_code}")
|
|
119
|
+
raise RuntimeError(req.status_code, f"download failed with error code: {req.status_code}")
|
|
120
|
+
|
|
121
|
+
|
|
92
122
|
def download(self, reference, filename):
|
|
93
123
|
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/octet-stream'}
|
|
94
124
|
params = {'id': f'sdb:IO|{reference}'}
|
|
@@ -111,6 +141,27 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
111
141
|
logger.error(f"download failed with error code: {req.status_code}")
|
|
112
142
|
raise RuntimeError(req.status_code, f"download failed with error code: {req.status_code}")
|
|
113
143
|
|
|
144
|
+
def thumbnail_bytes(self, entity_type, reference: str, size: Thumbnail = Thumbnail.LARGE) -> Union[BytesIO, None]:
|
|
145
|
+
headers = {HEADER_TOKEN: self.token, 'accept': 'image/png'}
|
|
146
|
+
params = {'id': f'sdb:{entity_type}|{reference}', 'size': f'{size.value}'}
|
|
147
|
+
with self.session.get(f'{self.protocol}://{self.server}/api/content/thumbnail', params=params, headers=headers, stream=True) as req:
|
|
148
|
+
if req.status_code == requests.codes.ok:
|
|
149
|
+
file_bytes = BytesIO()
|
|
150
|
+
for chunk in req.iter_content(chunk_size=CHUNK_SIZE):
|
|
151
|
+
file_bytes.write(chunk)
|
|
152
|
+
file_bytes.seek(0)
|
|
153
|
+
return file_bytes
|
|
154
|
+
elif req.status_code == requests.codes.unauthorized:
|
|
155
|
+
self.token = self.__token__()
|
|
156
|
+
return self.thumbnail_bytes(entity_type, reference, size)
|
|
157
|
+
elif req.status_code == requests.codes.not_found:
|
|
158
|
+
logger.error(req.content.decode("utf-8"))
|
|
159
|
+
logger.error(f"The requested reference is not found in the repository: {reference}")
|
|
160
|
+
raise RuntimeError(reference, "The requested reference is not found in the repository")
|
|
161
|
+
else:
|
|
162
|
+
logger.error(f"thumbnail failed with error code: {req.status_code}")
|
|
163
|
+
raise RuntimeError(req.status_code, f"thumbnail failed with error code: {req.status_code}")
|
|
164
|
+
|
|
114
165
|
def thumbnail(self, entity_type, reference, filename, size=Thumbnail.LARGE):
|
|
115
166
|
headers = {HEADER_TOKEN: self.token, 'accept': 'image/png'}
|
|
116
167
|
params = {'id': f'sdb:{entity_type}|{reference}', 'size': f'{size.value}'}
|
|
@@ -315,7 +366,7 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
315
366
|
return search_results
|
|
316
367
|
elif results.status_code == requests.codes.unauthorized:
|
|
317
368
|
self.token = self.__token__()
|
|
318
|
-
return self.
|
|
369
|
+
return self._search_fields(query, fields, start_index, page_size)
|
|
319
370
|
else:
|
|
320
371
|
logger.error(f"search failed with error code: {results.status_code}")
|
|
321
372
|
raise RuntimeError(results.status_code, f"search_index_filter failed")
|