pyPreservica 3.0.0__tar.gz → 3.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pypreservica-3.0.0 → pypreservica-3.0.2}/PKG-INFO +13 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/__init__.py +1 -1
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/common.py +5 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/contentAPI.py +4 -3
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/entityAPI.py +50 -14
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/mdformsAPI.py +3 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/parAPI.py +1 -37
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/retentionAPI.py +3 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/uploadAPI.py +27 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/workflowAPI.py +2 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/PKG-INFO +13 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/setup.py +2 -2
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_bitstream.py +8 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_users.py +32 -4
- {pypreservica-3.0.0 → pypreservica-3.0.2}/LICENSE.txt +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/README.md +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/adminAPI.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/authorityAPI.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/monitorAPI.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/opex.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/webHooksAPI.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/SOURCES.txt +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/dependency_links.txt +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/requires.txt +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/top_level.txt +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/setup.cfg +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_authority_records.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_children.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_content_api.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_crawl_fs.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_delete.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_download.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_entity.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_export_opex.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_groups.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_identifier.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_ingest.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_integrity_check.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_metadata.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_par.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_replace.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_retention.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_schema.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_security.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_thumbnail.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_upload.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_workflow.py +0 -0
- {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_xml_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pyPreservica
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Python library for the Preservica API
|
|
5
5
|
Home-page: https://pypreservica.readthedocs.io/
|
|
6
6
|
Author: James Carr
|
|
@@ -30,6 +30,17 @@ Requires-Dist: s3transfer
|
|
|
30
30
|
Requires-Dist: azure-storage-blob
|
|
31
31
|
Requires-Dist: tqdm
|
|
32
32
|
Requires-Dist: pyotp
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: keywords
|
|
40
|
+
Dynamic: license
|
|
41
|
+
Dynamic: project-url
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
33
44
|
|
|
34
45
|
|
|
35
46
|
# pyPreservica
|
|
@@ -23,6 +23,6 @@ from .mdformsAPI import MetadataGroupsAPI, Group, GroupField, GroupFieldType
|
|
|
23
23
|
__author__ = "James Carr (drjamescarr@gmail.com)"
|
|
24
24
|
|
|
25
25
|
# Version of the pyPreservica package
|
|
26
|
-
__version__ = "3.0.
|
|
26
|
+
__version__ = "3.0.2"
|
|
27
27
|
|
|
28
28
|
__license__ = "Apache License Version 2.0"
|
|
@@ -405,6 +405,9 @@ class Bitstream:
|
|
|
405
405
|
self.length = int(length)
|
|
406
406
|
self.fixity = fixity
|
|
407
407
|
self.content_url = content_url
|
|
408
|
+
self.bs_index = None
|
|
409
|
+
self.gen_index = None
|
|
410
|
+
self.co_ref = None
|
|
408
411
|
|
|
409
412
|
def __str__(self):
|
|
410
413
|
return f"""
|
|
@@ -880,10 +883,10 @@ class AuthenticatedAPI:
|
|
|
880
883
|
|
|
881
884
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
882
885
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
883
|
-
protocol: str = "https", request_hook=None):
|
|
886
|
+
protocol: str = "https", request_hook=None, credentials_path: str = 'credentials.properties'):
|
|
884
887
|
|
|
885
888
|
config = configparser.ConfigParser(interpolation=configparser.Interpolation())
|
|
886
|
-
config.read(
|
|
889
|
+
config.read(os.path.relpath(credentials_path), encoding='utf-8')
|
|
887
890
|
self.session: Session = requests.Session()
|
|
888
891
|
|
|
889
892
|
if request_hook is not None:
|
|
@@ -35,11 +35,12 @@ class Field:
|
|
|
35
35
|
|
|
36
36
|
class ContentAPI(AuthenticatedAPI):
|
|
37
37
|
|
|
38
|
-
def __init__(self, username=None, password=None, tenant=None, server=None,
|
|
39
|
-
|
|
38
|
+
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
39
|
+
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
40
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
40
41
|
|
|
41
42
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
42
|
-
protocol, request_hook)
|
|
43
|
+
protocol, request_hook, credentials_path)
|
|
43
44
|
self.callback = None
|
|
44
45
|
|
|
45
46
|
class SearchResult:
|
|
@@ -8,7 +8,7 @@ author: James Carr
|
|
|
8
8
|
licence: Apache License 2.0
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import os.path
|
|
13
13
|
import uuid
|
|
14
14
|
import xml.etree.ElementTree
|
|
@@ -35,10 +35,10 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
35
35
|
|
|
36
36
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
37
37
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
38
|
-
protocol: str = "https", request_hook: Callable = None):
|
|
38
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
39
39
|
|
|
40
40
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
41
|
-
protocol, request_hook)
|
|
41
|
+
protocol, request_hook, credentials_path)
|
|
42
42
|
|
|
43
43
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
44
44
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
@@ -118,6 +118,38 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
118
118
|
logger.error(exception)
|
|
119
119
|
raise exception
|
|
120
120
|
|
|
121
|
+
def bitstream_location(self, bitstream: Bitstream):
|
|
122
|
+
""""
|
|
123
|
+
Retrieves information about a bitstreams storage locations
|
|
124
|
+
"""
|
|
125
|
+
if not isinstance(bitstream, Bitstream):
|
|
126
|
+
logger.error("bitstream argument is not a Bitstream object")
|
|
127
|
+
raise RuntimeError("bitstream argument is not a Bitstream object")
|
|
128
|
+
|
|
129
|
+
storage_locations = []
|
|
130
|
+
|
|
131
|
+
url: str = f'{self.protocol}://{self.server}/api/entity/content-objects/{bitstream.co_ref}/generations/{bitstream.gen_index}/bitstreams/{bitstream.bs_index}/storage-locations'
|
|
132
|
+
|
|
133
|
+
with self.session.get(url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
|
|
134
|
+
if request.status_code == requests.codes.unauthorized:
|
|
135
|
+
self.token = self.__token__()
|
|
136
|
+
return self.bitstream_location(bitstream)
|
|
137
|
+
elif request.status_code == requests.codes.ok:
|
|
138
|
+
xml_response = str(request.content.decode('utf-8'))
|
|
139
|
+
entity_response = xml.etree.ElementTree.fromstring(xml_response)
|
|
140
|
+
logger.debug(xml_response)
|
|
141
|
+
locations = entity_response.find(f'.//{{{self.entity_ns}}}StorageLocation')
|
|
142
|
+
for adapter in locations:
|
|
143
|
+
storage_locations.append(adapter.attrib['name'])
|
|
144
|
+
else:
|
|
145
|
+
exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_location",
|
|
146
|
+
request.content.decode('utf-8'))
|
|
147
|
+
logger.error(exception)
|
|
148
|
+
raise exception
|
|
149
|
+
|
|
150
|
+
return storage_locations
|
|
151
|
+
|
|
152
|
+
|
|
121
153
|
def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
|
|
122
154
|
"""
|
|
123
155
|
Download a file represented as a Bitstream to a local filename
|
|
@@ -1474,7 +1506,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1474
1506
|
logger.error(exception)
|
|
1475
1507
|
raise exception
|
|
1476
1508
|
|
|
1477
|
-
def generation(self, url: str) -> Generation:
|
|
1509
|
+
def generation(self, url: str, content_ref: str = None) -> Generation:
|
|
1478
1510
|
"""
|
|
1479
1511
|
Retrieve a list of generation objects
|
|
1480
1512
|
|
|
@@ -1524,7 +1556,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1524
1556
|
bitstreams = entity_response.findall(f'./{{{self.entity_ns}}}Bitstreams/{{{self.entity_ns}}}Bitstream')
|
|
1525
1557
|
bitstream_list = []
|
|
1526
1558
|
for bit in bitstreams:
|
|
1527
|
-
|
|
1559
|
+
bs: Bitstream = self.bitstream(bit.text)
|
|
1560
|
+
bs.gen_index = index
|
|
1561
|
+
if content_ref is not None:
|
|
1562
|
+
bs.co_ref = content_ref
|
|
1563
|
+
bitstream_list.append(bs)
|
|
1528
1564
|
generation = Generation(strtobool(ge.attrib['original']), strtobool(ge.attrib['active']),
|
|
1529
1565
|
format_group.text if hasattr(format_group, 'text') else None,
|
|
1530
1566
|
effective_date.text if hasattr(effective_date, 'text') else None,
|
|
@@ -1741,7 +1777,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
1741
1777
|
result = []
|
|
1742
1778
|
for g in generations:
|
|
1743
1779
|
if hasattr(g, 'text'):
|
|
1744
|
-
generation = self.generation(g.text)
|
|
1780
|
+
generation = self.generation(g.text, content_object.reference)
|
|
1745
1781
|
generation.asset = content_object.asset
|
|
1746
1782
|
generation.content_object = content_object
|
|
1747
1783
|
generation.representation_type = content_object.representation_type
|
|
@@ -2281,7 +2317,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2281
2317
|
logger.error(exception)
|
|
2282
2318
|
raise exception
|
|
2283
2319
|
|
|
2284
|
-
def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str):
|
|
2320
|
+
def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2285
2321
|
"""
|
|
2286
2322
|
Delete an asset from the repository
|
|
2287
2323
|
|
|
@@ -2290,11 +2326,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2290
2326
|
:param supervisor_comment: The supervisor comment on the deletion
|
|
2291
2327
|
"""
|
|
2292
2328
|
if isinstance(asset, Asset):
|
|
2293
|
-
return self._delete_entity(asset, operator_comment, supervisor_comment)
|
|
2329
|
+
return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
|
|
2294
2330
|
else:
|
|
2295
2331
|
raise RuntimeError("delete_asset only deletes assets")
|
|
2296
2332
|
|
|
2297
|
-
def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str):
|
|
2333
|
+
def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2298
2334
|
"""
|
|
2299
2335
|
Delete an asset from the repository
|
|
2300
2336
|
|
|
@@ -2304,11 +2340,11 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2304
2340
|
:param supervisor_comment: The supervisor comment on the deletion
|
|
2305
2341
|
"""
|
|
2306
2342
|
if isinstance(folder, Folder):
|
|
2307
|
-
return self._delete_entity(folder, operator_comment, supervisor_comment)
|
|
2343
|
+
return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
|
|
2308
2344
|
else:
|
|
2309
2345
|
raise RuntimeError("delete_folder only deletes folders")
|
|
2310
2346
|
|
|
2311
|
-
def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str):
|
|
2347
|
+
def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
|
|
2312
2348
|
"""
|
|
2313
2349
|
Delete an asset from the repository
|
|
2314
2350
|
|
|
@@ -2319,7 +2355,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2319
2355
|
|
|
2320
2356
|
# check manager password is available:
|
|
2321
2357
|
config = configparser.ConfigParser()
|
|
2322
|
-
config.read(
|
|
2358
|
+
config.read(credentials_path, encoding='utf-8')
|
|
2323
2359
|
try:
|
|
2324
2360
|
manager_username = config['credentials']['manager.username']
|
|
2325
2361
|
manager_password = config['credentials']['manager.password']
|
|
@@ -2373,7 +2409,7 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2373
2409
|
headers=headers)
|
|
2374
2410
|
elif request.status_code == requests.codes.unauthorized:
|
|
2375
2411
|
self.token = self.__token__()
|
|
2376
|
-
return self._delete_entity(entity, operator_comment, supervisor_comment)
|
|
2412
|
+
return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
|
|
2377
2413
|
if request.status_code == requests.codes.unprocessable:
|
|
2378
2414
|
logger.error(request.content.decode('utf-8'))
|
|
2379
2415
|
raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
|
|
@@ -2385,4 +2421,4 @@ class EntityAPI(AuthenticatedAPI):
|
|
|
2385
2421
|
exception = HTTPException(entity.reference, request.status_code, request.url,
|
|
2386
2422
|
"_delete_entity", request.content.decode('utf-8'))
|
|
2387
2423
|
logger.error(exception)
|
|
2388
|
-
raise exception
|
|
2424
|
+
raise exception
|
|
@@ -134,12 +134,13 @@ def _json_from_object_(group: Group) -> dict:
|
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
class MetadataGroupsAPI(AuthenticatedAPI):
|
|
137
|
+
|
|
137
138
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
138
139
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
139
|
-
protocol: str = "https", request_hook: Callable = None):
|
|
140
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
140
141
|
|
|
141
142
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
142
|
-
protocol, request_hook)
|
|
143
|
+
protocol, request_hook, credentials_path)
|
|
143
144
|
|
|
144
145
|
xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
|
|
145
146
|
xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
|
|
@@ -23,43 +23,7 @@ def __get_contents__(document) -> AnyStr:
|
|
|
23
23
|
return json.dumps(json.loads(document))
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class PreservationActionRegistry:
|
|
27
|
-
|
|
28
|
-
def __init__(self, server: str = None, username: str = None, password: str = None, protocol: str = 'https'):
|
|
29
|
-
self.protocol = protocol
|
|
30
|
-
self.session = requests.Session()
|
|
31
|
-
self.session.headers.update({'accept': 'application/json;charset=UTF-8'})
|
|
32
|
-
config = configparser.ConfigParser()
|
|
33
|
-
config.read('credentials.properties')
|
|
34
|
-
if not server:
|
|
35
|
-
server = os.environ.get('PRESERVICA_SERVER')
|
|
36
|
-
if server is None:
|
|
37
|
-
try:
|
|
38
|
-
server = config['credentials']['server']
|
|
39
|
-
except KeyError:
|
|
40
|
-
pass
|
|
41
|
-
if not server:
|
|
42
|
-
msg = "No valid server found in method arguments, environment variables or credentials.properties file"
|
|
43
|
-
logger.error(msg)
|
|
44
|
-
raise RuntimeError(msg)
|
|
45
|
-
else:
|
|
46
|
-
self.server = server
|
|
47
|
-
if not username:
|
|
48
|
-
username = os.environ.get('PRESERVICA_USERNAME')
|
|
49
|
-
if username is None:
|
|
50
|
-
try:
|
|
51
|
-
username = config['credentials']['username']
|
|
52
|
-
except KeyError:
|
|
53
|
-
pass
|
|
54
|
-
self.username = username
|
|
55
|
-
if not password:
|
|
56
|
-
password = os.environ.get('PRESERVICA_PASSWORD')
|
|
57
|
-
if password is None:
|
|
58
|
-
try:
|
|
59
|
-
password = config['credentials']['password']
|
|
60
|
-
except KeyError:
|
|
61
|
-
pass
|
|
62
|
-
self.password = password
|
|
26
|
+
class PreservationActionRegistry(AuthenticatedAPI):
|
|
63
27
|
|
|
64
28
|
def format_family(self, guid: str) -> str:
|
|
65
29
|
return self.__guid__(guid, "format-families")
|
|
@@ -58,9 +58,10 @@ class RetentionPolicy:
|
|
|
58
58
|
class RetentionAPI(AuthenticatedAPI):
|
|
59
59
|
|
|
60
60
|
def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False,
|
|
61
|
-
two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
|
|
61
|
+
two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
62
62
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
63
|
-
protocol, request_hook)
|
|
63
|
+
protocol, request_hook, credentials_path)
|
|
64
|
+
|
|
64
65
|
if self.major_version < 7 and self.minor_version < 2:
|
|
65
66
|
raise RuntimeError("Retention API is only available when connected to a v6.2 System")
|
|
66
67
|
|
|
@@ -1659,7 +1659,22 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1659
1659
|
|
|
1660
1660
|
def crawl_filesystem(self, filesystem_path, bucket_name, preservica_parent, callback: bool = False,
|
|
1661
1661
|
security_tag: str = "open",
|
|
1662
|
-
delete_after_upload: bool = True, max_MB_ingested: int = -1):
|
|
1662
|
+
delete_after_upload: bool = True, max_MB_ingested: int = -1, max_workflows = 8):
|
|
1663
|
+
"""
|
|
1664
|
+
Crawl a filesystem and upload the contents to Preservica, the filesystem structure is mirrored in Preservica
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
:param str filesystem_path: The path to the filesystem to crawl
|
|
1668
|
+
:param str bucket_name: The name of the bucket to upload to, use None to send directly to Preservica
|
|
1669
|
+
:param str preservica_parent: The parent folder in Preservica to upload to
|
|
1670
|
+
:param bool callback: Show upload progress bar
|
|
1671
|
+
:param str security_tag: The security tag to apply to the uploaded assets
|
|
1672
|
+
:param bool delete_after_upload: Delete the local copy of the package after the upload has completed
|
|
1673
|
+
:param int max_MB_ingested: The maximum number of MB to ingest
|
|
1674
|
+
:param int max_workflows: The maximum number of workflows to run concurrently
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
"""
|
|
1663
1678
|
|
|
1664
1679
|
def get_parent(client, identifier, parent_reference):
|
|
1665
1680
|
id = str(os.path.dirname(identifier))
|
|
@@ -1684,12 +1699,17 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1684
1699
|
folder = entities.pop()
|
|
1685
1700
|
return folder
|
|
1686
1701
|
|
|
1687
|
-
from pyPreservica import EntityAPI
|
|
1702
|
+
from pyPreservica import EntityAPI, WorkflowAPI
|
|
1688
1703
|
entity_client = EntityAPI(username=self.username, password=self.password, server=self.server,
|
|
1689
1704
|
tenant=self.tenant,
|
|
1690
1705
|
two_fa_secret_key=self.two_fa_secret_key, use_shared_secret=self.shared_secret,
|
|
1691
1706
|
protocol=self.protocol)
|
|
1692
1707
|
|
|
1708
|
+
workflow_client = WorkflowAPI(username=self.username, password=self.password, server=self.server,
|
|
1709
|
+
tenant=self.tenant,
|
|
1710
|
+
two_fa_secret_key=self.two_fa_secret_key, use_shared_secret=self.shared_secret,
|
|
1711
|
+
protocol=self.protocol)
|
|
1712
|
+
|
|
1693
1713
|
if preservica_parent:
|
|
1694
1714
|
parent = entity_client.folder(preservica_parent)
|
|
1695
1715
|
logger.info(f"Folders will be created inside Preservica collection {parent.title}")
|
|
@@ -1732,6 +1752,11 @@ class UploadAPI(AuthenticatedAPI):
|
|
|
1732
1752
|
else:
|
|
1733
1753
|
progress_display = None
|
|
1734
1754
|
|
|
1755
|
+
workflow_queue_length = len(list(workflow_client.workflow_instances(workflow_state="Active", workflow_type="Ingest")))
|
|
1756
|
+
while workflow_queue_length > max_workflows:
|
|
1757
|
+
sleep(30)
|
|
1758
|
+
workflow_queue_length = len(list(workflow_client.workflow_instances(workflow_state="Active", workflow_type="Ingest")))
|
|
1759
|
+
|
|
1735
1760
|
if bucket_name is None:
|
|
1736
1761
|
self.upload_zip_package(path_to_zip_package=package, callback=progress_display,
|
|
1737
1762
|
delete_after_upload=delete_after_upload)
|
|
@@ -81,10 +81,10 @@ class WorkflowAPI(AuthenticatedAPI):
|
|
|
81
81
|
|
|
82
82
|
def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
|
|
83
83
|
use_shared_secret: bool = False, two_fa_secret_key: str = None,
|
|
84
|
-
protocol: str = "https", request_hook: Callable = None):
|
|
84
|
+
protocol: str = "https", request_hook: Callable = None, credentials_path: str = 'credentials.properties'):
|
|
85
85
|
|
|
86
86
|
super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
|
|
87
|
-
protocol, request_hook)
|
|
87
|
+
protocol, request_hook, credentials_path)
|
|
88
88
|
self.base_url = "sdb/rest/workflow"
|
|
89
89
|
|
|
90
90
|
def get_workflow_contexts_by_type(self, workflow_type: str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pyPreservica
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Python library for the Preservica API
|
|
5
5
|
Home-page: https://pypreservica.readthedocs.io/
|
|
6
6
|
Author: James Carr
|
|
@@ -30,6 +30,17 @@ Requires-Dist: s3transfer
|
|
|
30
30
|
Requires-Dist: azure-storage-blob
|
|
31
31
|
Requires-Dist: tqdm
|
|
32
32
|
Requires-Dist: pyotp
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: keywords
|
|
40
|
+
Dynamic: license
|
|
41
|
+
Dynamic: project-url
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
33
44
|
|
|
34
45
|
|
|
35
46
|
# pyPreservica
|
|
@@ -13,7 +13,7 @@ PKG = "pyPreservica"
|
|
|
13
13
|
|
|
14
14
|
# 'setup.py publish' shortcut.
|
|
15
15
|
if sys.argv[-1] == 'publish':
|
|
16
|
-
os.system('python
|
|
16
|
+
os.system('python -m build')
|
|
17
17
|
os.system('twine upload dist/*')
|
|
18
18
|
sys.exit()
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ if sys.argv[-1] == 'publish':
|
|
|
21
21
|
# This call to setup() does all the work
|
|
22
22
|
setup(
|
|
23
23
|
name=PKG,
|
|
24
|
-
version="3.0.
|
|
24
|
+
version="3.0.2",
|
|
25
25
|
description="Python library for the Preservica API",
|
|
26
26
|
long_description=README,
|
|
27
27
|
long_description_content_type="text/markdown",
|
|
@@ -97,3 +97,11 @@ def test_get_bitstream_content(setup_data):
|
|
|
97
97
|
assert os.path.isfile(bitstream.filename) is True
|
|
98
98
|
assert Path(bitstream.filename).stat().st_size == 1942466
|
|
99
99
|
os.remove(bitstream.filename)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_get_bitstream_locations(setup_data):
|
|
103
|
+
client = EntityAPI()
|
|
104
|
+
asset = client.asset(ASSET_ID)
|
|
105
|
+
for bs in client.bitstreams_for_asset(asset):
|
|
106
|
+
locations = client.bitstream_location(bs)
|
|
107
|
+
assert "Primary Adapter" in locations
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
from pyPreservica import *
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
def
|
|
5
|
+
def setup():
|
|
6
|
+
client = AdminAPI()
|
|
7
|
+
users = client.all_users()
|
|
8
|
+
for u in users:
|
|
9
|
+
if u == "pypreservica@gmail.com":
|
|
10
|
+
client.delete_user(u)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def tear_down():
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def setup_data():
|
|
20
|
+
print("\nSetting up resources...")
|
|
21
|
+
|
|
22
|
+
setup()
|
|
23
|
+
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
print("\nTearing down resources...")
|
|
27
|
+
|
|
28
|
+
tear_down()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_get_all_users(setup_data):
|
|
5
33
|
client = AdminAPI()
|
|
6
34
|
users = client.all_users()
|
|
7
35
|
assert type(users) is list
|
|
@@ -10,7 +38,7 @@ def test_get_all_users():
|
|
|
10
38
|
assert client.username in users
|
|
11
39
|
|
|
12
40
|
|
|
13
|
-
def test_get_user():
|
|
41
|
+
def test_get_user(setup_data):
|
|
14
42
|
client = AdminAPI()
|
|
15
43
|
user = client.user_details(client.username)
|
|
16
44
|
assert type(user) is dict
|
|
@@ -19,7 +47,7 @@ def test_get_user():
|
|
|
19
47
|
assert 'SDB_MANAGER_USER' in user['Roles']
|
|
20
48
|
|
|
21
49
|
|
|
22
|
-
def test_add_user():
|
|
50
|
+
def test_add_user(setup_data):
|
|
23
51
|
client = AdminAPI()
|
|
24
52
|
user = client.add_user("pypreservica@gmail.com", "pypreservica", ['SDB_MANAGER_USER', 'SDB_INGEST_USER'])
|
|
25
53
|
assert user['UserName'] == "pypreservica@gmail.com"
|
|
@@ -30,7 +58,7 @@ def test_add_user():
|
|
|
30
58
|
assert "pypreservica@gmail.com" not in client.all_users()
|
|
31
59
|
|
|
32
60
|
|
|
33
|
-
def test_change_display_name():
|
|
61
|
+
def test_change_display_name(setup_data):
|
|
34
62
|
client = AdminAPI()
|
|
35
63
|
user = client.add_user("pypreservica@gmail.com", "pypreservica", ['SDB_MANAGER_USER', 'SDB_INGEST_USER'])
|
|
36
64
|
assert user['UserName'] == "pypreservica@gmail.com"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|