pyPreservica 2.7.2__py3-none-any.whl → 3.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyPreservica/__init__.py +18 -6
- pyPreservica/adminAPI.py +29 -22
- pyPreservica/authorityAPI.py +6 -7
- pyPreservica/common.py +116 -19
- pyPreservica/contentAPI.py +179 -8
- pyPreservica/entityAPI.py +730 -214
- pyPreservica/mdformsAPI.py +501 -29
- pyPreservica/monitorAPI.py +2 -2
- pyPreservica/parAPI.py +1 -37
- pyPreservica/retentionAPI.py +58 -26
- pyPreservica/settingsAPI.py +295 -0
- pyPreservica/uploadAPI.py +298 -480
- pyPreservica/webHooksAPI.py +42 -1
- pyPreservica/workflowAPI.py +17 -13
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/METADATA +20 -9
- pypreservica-3.3.4.dist-info/RECORD +20 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/WHEEL +1 -1
- pyPreservica/vocabularyAPI.py +0 -141
- pyPreservica-2.7.2.dist-info/RECORD +0 -20
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info/licenses}/LICENSE.txt +0 -0
- {pyPreservica-2.7.2.dist-info → pypreservica-3.3.4.dist-info}/top_level.txt +0 -0
pyPreservica/__init__.py
CHANGED
|
@@ -6,23 +6,35 @@ author: James Carr
|
|
|
6
6
|
licence: Apache License 2.0
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from .common import *
|
|
10
|
-
from .contentAPI import ContentAPI
|
|
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
|
|
17
28
|
from .adminAPI import AdminAPI
|
|
18
29
|
from .monitorAPI import MonitorAPI, MonitorCategory, MonitorStatus, MessageStatus
|
|
19
|
-
from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler
|
|
30
|
+
from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler, FlaskWebhookHandler
|
|
20
31
|
from .authorityAPI import AuthorityAPI, Table
|
|
21
|
-
from .mdformsAPI import
|
|
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.4"
|
|
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
|
|
|
@@ -79,9 +82,9 @@ class FileHash:
|
|
|
79
82
|
|
|
80
83
|
def identifiersToDict(identifiers: set) -> dict:
|
|
81
84
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
Convert a set of tuples to a dict
|
|
86
|
+
:param identifiers:
|
|
87
|
+
:return:
|
|
85
88
|
"""
|
|
86
89
|
result = {}
|
|
87
90
|
for identifier_tuple in identifiers:
|
|
@@ -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
|
|
@@ -477,10 +503,10 @@ class Entity:
|
|
|
477
503
|
def __repr__(self):
|
|
478
504
|
return self.__str__()
|
|
479
505
|
|
|
480
|
-
def has_metadata(self):
|
|
506
|
+
def has_metadata(self) -> bool:
|
|
481
507
|
return bool(self.metadata)
|
|
482
508
|
|
|
483
|
-
def metadata_namespaces(self):
|
|
509
|
+
def metadata_namespaces(self) -> list:
|
|
484
510
|
return list(self.metadata.values())
|
|
485
511
|
|
|
486
512
|
|
|
@@ -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
|
"""
|
|
@@ -714,11 +764,33 @@ class AuthenticatedAPI:
|
|
|
714
764
|
|
|
715
765
|
return entity_dict
|
|
716
766
|
|
|
767
|
+
def edition(self) -> str:
|
|
768
|
+
"""
|
|
769
|
+
Return the edition of this tenancy
|
|
770
|
+
"""
|
|
771
|
+
if self.major_version < 8 and self.minor_version < 3:
|
|
772
|
+
raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
|
|
773
|
+
|
|
774
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
775
|
+
|
|
776
|
+
response = self.session.get(f'{self.protocol}://{self.server}/api/entitlement/edition', headers=headers)
|
|
777
|
+
|
|
778
|
+
if response.status_code == requests.codes.ok:
|
|
779
|
+
return response.json()['edition']
|
|
780
|
+
elif response.status_code == requests.codes.unauthorized:
|
|
781
|
+
self.token = self.__token__()
|
|
782
|
+
return self.edition()
|
|
783
|
+
else:
|
|
784
|
+
exception = HTTPException("", response.status_code, response.url,
|
|
785
|
+
"edition", response.content.decode('utf-8'))
|
|
786
|
+
logger.error(exception)
|
|
787
|
+
raise exception
|
|
788
|
+
|
|
717
789
|
def __version_namespace__(self):
|
|
718
790
|
"""
|
|
719
791
|
Generate version specific namespaces from the server version
|
|
720
792
|
"""
|
|
721
|
-
if self.major_version
|
|
793
|
+
if self.major_version > 6:
|
|
722
794
|
self.xip_ns = f"{NS_XIP_ROOT}v{self.major_version}.{self.minor_version}"
|
|
723
795
|
self.entity_ns = f"{NS_ENTITY_ROOT}v{self.major_version}.{self.minor_version}"
|
|
724
796
|
self.rm_ns = f"{NS_RM_ROOT}v{6}.{2}"
|
|
@@ -736,6 +808,8 @@ class AuthenticatedAPI:
|
|
|
736
808
|
self.sec_ns = f"{NS_SEC_ROOT}/v{self.major_version}.{self.minor_version}"
|
|
737
809
|
self.admin_ns = f"{NS_ADMIN}/v{self.major_version}.{self.minor_version}"
|
|
738
810
|
|
|
811
|
+
xml.etree.ElementTree.register_namespace("xip", f"{self.xip_ns}")
|
|
812
|
+
|
|
739
813
|
def __version_number__(self):
|
|
740
814
|
"""
|
|
741
815
|
Determine the version number of the server
|
|
@@ -750,6 +824,7 @@ class AuthenticatedAPI:
|
|
|
750
824
|
self.major_version = int(version_numbers[0])
|
|
751
825
|
self.minor_version = int(version_numbers[1])
|
|
752
826
|
self.patch_version = int(version_numbers[2])
|
|
827
|
+
|
|
753
828
|
return version
|
|
754
829
|
elif request.status_code == requests.codes.unauthorized:
|
|
755
830
|
self.token = self.__token__()
|
|
@@ -758,9 +833,12 @@ class AuthenticatedAPI:
|
|
|
758
833
|
logger.error(f"version number failed with http response {request.status_code}")
|
|
759
834
|
logger.error(str(request.content))
|
|
760
835
|
RuntimeError(request.status_code, "version number failed")
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
|
|
761
839
|
|
|
762
840
|
def __str__(self):
|
|
763
|
-
return f"pyPreservica version: {pyPreservica.__version__} (Preservica
|
|
841
|
+
return f"pyPreservica version: {pyPreservica.__version__} (Preservica 8.0 Compatible) " \
|
|
764
842
|
f"Connected to: {self.server} Preservica version: {self.version} as {self.username} " \
|
|
765
843
|
f"in tenancy {self.tenant}"
|
|
766
844
|
|
|
@@ -777,7 +855,7 @@ class AuthenticatedAPI:
|
|
|
777
855
|
with open('credentials.properties', 'wt', encoding="utf-8") as configfile:
|
|
778
856
|
config.write(configfile)
|
|
779
857
|
|
|
780
|
-
def manager_token(self, username: str, password: str):
|
|
858
|
+
def manager_token(self, username: str, password: str) -> str:
|
|
781
859
|
data = {'username': username, 'password': password, 'tenant': self.tenant}
|
|
782
860
|
response = self.session.post(f'{self.protocol}://{self.server}/api/accesstoken/login', data=data)
|
|
783
861
|
if response.status_code == requests.codes.ok:
|
|
@@ -789,7 +867,7 @@ class AuthenticatedAPI:
|
|
|
789
867
|
logger.error(str(response.content))
|
|
790
868
|
RuntimeError(response.status_code, "Could not generate valid manager approval token")
|
|
791
869
|
|
|
792
|
-
def __token__(self):
|
|
870
|
+
def __token__(self) -> str:
|
|
793
871
|
"""
|
|
794
872
|
Generate am API token to use to authenticate calls
|
|
795
873
|
:return: API Token
|
|
@@ -812,20 +890,23 @@ class AuthenticatedAPI:
|
|
|
812
890
|
if self.tenant is None:
|
|
813
891
|
self.tenant = response.json()['tenant']
|
|
814
892
|
if self.two_fa_secret_key:
|
|
893
|
+
logger.debug("Found Two Factor Token")
|
|
815
894
|
totp = pyotp.TOTP(self.two_fa_secret_key)
|
|
816
895
|
data = {'username': self.username,
|
|
817
896
|
'continuationToken': response.json()['continuationToken'],
|
|
818
897
|
'tenant': self.tenant, 'twoFactorToken': totp.now()}
|
|
898
|
+
|
|
899
|
+
header = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
819
900
|
response_2fa = self.session.post(
|
|
820
901
|
f'{self.protocol}://{self.server}/api/accesstoken/complete-2fa',
|
|
821
|
-
data=data)
|
|
902
|
+
data=data, headers=header)
|
|
822
903
|
if response_2fa.status_code == requests.codes.ok:
|
|
823
904
|
return response_2fa.json()['token']
|
|
824
905
|
else:
|
|
825
906
|
msg = "Failed to create a 2FA authentication token. Check your credentials are correct"
|
|
826
907
|
logger.error(msg)
|
|
827
|
-
logger.error(str(
|
|
828
|
-
raise RuntimeError(
|
|
908
|
+
logger.error(str(response_2fa.content))
|
|
909
|
+
raise RuntimeError(response_2fa.status_code, msg)
|
|
829
910
|
else:
|
|
830
911
|
msg = "2FA twoFactorToken required to authenticate against this account using 2FA"
|
|
831
912
|
logger.error(msg)
|
|
@@ -857,12 +938,16 @@ class AuthenticatedAPI:
|
|
|
857
938
|
raise RuntimeError(response.status_code, msg)
|
|
858
939
|
|
|
859
940
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
860
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
941
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
942
|
+
protocol: str = "https", request_hook=None, credentials_path: str = 'credentials.properties'):
|
|
861
943
|
|
|
862
944
|
config = configparser.ConfigParser(interpolation=configparser.Interpolation())
|
|
863
|
-
config.read(
|
|
945
|
+
config.read(os.path.relpath(credentials_path), encoding='utf-8')
|
|
864
946
|
self.session: Session = requests.Session()
|
|
865
947
|
|
|
948
|
+
if request_hook is not None:
|
|
949
|
+
self.session.hooks['response'].append(request_hook)
|
|
950
|
+
|
|
866
951
|
retries = Retry(
|
|
867
952
|
total=3,
|
|
868
953
|
backoff_factor=0.1,
|
|
@@ -950,3 +1035,15 @@ class AuthenticatedAPI:
|
|
|
950
1035
|
|
|
951
1036
|
logger.debug(self.xip_ns)
|
|
952
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')
|