pyPreservica 2.7.2__py3-none-any.whl → 2.7.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 +1 -1
- pyPreservica/common.py +30 -4
- pyPreservica/contentAPI.py +9 -5
- pyPreservica/entityAPI.py +7 -3
- pyPreservica/mdformsAPI.py +7 -2
- pyPreservica/retentionAPI.py +5 -3
- pyPreservica/uploadAPI.py +151 -98
- pyPreservica/workflowAPI.py +11 -7
- {pyPreservica-2.7.2.dist-info → pyPreservica-2.7.4.dist-info}/METADATA +1 -1
- pyPreservica-2.7.4.dist-info/RECORD +19 -0
- {pyPreservica-2.7.2.dist-info → pyPreservica-2.7.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-2.7.4.dist-info}/LICENSE.txt +0 -0
- {pyPreservica-2.7.2.dist-info → pyPreservica-2.7.4.dist-info}/top_level.txt +0 -0
pyPreservica/__init__.py
CHANGED
pyPreservica/common.py
CHANGED
|
@@ -477,10 +477,10 @@ class Entity:
|
|
|
477
477
|
def __repr__(self):
|
|
478
478
|
return self.__str__()
|
|
479
479
|
|
|
480
|
-
def has_metadata(self):
|
|
480
|
+
def has_metadata(self) -> bool:
|
|
481
481
|
return bool(self.metadata)
|
|
482
482
|
|
|
483
|
-
def metadata_namespaces(self):
|
|
483
|
+
def metadata_namespaces(self) -> list:
|
|
484
484
|
return list(self.metadata.values())
|
|
485
485
|
|
|
486
486
|
|
|
@@ -714,11 +714,33 @@ class AuthenticatedAPI:
|
|
|
714
714
|
|
|
715
715
|
return entity_dict
|
|
716
716
|
|
|
717
|
+
def edition(self) -> str:
|
|
718
|
+
"""
|
|
719
|
+
Return the edition of this tenancy
|
|
720
|
+
"""
|
|
721
|
+
if self.major_version < 8 and self.minor_version < 3:
|
|
722
|
+
raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
|
|
723
|
+
|
|
724
|
+
headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
|
|
725
|
+
|
|
726
|
+
response = self.session.get(f'{self.protocol}://{self.server}/api/entitlement/edition', headers=headers)
|
|
727
|
+
|
|
728
|
+
if response.status_code == requests.codes.ok:
|
|
729
|
+
return response.json()['edition']
|
|
730
|
+
elif response.status_code == requests.codes.unauthorized:
|
|
731
|
+
self.token = self.__token__()
|
|
732
|
+
return self.edition()
|
|
733
|
+
else:
|
|
734
|
+
exception = HTTPException("", response.status_code, response.url,
|
|
735
|
+
"edition", response.content.decode('utf-8'))
|
|
736
|
+
logger.error(exception)
|
|
737
|
+
raise exception
|
|
738
|
+
|
|
717
739
|
def __version_namespace__(self):
|
|
718
740
|
"""
|
|
719
741
|
Generate version specific namespaces from the server version
|
|
720
742
|
"""
|
|
721
|
-
if self.major_version
|
|
743
|
+
if self.major_version > 6:
|
|
722
744
|
self.xip_ns = f"{NS_XIP_ROOT}v{self.major_version}.{self.minor_version}"
|
|
723
745
|
self.entity_ns = f"{NS_ENTITY_ROOT}v{self.major_version}.{self.minor_version}"
|
|
724
746
|
self.rm_ns = f"{NS_RM_ROOT}v{6}.{2}"
|
|
@@ -857,12 +879,16 @@ class AuthenticatedAPI:
|
|
|
857
879
|
raise RuntimeError(response.status_code, msg)
|
|
858
880
|
|
|
859
881
|
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,
|
|
882
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
883
|
+
protocol: str = "https", request_hook=None):
|
|
861
884
|
|
|
862
885
|
config = configparser.ConfigParser(interpolation=configparser.Interpolation())
|
|
863
886
|
config.read('credentials.properties', encoding='utf-8')
|
|
864
887
|
self.session: Session = requests.Session()
|
|
865
888
|
|
|
889
|
+
if request_hook is not None:
|
|
890
|
+
self.session.hooks['response'].append(request_hook)
|
|
891
|
+
|
|
866
892
|
retries = Retry(
|
|
867
893
|
total=3,
|
|
868
894
|
backoff_factor=0.1,
|
pyPreservica/contentAPI.py
CHANGED
|
@@ -10,7 +10,7 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
-
from typing import Generator
|
|
13
|
+
from typing import Generator, Callable
|
|
14
14
|
from pyPreservica.common import *
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
@@ -19,8 +19,10 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
class ContentAPI(AuthenticatedAPI):
|
|
20
20
|
|
|
21
21
|
def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False,
|
|
22
|
-
two_fa_secret_key: str = None, protocol: str = "https"):
|
|
23
|
-
|
|
22
|
+
two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
|
|
23
|
+
|
|
24
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
25
|
+
protocol, request_hook)
|
|
24
26
|
self.callback = None
|
|
25
27
|
|
|
26
28
|
class SearchResult:
|
|
@@ -130,7 +132,8 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
130
132
|
logger.error(f"indexed_fields failed with error code: {results.status_code}")
|
|
131
133
|
raise RuntimeError(results.status_code, f"indexed_fields failed with error code: {results.status_code}")
|
|
132
134
|
|
|
133
|
-
def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
|
|
135
|
+
def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
|
|
136
|
+
list_indexes: list = None):
|
|
134
137
|
if list_indexes is None or len(list_indexes) == 0:
|
|
135
138
|
metadata_fields = ["xip.reference", "xip.title", "xip.description", "xip.document_type",
|
|
136
139
|
"xip.parent_ref", "xip.security_descriptor"]
|
|
@@ -193,7 +196,8 @@ class ContentAPI(AuthenticatedAPI):
|
|
|
193
196
|
logger.error(f"search failed with error code: {results.status_code}")
|
|
194
197
|
raise RuntimeError(results.status_code, f"simple_search failed with error code: {results.status_code}")
|
|
195
198
|
|
|
196
|
-
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
|
|
199
|
+
def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
|
|
200
|
+
filter_values: dict = None,
|
|
197
201
|
sort_values: dict = None):
|
|
198
202
|
if filter_values is None:
|
|
199
203
|
filter_values = {}
|
pyPreservica/entityAPI.py
CHANGED
|
@@ -15,7 +15,7 @@ import xml.etree.ElementTree
|
|
|
15
15
|
from datetime import datetime, timedelta, timezone
|
|
16
16
|
from io import BytesIO
|
|
17
17
|
from time import sleep
|
|
18
|
-
from typing import Any, Generator, Tuple, Iterable, Union
|
|
18
|
+
from typing import Any, Generator, Tuple, Iterable, Union, Callable
|
|
19
19
|
|
|
20
20
|
from pyPreservica.common import *
|
|
21
21
|
|
|
@@ -34,8 +34,12 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
37
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
38
|
-
|
|
37
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
38
|
+
protocol: str = "https", request_hook: Callable = None):
|
|
39
|
+
|
|
40
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
41
|
+
protocol, request_hook)
|
|
42
|
+
|
|
39
43
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
40
44
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
41
45
|
|
pyPreservica/mdformsAPI.py
CHANGED
|
@@ -10,14 +10,19 @@ licence: Apache License 2.0
|
|
|
10
10
|
"""
|
|
11
11
|
import json
|
|
12
12
|
import xml.etree.ElementTree
|
|
13
|
+
from typing import Callable
|
|
13
14
|
|
|
14
15
|
from pyPreservica.common import *
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class MDFormsAPI(AuthenticatedAPI):
|
|
18
19
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
19
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
20
|
-
|
|
20
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
21
|
+
protocol: str = "https", request_hook: Callable = None):
|
|
22
|
+
|
|
23
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
24
|
+
protocol, request_hook)
|
|
25
|
+
|
|
21
26
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
22
27
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
23
28
|
|
pyPreservica/retentionAPI.py
CHANGED
|
@@ -11,7 +11,7 @@ licence: Apache License 2.0
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
import xml.etree.ElementTree
|
|
14
|
-
from typing import Set
|
|
14
|
+
from typing import Set, Callable
|
|
15
15
|
|
|
16
16
|
from pyPreservica.common import *
|
|
17
17
|
|
|
@@ -59,8 +59,10 @@ class RetentionPolicy:
|
|
|
59
59
|
class RetentionAPI(AuthenticatedAPI):
|
|
60
60
|
|
|
61
61
|
def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False,
|
|
62
|
-
two_fa_secret_key: str = None, protocol: str = "https"):
|
|
63
|
-
|
|
62
|
+
two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
|
|
63
|
+
|
|
64
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
65
|
+
protocol, request_hook)
|
|
64
66
|
if self.major_version < 7 and self.minor_version < 2:
|
|
65
67
|
raise RuntimeError("Retention API is only available when connected to a v6.2 System")
|
|
66
68
|
|
pyPreservica/uploadAPI.py
CHANGED
|
@@ -80,7 +80,8 @@ class PutObjectTask(s3transfer.tasks.Task):
|
|
|
80
80
|
class CompleteMultipartUploadTask(s3transfer.tasks.Task):
|
|
81
81
|
# Copied from s3transfer/tasks.py, changed to return a result.
|
|
82
82
|
def _main(self, client, bucket, key, upload_id, parts, extra_args):
|
|
83
|
-
return client.complete_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id,
|
|
83
|
+
return client.complete_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id,
|
|
84
|
+
MultipartUpload={"Parts": parts},
|
|
84
85
|
**extra_args, )
|
|
85
86
|
|
|
86
87
|
|
|
@@ -99,8 +100,8 @@ def prettify(elem):
|
|
|
99
100
|
def __create_io__(xip=None, file_name=None, parent_folder=None, **kwargs):
|
|
100
101
|
if xip is None:
|
|
101
102
|
xip = Element('xip:XIP')
|
|
103
|
+
xip.set('xmlns:xip', 'http://preservica.com/XIP/v6.0')
|
|
102
104
|
assert xip is not None
|
|
103
|
-
xip.set('xmlns:xip', 'http://preservica.com/XIP/v6.0')
|
|
104
105
|
io = SubElement(xip, 'xip:InformationObject')
|
|
105
106
|
ref = SubElement(io, 'xip:Ref')
|
|
106
107
|
|
|
@@ -224,7 +225,8 @@ def __make_representation_multiple_co__(xip, rep_name, rep_type, rep_files, io_r
|
|
|
224
225
|
return refs_dict
|
|
225
226
|
|
|
226
227
|
|
|
227
|
-
def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
|
|
228
|
+
def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
|
|
229
|
+
additional_namespaces=None):
|
|
228
230
|
"""
|
|
229
231
|
Create a custom CMIS transform to display metadata within UA.
|
|
230
232
|
|
|
@@ -242,7 +244,8 @@ def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Titl
|
|
|
242
244
|
|
|
243
245
|
namespaces = {"version": "2.0", "xmlns:xsl": "http://www.w3.org/1999/XSL/Transform",
|
|
244
246
|
"xmlns:fn": "http://www.w3.org/2005/xpath-functions", "xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
245
|
-
"xmlns:csv": xml_namespace, "xmlns": "http://www.tessella.com/sdb/cmis/metadata",
|
|
247
|
+
"xmlns:csv": xml_namespace, "xmlns": "http://www.tessella.com/sdb/cmis/metadata",
|
|
248
|
+
"exclude-result-prefixes": "csv"}
|
|
246
249
|
|
|
247
250
|
if additional_namespaces is not None:
|
|
248
251
|
for prefix, uri in additional_namespaces.items():
|
|
@@ -311,7 +314,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
|
|
|
311
314
|
headers.add(xml_tag)
|
|
312
315
|
break
|
|
313
316
|
|
|
314
|
-
namespaces = {"xmlns:xs": "http://www.w3.org/2001/XMLSchema", "attributeFormDefault": "unqualified",
|
|
317
|
+
namespaces = {"xmlns:xs": "http://www.w3.org/2001/XMLSchema", "attributeFormDefault": "unqualified",
|
|
318
|
+
"elementFormDefault": "qualified",
|
|
315
319
|
"targetNamespace": xml_namespace}
|
|
316
320
|
|
|
317
321
|
if additional_namespaces is not None:
|
|
@@ -333,7 +337,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
|
|
|
333
337
|
prefix, sep, tag = header.partition(":")
|
|
334
338
|
try:
|
|
335
339
|
namespace = additional_namespaces[prefix]
|
|
336
|
-
xml.etree.ElementTree.SubElement(xml_sequence, "xs:element",
|
|
340
|
+
xml.etree.ElementTree.SubElement(xml_sequence, "xs:element",
|
|
341
|
+
{"ref": header, "xmlns:" + prefix: namespace})
|
|
337
342
|
except KeyError:
|
|
338
343
|
xml.etree.ElementTree.SubElement(xml_sequence, "xs:element", {"type": "xs:string", "name": header})
|
|
339
344
|
else:
|
|
@@ -350,7 +355,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
|
|
|
350
355
|
return xsd_file
|
|
351
356
|
|
|
352
357
|
|
|
353
|
-
def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
|
|
358
|
+
def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
|
|
359
|
+
additional_namespaces=None):
|
|
354
360
|
"""
|
|
355
361
|
Create a custom Preservica search index based on the columns in a csv file
|
|
356
362
|
|
|
@@ -400,7 +406,8 @@ def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Tit
|
|
|
400
406
|
return search_xml
|
|
401
407
|
|
|
402
408
|
|
|
403
|
-
def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename", export_folder=None,
|
|
409
|
+
def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename", export_folder=None,
|
|
410
|
+
additional_namespaces=None):
|
|
404
411
|
"""
|
|
405
412
|
Export the rows of a CSV file as XML metadata documents which can be added to Preservica assets
|
|
406
413
|
|
|
@@ -451,7 +458,8 @@ def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename
|
|
|
451
458
|
yield name
|
|
452
459
|
|
|
453
460
|
|
|
454
|
-
def generic_asset_package(preservation_files_dict=None, access_files_dict=None, export_folder=None, parent_folder=None,
|
|
461
|
+
def generic_asset_package(preservation_files_dict=None, access_files_dict=None, export_folder=None, parent_folder=None,
|
|
462
|
+
compress=True,
|
|
455
463
|
**kwargs):
|
|
456
464
|
# some basic validation
|
|
457
465
|
if export_folder is None:
|
|
@@ -501,8 +509,10 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
|
|
|
501
509
|
if has_preservation_files:
|
|
502
510
|
for representation_name in preservation_files_dict.keys():
|
|
503
511
|
preservation_files_list = preservation_files_dict[representation_name]
|
|
504
|
-
preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
|
|
505
|
-
|
|
512
|
+
preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
|
|
513
|
+
rep_type="Preservation",
|
|
514
|
+
rep_files=preservation_files_list,
|
|
515
|
+
io_ref=io_ref)
|
|
506
516
|
preservation_representation_refs_dict[representation_name] = preservation_refs_dict
|
|
507
517
|
|
|
508
518
|
if has_access_files:
|
|
@@ -519,13 +529,16 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
|
|
|
519
529
|
default_content_objects_title = os.path.splitext(os.path.basename(filename))[0]
|
|
520
530
|
|
|
521
531
|
preservation_content_title = kwargs.get('Preservation_Content_Title', default_content_objects_title)
|
|
522
|
-
preservation_content_description = kwargs.get('Preservation_Content_Description',
|
|
532
|
+
preservation_content_description = kwargs.get('Preservation_Content_Description',
|
|
533
|
+
default_content_objects_title)
|
|
523
534
|
|
|
524
535
|
if isinstance(preservation_content_title, dict):
|
|
525
|
-
preservation_content_title = preservation_content_title.get("filename",
|
|
536
|
+
preservation_content_title = preservation_content_title.get("filename",
|
|
537
|
+
default_content_objects_title)
|
|
526
538
|
|
|
527
539
|
if isinstance(preservation_content_description, dict):
|
|
528
|
-
preservation_content_description = preservation_content_description.get("filename",
|
|
540
|
+
preservation_content_description = preservation_content_description.get("filename",
|
|
541
|
+
default_content_objects_title)
|
|
529
542
|
|
|
530
543
|
__make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag,
|
|
531
544
|
preservation_content_description, content_type)
|
|
@@ -545,7 +558,8 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
|
|
|
545
558
|
if isinstance(access_content_description, dict):
|
|
546
559
|
access_content_description = access_content_title.get("filename", default_content_objects_title)
|
|
547
560
|
|
|
548
|
-
__make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
|
|
561
|
+
__make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
|
|
562
|
+
access_content_description,
|
|
549
563
|
content_type)
|
|
550
564
|
|
|
551
565
|
if has_preservation_files:
|
|
@@ -598,12 +612,12 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
|
|
|
598
612
|
for identifier_key, identifier_value in identifier_map.items():
|
|
599
613
|
if identifier_key:
|
|
600
614
|
if identifier_value:
|
|
601
|
-
identifier = SubElement(xip, 'Identifier')
|
|
602
|
-
id_type = SubElement(identifier, "Type")
|
|
615
|
+
identifier = SubElement(xip, 'xip:Identifier')
|
|
616
|
+
id_type = SubElement(identifier, "xip:Type")
|
|
603
617
|
id_type.text = identifier_key
|
|
604
|
-
id_value = SubElement(identifier, "Value")
|
|
618
|
+
id_value = SubElement(identifier, "xip:Value")
|
|
605
619
|
id_value.text = identifier_value
|
|
606
|
-
id_io = SubElement(identifier, "Entity")
|
|
620
|
+
id_io = SubElement(identifier, "xip:Entity")
|
|
607
621
|
id_io.text = io_ref
|
|
608
622
|
|
|
609
623
|
if 'Asset_Metadata' in kwargs:
|
|
@@ -613,22 +627,22 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
|
|
|
613
627
|
if metadata_path:
|
|
614
628
|
if os.path.exists(metadata_path) and os.path.isfile(metadata_path):
|
|
615
629
|
descriptive_metadata = xml.etree.ElementTree.parse(source=metadata_path)
|
|
616
|
-
metadata = SubElement(xip, 'Metadata', {'schemaUri': metadata_ns})
|
|
617
|
-
metadata_ref = SubElement(metadata, 'Ref')
|
|
630
|
+
metadata = SubElement(xip, 'xip:Metadata', {'schemaUri': metadata_ns})
|
|
631
|
+
metadata_ref = SubElement(metadata, 'xip:Ref')
|
|
618
632
|
metadata_ref.text = str(uuid.uuid4())
|
|
619
|
-
entity = SubElement(metadata, 'Entity')
|
|
633
|
+
entity = SubElement(metadata, 'xip:Entity')
|
|
620
634
|
entity.text = io_ref
|
|
621
|
-
content = SubElement(metadata, 'Content')
|
|
635
|
+
content = SubElement(metadata, 'xip:Content')
|
|
622
636
|
content.append(descriptive_metadata.getroot())
|
|
623
637
|
elif isinstance(metadata_path, str):
|
|
624
638
|
try:
|
|
625
639
|
descriptive_metadata = xml.etree.ElementTree.fromstring(metadata_path)
|
|
626
|
-
metadata = SubElement(xip, 'Metadata', {'schemaUri': metadata_ns})
|
|
627
|
-
metadata_ref = SubElement(metadata, 'Ref')
|
|
640
|
+
metadata = SubElement(xip, 'xip:Metadata', {'schemaUri': metadata_ns})
|
|
641
|
+
metadata_ref = SubElement(metadata, 'xip:Ref')
|
|
628
642
|
metadata_ref.text = str(uuid.uuid4())
|
|
629
|
-
entity = SubElement(metadata, 'Entity')
|
|
643
|
+
entity = SubElement(metadata, 'xip:Entity')
|
|
630
644
|
entity.text = io_ref
|
|
631
|
-
content = SubElement(metadata, 'Content')
|
|
645
|
+
content = SubElement(metadata, 'xip:Content')
|
|
632
646
|
content.append(descriptive_metadata)
|
|
633
647
|
except RuntimeError:
|
|
634
648
|
logging.info(f"Could not parse asset metadata in namespace {metadata_ns}")
|
|
@@ -712,71 +726,72 @@ def multi_asset_package(asset_file_list=None, export_folder=None, parent_folder=
|
|
|
712
726
|
os.mkdir(os.path.join(inner_folder, CONTENT_FOLDER))
|
|
713
727
|
|
|
714
728
|
asset_map = dict()
|
|
715
|
-
xip = Element('XIP')
|
|
729
|
+
xip = Element('xip:XIP')
|
|
730
|
+
xip.set('xmlns:xip', 'http://preservica.com/XIP/v6.0')
|
|
716
731
|
for file in asset_file_list:
|
|
717
732
|
default_asset_title = os.path.splitext(os.path.basename(file))[0]
|
|
718
733
|
xip, io_ref = __create_io__(xip, file_name=default_asset_title, parent_folder=parent_folder, **kwargs)
|
|
719
734
|
asset_map[file] = io_ref
|
|
720
|
-
representation = SubElement(xip, 'Representation')
|
|
721
|
-
io_link = SubElement(representation, 'InformationObject')
|
|
735
|
+
representation = SubElement(xip, 'xip:Representation')
|
|
736
|
+
io_link = SubElement(representation, 'xip:InformationObject')
|
|
722
737
|
io_link.text = io_ref
|
|
723
|
-
access_name = SubElement(representation, 'Name')
|
|
738
|
+
access_name = SubElement(representation, 'xip:Name')
|
|
724
739
|
access_name.text = "Preservation"
|
|
725
|
-
access_type = SubElement(representation, 'Type')
|
|
740
|
+
access_type = SubElement(representation, 'xip:Type')
|
|
726
741
|
access_type.text = "Preservation"
|
|
727
|
-
content_objects = SubElement(representation, 'ContentObjects')
|
|
728
|
-
content_object = SubElement(content_objects, 'ContentObject')
|
|
742
|
+
content_objects = SubElement(representation, 'xip:ContentObjects')
|
|
743
|
+
content_object = SubElement(content_objects, 'xip:ContentObject')
|
|
729
744
|
content_object_ref = str(uuid.uuid4())
|
|
730
745
|
content_object.text = content_object_ref
|
|
731
746
|
|
|
732
747
|
default_content_objects_title = os.path.splitext(os.path.basename(file))[0]
|
|
733
|
-
content_object = SubElement(xip, 'ContentObject')
|
|
734
|
-
ref_element = SubElement(content_object, "Ref")
|
|
748
|
+
content_object = SubElement(xip, 'xip:ContentObject')
|
|
749
|
+
ref_element = SubElement(content_object, "xip:Ref")
|
|
735
750
|
ref_element.text = content_object_ref
|
|
736
|
-
title = SubElement(content_object, "Title")
|
|
751
|
+
title = SubElement(content_object, "xip:Title")
|
|
737
752
|
title.text = default_content_objects_title
|
|
738
|
-
description = SubElement(content_object, "Description")
|
|
753
|
+
description = SubElement(content_object, "xip:Description")
|
|
739
754
|
description.text = default_content_objects_title
|
|
740
|
-
security_tag_element = SubElement(content_object, "SecurityTag")
|
|
755
|
+
security_tag_element = SubElement(content_object, "xip:SecurityTag")
|
|
741
756
|
security_tag_element.text = security_tag
|
|
742
|
-
custom_type = SubElement(content_object, "CustomType")
|
|
757
|
+
custom_type = SubElement(content_object, "xip:CustomType")
|
|
743
758
|
custom_type.text = content_type
|
|
744
|
-
parent = SubElement(content_object, "Parent")
|
|
759
|
+
parent = SubElement(content_object, "xip:Parent")
|
|
745
760
|
parent.text = io_ref
|
|
746
761
|
|
|
747
|
-
generation = SubElement(xip, 'Generation', {"original": "true", "active": "true"})
|
|
748
|
-
content_object = SubElement(generation, "ContentObject")
|
|
762
|
+
generation = SubElement(xip, 'xip:Generation', {"original": "true", "active": "true"})
|
|
763
|
+
content_object = SubElement(generation, "xip:ContentObject")
|
|
749
764
|
content_object.text = content_object_ref
|
|
750
|
-
label = SubElement(generation, "Label")
|
|
765
|
+
label = SubElement(generation, "xip:Label")
|
|
751
766
|
label.text = os.path.splitext(os.path.basename(file))[0]
|
|
752
|
-
effective_date = SubElement(generation, "EffectiveDate")
|
|
767
|
+
effective_date = SubElement(generation, "xip:EffectiveDate")
|
|
753
768
|
effective_date.text = datetime.now().isoformat()
|
|
754
|
-
bitstreams = SubElement(generation, "Bitstreams")
|
|
755
|
-
bitstream = SubElement(bitstreams, "Bitstream")
|
|
769
|
+
bitstreams = SubElement(generation, "xip:Bitstreams")
|
|
770
|
+
bitstream = SubElement(bitstreams, "xip:Bitstream")
|
|
756
771
|
bitstream.text = os.path.basename(file)
|
|
757
|
-
SubElement(generation, "Formats")
|
|
758
|
-
SubElement(generation, "Properties")
|
|
772
|
+
SubElement(generation, "xip:Formats")
|
|
773
|
+
SubElement(generation, "xip:Properties")
|
|
759
774
|
|
|
760
|
-
bitstream = SubElement(xip, 'Bitstream')
|
|
761
|
-
filename_element = SubElement(bitstream, "Filename")
|
|
775
|
+
bitstream = SubElement(xip, 'xip:Bitstream')
|
|
776
|
+
filename_element = SubElement(bitstream, "xip:Filename")
|
|
762
777
|
filename_element.text = os.path.basename(file)
|
|
763
|
-
filesize = SubElement(bitstream, "FileSize")
|
|
778
|
+
filesize = SubElement(bitstream, "xip:FileSize")
|
|
764
779
|
file_stats = os.stat(file)
|
|
765
780
|
filesize.text = str(file_stats.st_size)
|
|
766
|
-
physical_location = SubElement(bitstream, "PhysicalLocation")
|
|
767
|
-
fixities = SubElement(bitstream, "Fixities")
|
|
781
|
+
physical_location = SubElement(bitstream, "xip:PhysicalLocation")
|
|
782
|
+
fixities = SubElement(bitstream, "xip:Fixities")
|
|
768
783
|
fixity_result = fixity_callback(filename_element.text, file)
|
|
769
784
|
if type(fixity_result) == tuple:
|
|
770
|
-
fixity = SubElement(fixities, "Fixity")
|
|
771
|
-
fixity_algorithm_ref = SubElement(fixity, "FixityAlgorithmRef")
|
|
772
|
-
fixity_value = SubElement(fixity, "FixityValue")
|
|
785
|
+
fixity = SubElement(fixities, "xip:Fixity")
|
|
786
|
+
fixity_algorithm_ref = SubElement(fixity, "xip:FixityAlgorithmRef")
|
|
787
|
+
fixity_value = SubElement(fixity, "xip:FixityValue")
|
|
773
788
|
fixity_algorithm_ref.text = fixity_result[0]
|
|
774
789
|
fixity_value.text = fixity_result[1]
|
|
775
790
|
elif type(fixity_result) == dict:
|
|
776
791
|
for key, val in fixity_result.items():
|
|
777
|
-
fixity = SubElement(fixities, "Fixity")
|
|
778
|
-
fixity_algorithm_ref = SubElement(fixity, "FixityAlgorithmRef")
|
|
779
|
-
fixity_value = SubElement(fixity, "FixityValue")
|
|
792
|
+
fixity = SubElement(fixities, "xip:Fixity")
|
|
793
|
+
fixity_algorithm_ref = SubElement(fixity, "xip:FixityAlgorithmRef")
|
|
794
|
+
fixity_value = SubElement(fixity, "xip:FixityValue")
|
|
780
795
|
fixity_algorithm_ref.text = key
|
|
781
796
|
fixity_value.text = val
|
|
782
797
|
else:
|
|
@@ -790,12 +805,12 @@ def multi_asset_package(asset_file_list=None, export_folder=None, parent_folder=
|
|
|
790
805
|
for identifier_key, identifier_value in identifier_map_values.items():
|
|
791
806
|
if identifier_key:
|
|
792
807
|
if identifier_value:
|
|
793
|
-
identifier = SubElement(xip, 'Identifier')
|
|
794
|
-
id_type = SubElement(identifier, "Type")
|
|
808
|
+
identifier = SubElement(xip, 'xip:Identifier')
|
|
809
|
+
id_type = SubElement(identifier, "xip:Type")
|
|
795
810
|
id_type.text = identifier_key
|
|
796
|
-
id_value = SubElement(identifier, "Value")
|
|
811
|
+
id_value = SubElement(identifier, "xip:Value")
|
|
797
812
|
id_value.text = identifier_value
|
|
798
|
-
id_io = SubElement(identifier, "Entity")
|
|
813
|
+
id_io = SubElement(identifier, "xip:Entity")
|
|
799
814
|
id_io.text = io_ref
|
|
800
815
|
|
|
801
816
|
src_file = file
|
|
@@ -815,7 +830,8 @@ def multi_asset_package(asset_file_list=None, export_folder=None, parent_folder=
|
|
|
815
830
|
return top_level_folder + ".zip"
|
|
816
831
|
|
|
817
832
|
|
|
818
|
-
def complex_asset_package(preservation_files_list=None, access_files_list=None, export_folder=None, parent_folder=None,
|
|
833
|
+
def complex_asset_package(preservation_files_list=None, access_files_list=None, export_folder=None, parent_folder=None,
|
|
834
|
+
compress=True,
|
|
819
835
|
**kwargs):
|
|
820
836
|
"""
|
|
821
837
|
|
|
@@ -908,13 +924,15 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
908
924
|
if has_preservation_files:
|
|
909
925
|
# add the content objects
|
|
910
926
|
representation_name = kwargs.get('Preservation_Representation_Name', "Preservation")
|
|
911
|
-
preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
|
|
927
|
+
preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
|
|
928
|
+
rep_type="Preservation",
|
|
912
929
|
rep_files=preservation_files_list, io_ref=io_ref)
|
|
913
930
|
|
|
914
931
|
if has_access_files:
|
|
915
932
|
# add the content objects
|
|
916
933
|
access_name = kwargs.get('Access_Representation_Name', "Access")
|
|
917
|
-
access_refs_dict = __make_representation_multiple_co__(xip, rep_name=access_name, rep_type="Access",
|
|
934
|
+
access_refs_dict = __make_representation_multiple_co__(xip, rep_name=access_name, rep_type="Access",
|
|
935
|
+
rep_files=access_files_list,
|
|
918
936
|
io_ref=io_ref)
|
|
919
937
|
|
|
920
938
|
if has_preservation_files:
|
|
@@ -922,7 +940,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
922
940
|
for content_ref, filename in preservation_refs_dict.items():
|
|
923
941
|
default_content_objects_title = os.path.splitext(os.path.basename(filename))[0]
|
|
924
942
|
preservation_content_title = kwargs.get('Preservation_Content_Title', default_content_objects_title)
|
|
925
|
-
preservation_content_description = kwargs.get('Preservation_Content_Description',
|
|
943
|
+
preservation_content_description = kwargs.get('Preservation_Content_Description',
|
|
944
|
+
default_content_objects_title)
|
|
926
945
|
|
|
927
946
|
if isinstance(preservation_content_title, dict):
|
|
928
947
|
preservation_content_title = preservation_content_title[filename]
|
|
@@ -930,7 +949,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
930
949
|
if isinstance(preservation_content_description, dict):
|
|
931
950
|
preservation_content_description = preservation_content_description[filename]
|
|
932
951
|
|
|
933
|
-
__make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag,
|
|
952
|
+
__make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag,
|
|
953
|
+
preservation_content_description,
|
|
934
954
|
content_type)
|
|
935
955
|
|
|
936
956
|
if has_access_files:
|
|
@@ -947,7 +967,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
947
967
|
if isinstance(access_content_description, dict):
|
|
948
968
|
access_content_title = access_content_title[filename]
|
|
949
969
|
|
|
950
|
-
__make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
|
|
970
|
+
__make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
|
|
971
|
+
access_content_description, content_type)
|
|
951
972
|
|
|
952
973
|
if has_preservation_files:
|
|
953
974
|
|
|
@@ -955,7 +976,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
955
976
|
|
|
956
977
|
for content_ref, filename in preservation_refs_dict.items():
|
|
957
978
|
preservation_file_name = os.path.basename(filename)
|
|
958
|
-
__make_generation__(xip, preservation_file_name, content_ref, preservation_generation_label,
|
|
979
|
+
__make_generation__(xip, preservation_file_name, content_ref, preservation_generation_label,
|
|
980
|
+
PRESERVATION_CONTENT_FOLDER)
|
|
959
981
|
|
|
960
982
|
if has_access_files:
|
|
961
983
|
|
|
@@ -1070,7 +1092,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
|
|
|
1070
1092
|
return top_level_folder + ".zip"
|
|
1071
1093
|
|
|
1072
1094
|
|
|
1073
|
-
def simple_asset_package(preservation_file=None, access_file=None, export_folder=None, parent_folder=None,
|
|
1095
|
+
def simple_asset_package(preservation_file=None, access_file=None, export_folder=None, parent_folder=None,
|
|
1096
|
+
compress=True, **kwargs):
|
|
1074
1097
|
"""
|
|
1075
1098
|
Create a Preservica package containing a single Asset from a single preservation file
|
|
1076
1099
|
and an optional access file.
|
|
@@ -1130,7 +1153,8 @@ def _unpad(s):
|
|
|
1130
1153
|
|
|
1131
1154
|
class UploadAPI(AuthenticatedAPI):
|
|
1132
1155
|
|
|
1133
|
-
def ingest_tweet(self, twitter_user=None, tweet_id: int = 0, twitter_consumer_key=None, twitter_secret_key=None,
|
|
1156
|
+
def ingest_tweet(self, twitter_user=None, tweet_id: int = 0, twitter_consumer_key=None, twitter_secret_key=None,
|
|
1157
|
+
folder=None,
|
|
1134
1158
|
callback=None, **kwargs):
|
|
1135
1159
|
|
|
1136
1160
|
"""
|
|
@@ -1174,7 +1198,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1174
1198
|
video_name_document_.close()
|
|
1175
1199
|
return video_name_, True
|
|
1176
1200
|
|
|
1177
|
-
entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1201
|
+
entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1202
|
+
tenant=self.tenant)
|
|
1178
1203
|
if hasattr(folder, "reference"):
|
|
1179
1204
|
folder = entity_client.folder(folder.reference)
|
|
1180
1205
|
else:
|
|
@@ -1312,7 +1337,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1312
1337
|
os.remove(ob)
|
|
1313
1338
|
os.remove("metadata.xml")
|
|
1314
1339
|
|
|
1315
|
-
def ingest_twitter_feed(self, twitter_user=None, num_tweets: int = 25, twitter_consumer_key=None,
|
|
1340
|
+
def ingest_twitter_feed(self, twitter_user=None, num_tweets: int = 25, twitter_consumer_key=None,
|
|
1341
|
+
twitter_secret_key=None, folder=None,
|
|
1316
1342
|
callback=None, **kwargs):
|
|
1317
1343
|
|
|
1318
1344
|
"""
|
|
@@ -1357,7 +1383,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1357
1383
|
video_name_document_.flush()
|
|
1358
1384
|
return video_name_, True
|
|
1359
1385
|
|
|
1360
|
-
entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1386
|
+
entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1387
|
+
tenant=self.tenant)
|
|
1361
1388
|
if hasattr(folder, "reference"):
|
|
1362
1389
|
folder = entity_client.folder(folder.reference)
|
|
1363
1390
|
else:
|
|
@@ -1488,7 +1515,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1488
1515
|
asset_title = kwargs.get("Title", text)
|
|
1489
1516
|
asset_description = kwargs.get("Description", full_text)
|
|
1490
1517
|
|
|
1491
|
-
p = complex_asset_package(preservation_files_list=content_objects, parent_folder=folder,
|
|
1518
|
+
p = complex_asset_package(preservation_files_list=content_objects, parent_folder=folder,
|
|
1519
|
+
Title=asset_title,
|
|
1492
1520
|
Description=asset_description, CustomType="Tweet", Identifiers=identifiers,
|
|
1493
1521
|
Asset_Metadata=asset_metadata, SecurityTag=security_tag)
|
|
1494
1522
|
self.upload_zip_package(p, folder=folder, callback=callback)
|
|
@@ -1572,7 +1600,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1572
1600
|
duration = meta.get('duration')
|
|
1573
1601
|
|
|
1574
1602
|
package = simple_asset_package(preservation_file=f"{vid_id}.mp4", parent_folder=parent_folder, Title=title,
|
|
1575
|
-
Description=description, Identifiers=identifier_map,
|
|
1603
|
+
Description=description, Identifiers=identifier_map,
|
|
1604
|
+
Asset_Metadata=descriptive_metadata,
|
|
1576
1605
|
Preservation_Content_Title=title, SecurityTag=security_tag)
|
|
1577
1606
|
|
|
1578
1607
|
self.upload_zip_package(path_to_zip_package=package, folder=parent_folder, callback=callback)
|
|
@@ -1593,7 +1622,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1593
1622
|
self.token = self.__token__()
|
|
1594
1623
|
return self.upload_credentials(location_id)
|
|
1595
1624
|
else:
|
|
1596
|
-
exception = HTTPException(location_id, request.status_code, request.url, "upload_credentials",
|
|
1625
|
+
exception = HTTPException(location_id, request.status_code, request.url, "upload_credentials",
|
|
1626
|
+
request.content.decode('utf-8'))
|
|
1597
1627
|
logger.error(exception)
|
|
1598
1628
|
raise exception
|
|
1599
1629
|
|
|
@@ -1612,7 +1642,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1612
1642
|
self.token = self.__token__()
|
|
1613
1643
|
return self.upload_locations()
|
|
1614
1644
|
else:
|
|
1615
|
-
exception = HTTPException("", request.status_code, request.url, "upload_locations",
|
|
1645
|
+
exception = HTTPException("", request.status_code, request.url, "upload_locations",
|
|
1646
|
+
request.content.decode('utf-8'))
|
|
1616
1647
|
logger.error(exception)
|
|
1617
1648
|
raise exception
|
|
1618
1649
|
|
|
@@ -1624,7 +1655,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1624
1655
|
"""
|
|
1625
1656
|
return self.upload_locations()
|
|
1626
1657
|
|
|
1627
|
-
def crawl_filesystem(self, filesystem_path, bucket_name, preservica_parent, callback: bool = False,
|
|
1658
|
+
def crawl_filesystem(self, filesystem_path, bucket_name, preservica_parent, callback: bool = False,
|
|
1659
|
+
security_tag: str = "open",
|
|
1628
1660
|
delete_after_upload: bool = True, max_MB_ingested: int = -1):
|
|
1629
1661
|
|
|
1630
1662
|
def get_parent(client, identifier, parent_reference):
|
|
@@ -1651,8 +1683,10 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1651
1683
|
return folder
|
|
1652
1684
|
|
|
1653
1685
|
from pyPreservica import EntityAPI
|
|
1654
|
-
entity_client = EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1655
|
-
|
|
1686
|
+
entity_client = EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1687
|
+
tenant=self.tenant,
|
|
1688
|
+
two_fa_secret_key=self.two_fa_secret_key, use_shared_secret=self.shared_secret,
|
|
1689
|
+
protocol=self.protocol)
|
|
1656
1690
|
|
|
1657
1691
|
if preservica_parent:
|
|
1658
1692
|
parent = entity_client.folder(preservica_parent)
|
|
@@ -1696,8 +1730,14 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1696
1730
|
else:
|
|
1697
1731
|
progress_display = None
|
|
1698
1732
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1733
|
+
if bucket_name is None:
|
|
1734
|
+
self.upload_zip_package(path_to_zip_package=package, callback=progress_display,
|
|
1735
|
+
delete_after_upload=delete_after_upload)
|
|
1736
|
+
else:
|
|
1737
|
+
self.upload_zip_package_to_S3(path_to_zip_package=package, bucket_name=bucket_name,
|
|
1738
|
+
callback=progress_display,
|
|
1739
|
+
delete_after_upload=delete_after_upload)
|
|
1740
|
+
|
|
1701
1741
|
logger.info(f"Uploaded " + "{:.1f}".format(bytes_ingested / (1024 * 1024)) + " MB")
|
|
1702
1742
|
|
|
1703
1743
|
if max_MB_ingested > 0:
|
|
@@ -1705,7 +1745,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1705
1745
|
logger.info(f"Reached Max Upload Limit")
|
|
1706
1746
|
break
|
|
1707
1747
|
|
|
1708
|
-
def upload_zip_to_Source(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
|
|
1748
|
+
def upload_zip_to_Source(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
|
|
1749
|
+
show_progress=False):
|
|
1709
1750
|
|
|
1710
1751
|
"""
|
|
1711
1752
|
Uploads a zip file package to either an Azure container or S3 bucket
|
|
@@ -1726,13 +1767,17 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1726
1767
|
callback = None
|
|
1727
1768
|
if show_progress:
|
|
1728
1769
|
callback = UploadProgressConsoleCallback(path_to_zip_package)
|
|
1729
|
-
self.upload_zip_package_to_S3(path_to_zip_package=path_to_zip_package, bucket_name=container_name,
|
|
1770
|
+
self.upload_zip_package_to_S3(path_to_zip_package=path_to_zip_package, bucket_name=container_name,
|
|
1771
|
+
folder=folder,
|
|
1730
1772
|
callback=callback, delete_after_upload=delete_after_upload)
|
|
1731
1773
|
else:
|
|
1732
|
-
self.upload_zip_package_to_Azure(path_to_zip_package=path_to_zip_package,
|
|
1733
|
-
|
|
1774
|
+
self.upload_zip_package_to_Azure(path_to_zip_package=path_to_zip_package,
|
|
1775
|
+
container_name=container_name, folder=folder,
|
|
1776
|
+
delete_after_upload=delete_after_upload,
|
|
1777
|
+
show_progress=show_progress)
|
|
1734
1778
|
|
|
1735
|
-
def upload_zip_package_to_Azure(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
|
|
1779
|
+
def upload_zip_package_to_Azure(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
|
|
1780
|
+
show_progress=False):
|
|
1736
1781
|
|
|
1737
1782
|
"""
|
|
1738
1783
|
Uploads a zip file package to an Azure container connected to a Preservica Cloud System
|
|
@@ -1745,7 +1790,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1745
1790
|
"""
|
|
1746
1791
|
|
|
1747
1792
|
if (self.major_version < 7) and (self.minor_version < 5):
|
|
1748
|
-
raise RuntimeError(
|
|
1793
|
+
raise RuntimeError(
|
|
1794
|
+
"This call [upload_zip_package_to_Azure] is only available against v6.5 systems and above")
|
|
1749
1795
|
|
|
1750
1796
|
from azure.storage.blob import ContainerClient
|
|
1751
1797
|
|
|
@@ -1773,11 +1819,13 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1773
1819
|
|
|
1774
1820
|
if show_progress:
|
|
1775
1821
|
with tqdm.wrapattr(open(path_to_zip_package, 'rb'), "read", total=len_bytes) as data:
|
|
1776
|
-
blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
|
|
1822
|
+
blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
|
|
1823
|
+
length=len_bytes)
|
|
1777
1824
|
properties = blob_client.get_blob_properties()
|
|
1778
1825
|
else:
|
|
1779
1826
|
with open(path_to_zip_package, "rb") as data:
|
|
1780
|
-
blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
|
|
1827
|
+
blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
|
|
1828
|
+
length=len_bytes)
|
|
1781
1829
|
properties = blob_client.get_blob_properties()
|
|
1782
1830
|
|
|
1783
1831
|
if delete_after_upload:
|
|
@@ -1785,7 +1833,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1785
1833
|
|
|
1786
1834
|
return properties
|
|
1787
1835
|
|
|
1788
|
-
def upload_zip_package_to_S3(self, path_to_zip_package, bucket_name, folder=None, callback=None,
|
|
1836
|
+
def upload_zip_package_to_S3(self, path_to_zip_package, bucket_name, folder=None, callback=None,
|
|
1837
|
+
delete_after_upload=False):
|
|
1789
1838
|
|
|
1790
1839
|
"""
|
|
1791
1840
|
Uploads a zip file package to an S3 bucket connected to a Preservica Cloud System
|
|
@@ -1814,7 +1863,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1814
1863
|
session_token = credentials['sessionToken']
|
|
1815
1864
|
endpoint = credentials['endpoint']
|
|
1816
1865
|
|
|
1817
|
-
session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key,
|
|
1866
|
+
session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key,
|
|
1867
|
+
aws_session_token=session_token)
|
|
1818
1868
|
s3 = session.resource(service_name="s3")
|
|
1819
1869
|
|
|
1820
1870
|
logger.debug(f"S3 Session: {s3}")
|
|
@@ -1833,7 +1883,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1833
1883
|
|
|
1834
1884
|
metadata_map = {'Metadata': metadata}
|
|
1835
1885
|
|
|
1836
|
-
s3_object.upload_file(path_to_zip_package, Callback=callback, ExtraArgs=metadata_map,
|
|
1886
|
+
s3_object.upload_file(path_to_zip_package, Callback=callback, ExtraArgs=metadata_map,
|
|
1887
|
+
Config=transfer_config)
|
|
1837
1888
|
|
|
1838
1889
|
if delete_after_upload:
|
|
1839
1890
|
os.remove(path_to_zip_package)
|
|
@@ -1859,7 +1910,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1859
1910
|
endpoint = f'{self.protocol}://{self.server}/api/s3/buckets'
|
|
1860
1911
|
self.token = self.__token__()
|
|
1861
1912
|
|
|
1862
|
-
s3_client = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=self.token,
|
|
1913
|
+
s3_client = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=self.token,
|
|
1914
|
+
aws_secret_access_key="NOT_USED",
|
|
1863
1915
|
config=Config(s3={'addressing_style': 'path'}))
|
|
1864
1916
|
|
|
1865
1917
|
metadata = {}
|
|
@@ -1879,7 +1931,8 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1879
1931
|
transfer.CompleteMultipartUploadTask = CompleteMultipartUploadTask
|
|
1880
1932
|
transfer.upload_file = upload_file
|
|
1881
1933
|
|
|
1882
|
-
response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket, key=key_id,
|
|
1934
|
+
response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket, key=key_id,
|
|
1935
|
+
extra_args=metadata,
|
|
1883
1936
|
callback=callback)
|
|
1884
1937
|
|
|
1885
1938
|
if delete_after_upload:
|
pyPreservica/workflowAPI.py
CHANGED
|
@@ -11,6 +11,7 @@ licence: Apache License 2.0
|
|
|
11
11
|
|
|
12
12
|
import uuid
|
|
13
13
|
import datetime
|
|
14
|
+
from typing import Callable
|
|
14
15
|
from xml.etree import ElementTree
|
|
15
16
|
|
|
16
17
|
from pyPreservica.common import *
|
|
@@ -21,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|
|
21
22
|
class WorkflowInstance:
|
|
22
23
|
"""
|
|
23
24
|
Defines a workflow Instance.
|
|
24
|
-
The workflow Instance is context which has been executed
|
|
25
|
+
The workflow Instance is a context which has been executed
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
def __init__(self, instance_id: int):
|
|
@@ -79,8 +80,11 @@ class WorkflowAPI(AuthenticatedAPI):
|
|
|
79
80
|
workflow_types = ['Ingest', 'Access', 'Transformation', 'DataManagement']
|
|
80
81
|
|
|
81
82
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
82
|
-
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
83
|
-
|
|
83
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
84
|
+
protocol: str = "https", request_hook: Callable = None):
|
|
85
|
+
|
|
86
|
+
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
87
|
+
protocol, request_hook)
|
|
84
88
|
self.base_url = "sdb/rest/workflow"
|
|
85
89
|
|
|
86
90
|
def get_workflow_contexts_by_type(self, workflow_type: str):
|
|
@@ -245,13 +249,13 @@ class WorkflowAPI(AuthenticatedAPI):
|
|
|
245
249
|
assert instance_id == w_id
|
|
246
250
|
workflow_instance = WorkflowInstance(int(instance_id))
|
|
247
251
|
started_element = entity_response.find(f".//{{{NS_WORKFLOW}}}Started")
|
|
248
|
-
if started_element:
|
|
252
|
+
if started_element is not None:
|
|
249
253
|
if hasattr(started_element, "text"):
|
|
250
254
|
workflow_instance.started = datetime.datetime.strptime(started_element.text,
|
|
251
255
|
'%Y-%m-%dT%H:%M:%S.%fZ')
|
|
252
256
|
|
|
253
257
|
finished_element = entity_response.find(f".//{{{NS_WORKFLOW}}}Finished")
|
|
254
|
-
if finished_element:
|
|
258
|
+
if finished_element is not None:
|
|
255
259
|
if hasattr(finished_element, "text"):
|
|
256
260
|
workflow_instance.finished = datetime.datetime.strptime(finished_element.text,
|
|
257
261
|
'%Y-%m-%dT%H:%M:%S.%fZ')
|
|
@@ -353,13 +357,13 @@ class WorkflowAPI(AuthenticatedAPI):
|
|
|
353
357
|
workflow_instance = WorkflowInstance(int(instance_id))
|
|
354
358
|
|
|
355
359
|
started_element = instance.find(f".//{{{NS_WORKFLOW}}}Started")
|
|
356
|
-
if started_element:
|
|
360
|
+
if started_element is not None:
|
|
357
361
|
if hasattr(started_element, "text"):
|
|
358
362
|
workflow_instance.started = datetime.datetime.strptime(started_element.text,
|
|
359
363
|
'%Y-%m-%dT%H:%M:%S.%fZ')
|
|
360
364
|
|
|
361
365
|
finished_element = instance.find(f".//{{{NS_WORKFLOW}}}Finished")
|
|
362
|
-
if finished_element:
|
|
366
|
+
if finished_element is not None:
|
|
363
367
|
if hasattr(finished_element, "text"):
|
|
364
368
|
workflow_instance.finished = datetime.datetime.strptime(finished_element.text,
|
|
365
369
|
'%Y-%m-%dT%H:%M:%S.%fZ')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
pyPreservica/__init__.py,sha256=RmOn1lvHV9iH7Hmz5YQ0F06ASdqetV0_-m1I8S4erqY,1117
|
|
2
|
+
pyPreservica/adminAPI.py,sha256=511bc5KtrCAXbDyBk39dmDnxUVDaOu6xaiyu0jYhxa4,37781
|
|
3
|
+
pyPreservica/authorityAPI.py,sha256=Eule8g6LXr8c8SFcJgpRah4lH1FgevUItO5HhHDEaZE,9172
|
|
4
|
+
pyPreservica/common.py,sha256=yZNMlq8aOOLSbFS2DDHYBUWyN5ojDjYUYmcePVbUd44,37636
|
|
5
|
+
pyPreservica/contentAPI.py,sha256=F3VwaybSUel0OfhWOckqfM77AVQCD1erHbu-Xrv4cd0,17388
|
|
6
|
+
pyPreservica/entityAPI.py,sha256=f-RlCEtUq0KDB62LuSPy0Kb-lT6Hp2gPOmSiTeomqkM,114853
|
|
7
|
+
pyPreservica/mdformsAPI.py,sha256=As5YN5m1k59N6SpM8fLldVviJ0n4lhlZD8rV5v0Jehs,4698
|
|
8
|
+
pyPreservica/monitorAPI.py,sha256=HD-PUPdSI9wGAa07e2_2_-FLINH8PoWUwpFogz7F-j4,6269
|
|
9
|
+
pyPreservica/opex.py,sha256=ccra1S4ojUXS3PlbU8WfxajOkJrwG4OykBnNrYP_jus,4875
|
|
10
|
+
pyPreservica/parAPI.py,sha256=bgaQvYfWNnzdD7ibKMV3ZV85pNkEdSoLsgVigoiFFfw,10771
|
|
11
|
+
pyPreservica/retentionAPI.py,sha256=EmQvmUW_I_sPslCiTZDZ2uqloesjfxmmc5AQImWX2cs,23695
|
|
12
|
+
pyPreservica/uploadAPI.py,sha256=Nl6Z4h1cW9TmZyrhVLPff3jSAnzCdkfn0xDJ9Q_ZhdI,96444
|
|
13
|
+
pyPreservica/webHooksAPI.py,sha256=0wP-59mep8gtlIZ9P5vV68-HnNdTuuo2kzGcDWj0bNg,6790
|
|
14
|
+
pyPreservica/workflowAPI.py,sha256=wDDR5_CsJ3dhX79E5mJaziAtgYb830J0ZpNJppzgvqk,17493
|
|
15
|
+
pyPreservica-2.7.4.dist-info/LICENSE.txt,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
16
|
+
pyPreservica-2.7.4.dist-info/METADATA,sha256=bbipiUj1ByD22LxVh2X7a3ytTLitfSt9GVi_nKfVoXs,2784
|
|
17
|
+
pyPreservica-2.7.4.dist-info/WHEEL,sha256=YiKiUUeZQGmGJoR_0N1Y933DOBowq4AIvDe2-UIy8E4,91
|
|
18
|
+
pyPreservica-2.7.4.dist-info/top_level.txt,sha256=iIBh6NAznYQHOV8mv_y_kGKSDITek9rANyFDwJsbU-c,13
|
|
19
|
+
pyPreservica-2.7.4.dist-info/RECORD,,
|
pyPreservica/vocabularyAPI.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
pyPreservica ControlledVocabularyAPI module definition
|
|
3
|
-
|
|
4
|
-
A client library for the Preservica Repository web services Webhook API
|
|
5
|
-
https://us.preservica.com/api/reference-metadata/documentation.html
|
|
6
|
-
|
|
7
|
-
author: James Carr
|
|
8
|
-
licence: Apache License 2.0
|
|
9
|
-
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from pyPreservica.common import *
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
BASE_ENDPOINT = '/api/reference-metadata'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Table:
|
|
20
|
-
def __init__(self, reference: str, name: str, security_tag: str, displayField: str, metadataConnections: list):
|
|
21
|
-
self.reference = reference
|
|
22
|
-
self.name = name
|
|
23
|
-
self.security_tag = security_tag
|
|
24
|
-
self.displayField = displayField
|
|
25
|
-
self.metadataConnections = metadataConnections
|
|
26
|
-
self.fields = None
|
|
27
|
-
|
|
28
|
-
def __str__(self):
|
|
29
|
-
return f"Ref:\t\t\t{self.reference}\n" \
|
|
30
|
-
f"Name:\t\t\t{self.name}\n" \
|
|
31
|
-
f"Security Tag:\t{self.security_tag}\n" \
|
|
32
|
-
f"Display Field:\t\t\t{self.displayField}\n" \
|
|
33
|
-
f"Metadata Connections:\t\t\t{self.metadataConnections}\n" \
|
|
34
|
-
f"Fields:\t\t\t{self.fields}\n"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class ControlledVocabularyAPI(AuthenticatedAPI):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def load_skos(self, uri):
|
|
41
|
-
"""
|
|
42
|
-
Load a SKOS controlled vocabulary in skos RDF format
|
|
43
|
-
|
|
44
|
-
Simple Knowledge Organization System (SKOS)
|
|
45
|
-
|
|
46
|
-
:param uri:
|
|
47
|
-
:return:
|
|
48
|
-
"""
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
def record(self, reference: str):
|
|
52
|
-
"""
|
|
53
|
-
Get individual record by its ref.
|
|
54
|
-
:param reference:
|
|
55
|
-
:return:
|
|
56
|
-
"""
|
|
57
|
-
headers = {HEADER_TOKEN: self.token, 'accept': 'application/json;charset=UTF-8'}
|
|
58
|
-
response = self.session.get(f'{self.protocol}://{self.server}{BASE_ENDPOINT}/records/{reference}',
|
|
59
|
-
headers=headers)
|
|
60
|
-
if response.status_code == requests.codes.unauthorized:
|
|
61
|
-
self.token = self.__token__()
|
|
62
|
-
return self.record(reference)
|
|
63
|
-
if response.status_code == requests.codes.ok:
|
|
64
|
-
json_response = str(response.content.decode('utf-8'))
|
|
65
|
-
return json.loads(json_response)
|
|
66
|
-
else:
|
|
67
|
-
exception = HTTPException("", response.status_code, response.url, "record",
|
|
68
|
-
response.content.decode('utf-8'))
|
|
69
|
-
logger.error(exception)
|
|
70
|
-
raise exception
|
|
71
|
-
|
|
72
|
-
def records(self, table: Table):
|
|
73
|
-
"""
|
|
74
|
-
Get all records from a table.
|
|
75
|
-
:return:
|
|
76
|
-
"""
|
|
77
|
-
headers = {HEADER_TOKEN: self.token, 'accept': 'application/json;charset=UTF-8'}
|
|
78
|
-
response = self.session.get(f'{self.protocol}://{self.server}{BASE_ENDPOINT}/tables/{table.reference}/records',
|
|
79
|
-
headers=headers)
|
|
80
|
-
if response.status_code == requests.codes.unauthorized:
|
|
81
|
-
self.token = self.__token__()
|
|
82
|
-
return self.records(table)
|
|
83
|
-
if response.status_code == requests.codes.ok:
|
|
84
|
-
json_response = str(response.content.decode('utf-8'))
|
|
85
|
-
return json.loads(json_response)['records']
|
|
86
|
-
else:
|
|
87
|
-
exception = HTTPException("", response.status_code, response.url, "records",
|
|
88
|
-
response.content.decode('utf-8'))
|
|
89
|
-
logger.error(exception)
|
|
90
|
-
raise exception
|
|
91
|
-
|
|
92
|
-
def table(self, reference: str):
|
|
93
|
-
"""
|
|
94
|
-
fetch a metadata table by id
|
|
95
|
-
|
|
96
|
-
:param reference:
|
|
97
|
-
:return:
|
|
98
|
-
"""
|
|
99
|
-
headers = {HEADER_TOKEN: self.token, 'accept': 'application/json;charset=UTF-8'}
|
|
100
|
-
response = self.session.get(f'{self.protocol}://{self.server}{BASE_ENDPOINT}/tables/{reference}',
|
|
101
|
-
headers=headers)
|
|
102
|
-
if response.status_code == requests.codes.unauthorized:
|
|
103
|
-
self.token = self.__token__()
|
|
104
|
-
return self.table(reference)
|
|
105
|
-
if response.status_code == requests.codes.ok:
|
|
106
|
-
json_response = str(response.content.decode('utf-8'))
|
|
107
|
-
doc = json.loads(json_response)
|
|
108
|
-
table = Table(doc['ref'], doc['name'], doc['securityDescriptor'], doc['displayField'],
|
|
109
|
-
doc['metadataConnections'])
|
|
110
|
-
table.fields = doc['fields']
|
|
111
|
-
return table
|
|
112
|
-
else:
|
|
113
|
-
exception = HTTPException("", response.status_code, response.url, "table",
|
|
114
|
-
response.content.decode('utf-8'))
|
|
115
|
-
logger.error(exception)
|
|
116
|
-
raise exception
|
|
117
|
-
|
|
118
|
-
def tables(self):
|
|
119
|
-
"""
|
|
120
|
-
List reference metadata tables, optionally filtering by metadata connections.
|
|
121
|
-
:return:
|
|
122
|
-
"""
|
|
123
|
-
headers = {HEADER_TOKEN: self.token, 'accept': 'application/json;charset=UTF-8'}
|
|
124
|
-
response = self.session.get(f'{self.protocol}://{self.server}{BASE_ENDPOINT}/tables', headers=headers)
|
|
125
|
-
if response.status_code == requests.codes.unauthorized:
|
|
126
|
-
self.token = self.__token__()
|
|
127
|
-
return self.tables()
|
|
128
|
-
if response.status_code == requests.codes.ok:
|
|
129
|
-
json_response = str(response.content.decode('utf-8'))
|
|
130
|
-
doc = json.loads(json_response)
|
|
131
|
-
results = set()
|
|
132
|
-
for table in doc['tables']:
|
|
133
|
-
t = Table(table['ref'], table['name'], table['securityDescriptor'], table['displayField'],
|
|
134
|
-
table['metadataConnections'])
|
|
135
|
-
results.add(t)
|
|
136
|
-
return results
|
|
137
|
-
else:
|
|
138
|
-
exception = HTTPException("", response.status_code, response.url, "tables",
|
|
139
|
-
response.content.decode('utf-8'))
|
|
140
|
-
logger.error(exception)
|
|
141
|
-
raise exception
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
pyPreservica/__init__.py,sha256=7niGhdk2fS4pMgHaqr-x-oZMJQg5-Nte_jfMNxZ3Lkk,1117
|
|
2
|
-
pyPreservica/adminAPI.py,sha256=511bc5KtrCAXbDyBk39dmDnxUVDaOu6xaiyu0jYhxa4,37781
|
|
3
|
-
pyPreservica/authorityAPI.py,sha256=Eule8g6LXr8c8SFcJgpRah4lH1FgevUItO5HhHDEaZE,9172
|
|
4
|
-
pyPreservica/common.py,sha256=upTmwvPK9kcgSO3L8mfIMVrCOWJCCYIHi5GK516jOok,36518
|
|
5
|
-
pyPreservica/contentAPI.py,sha256=d6bK7qXFLep8rOs8y-dJGzoFuqanO3fo82G4dW0YWWE,17245
|
|
6
|
-
pyPreservica/entityAPI.py,sha256=qNUjhAAdboBuY319AtyQV1n5N3Yrs1VY1enBxqQf5RQ,114750
|
|
7
|
-
pyPreservica/mdformsAPI.py,sha256=wHRCv5mcvztVFt9zBnUgHeGgSiNIrzR9IQOSV25daJ0,4576
|
|
8
|
-
pyPreservica/monitorAPI.py,sha256=HD-PUPdSI9wGAa07e2_2_-FLINH8PoWUwpFogz7F-j4,6269
|
|
9
|
-
pyPreservica/opex.py,sha256=ccra1S4ojUXS3PlbU8WfxajOkJrwG4OykBnNrYP_jus,4875
|
|
10
|
-
pyPreservica/parAPI.py,sha256=bgaQvYfWNnzdD7ibKMV3ZV85pNkEdSoLsgVigoiFFfw,10771
|
|
11
|
-
pyPreservica/retentionAPI.py,sha256=Cx1ofz9V31a8c8utEfKYLlfQaHSaaqg_D4R3LUFBEx0,23612
|
|
12
|
-
pyPreservica/uploadAPI.py,sha256=8GCQlofg-fxJEY_djeValbVwBZoTBujJEDFxpS_fEkQ,93894
|
|
13
|
-
pyPreservica/vocabularyAPI.py,sha256=jPl6KDZoBGqlY0oEYjTpZ9kNEPzchDW-gyp-HH-MSKk,5729
|
|
14
|
-
pyPreservica/webHooksAPI.py,sha256=0wP-59mep8gtlIZ9P5vV68-HnNdTuuo2kzGcDWj0bNg,6790
|
|
15
|
-
pyPreservica/workflowAPI.py,sha256=ENFWxcuPW5WX9jG2CAha6UzTywULWvosgTUVsmvs8f8,17323
|
|
16
|
-
pyPreservica-2.7.2.dist-info/LICENSE.txt,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
17
|
-
pyPreservica-2.7.2.dist-info/METADATA,sha256=GDQ2AtPbqDpmjxUGGIwq-8EIoOXkz_fsxwkpMf68TDc,2784
|
|
18
|
-
pyPreservica-2.7.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
19
|
-
pyPreservica-2.7.2.dist-info/top_level.txt,sha256=iIBh6NAznYQHOV8mv_y_kGKSDITek9rANyFDwJsbU-c,13
|
|
20
|
-
pyPreservica-2.7.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|