pyPreservica 2.9.4__tar.gz → 3.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {pypreservica-2.9.4 → pypreservica-3.0.1}/PKG-INFO +1 -1
  2. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/__init__.py +1 -1
  3. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/common.py +2 -2
  4. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/contentAPI.py +4 -3
  5. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/entityAPI.py +10 -10
  6. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/mdformsAPI.py +3 -2
  7. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/parAPI.py +1 -37
  8. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/retentionAPI.py +3 -2
  9. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/uploadAPI.py +61 -10
  10. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/workflowAPI.py +2 -2
  11. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica.egg-info/PKG-INFO +1 -1
  12. {pypreservica-2.9.4 → pypreservica-3.0.1}/setup.py +1 -1
  13. {pypreservica-2.9.4 → pypreservica-3.0.1}/LICENSE.txt +0 -0
  14. {pypreservica-2.9.4 → pypreservica-3.0.1}/README.md +0 -0
  15. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/adminAPI.py +0 -0
  16. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/authorityAPI.py +0 -0
  17. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/monitorAPI.py +0 -0
  18. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/opex.py +0 -0
  19. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica/webHooksAPI.py +0 -0
  20. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica.egg-info/SOURCES.txt +0 -0
  21. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica.egg-info/dependency_links.txt +0 -0
  22. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica.egg-info/requires.txt +0 -0
  23. {pypreservica-2.9.4 → pypreservica-3.0.1}/pyPreservica.egg-info/top_level.txt +0 -0
  24. {pypreservica-2.9.4 → pypreservica-3.0.1}/setup.cfg +0 -0
  25. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_authority_records.py +0 -0
  26. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_bitstream.py +0 -0
  27. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_children.py +0 -0
  28. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_content_api.py +0 -0
  29. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_crawl_fs.py +0 -0
  30. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_delete.py +0 -0
  31. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_download.py +0 -0
  32. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_entity.py +0 -0
  33. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_export_opex.py +0 -0
  34. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_groups.py +0 -0
  35. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_identifier.py +0 -0
  36. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_ingest.py +0 -0
  37. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_integrity_check.py +0 -0
  38. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_metadata.py +0 -0
  39. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_par.py +0 -0
  40. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_replace.py +0 -0
  41. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_retention.py +0 -0
  42. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_schema.py +0 -0
  43. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_security.py +0 -0
  44. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_thumbnail.py +0 -0
  45. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_upload.py +0 -0
  46. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_users.py +0 -0
  47. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_workflow.py +0 -0
  48. {pypreservica-2.9.4 → pypreservica-3.0.1}/tests/test_xml_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyPreservica
3
- Version: 2.9.4
3
+ Version: 3.0.1
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -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__ = "2.9.4"
26
+ __version__ = "3.0.1"
27
27
 
28
28
  __license__ = "Apache License Version 2.0"
@@ -880,10 +880,10 @@ class AuthenticatedAPI:
880
880
 
881
881
  def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
882
882
  use_shared_secret: bool = False, two_fa_secret_key: str = None,
883
- protocol: str = "https", request_hook=None):
883
+ protocol: str = "https", request_hook=None, credentials_path: str = 'credentials.properties'):
884
884
 
885
885
  config = configparser.ConfigParser(interpolation=configparser.Interpolation())
886
- config.read('credentials.properties', encoding='utf-8')
886
+ config.read(os.path.relpath(credentials_path), encoding='utf-8')
887
887
  self.session: Session = requests.Session()
888
888
 
889
889
  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, use_shared_secret=False,
39
- two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
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:
@@ -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")
@@ -2281,7 +2281,7 @@ class EntityAPI(AuthenticatedAPI):
2281
2281
  logger.error(exception)
2282
2282
  raise exception
2283
2283
 
2284
- def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str):
2284
+ def delete_asset(self, asset: Asset, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
2285
2285
  """
2286
2286
  Delete an asset from the repository
2287
2287
 
@@ -2290,11 +2290,11 @@ class EntityAPI(AuthenticatedAPI):
2290
2290
  :param supervisor_comment: The supervisor comment on the deletion
2291
2291
  """
2292
2292
  if isinstance(asset, Asset):
2293
- return self._delete_entity(asset, operator_comment, supervisor_comment)
2293
+ return self._delete_entity(asset, operator_comment, supervisor_comment, credentials_path)
2294
2294
  else:
2295
2295
  raise RuntimeError("delete_asset only deletes assets")
2296
2296
 
2297
- def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str):
2297
+ def delete_folder(self, folder: Folder, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
2298
2298
  """
2299
2299
  Delete an asset from the repository
2300
2300
 
@@ -2304,11 +2304,11 @@ class EntityAPI(AuthenticatedAPI):
2304
2304
  :param supervisor_comment: The supervisor comment on the deletion
2305
2305
  """
2306
2306
  if isinstance(folder, Folder):
2307
- return self._delete_entity(folder, operator_comment, supervisor_comment)
2307
+ return self._delete_entity(folder, operator_comment, supervisor_comment, credentials_path)
2308
2308
  else:
2309
2309
  raise RuntimeError("delete_folder only deletes folders")
2310
2310
 
2311
- def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str):
2311
+ def _delete_entity(self, entity: Entity, operator_comment: str, supervisor_comment: str, credentials_path: str = "credentials.properties"):
2312
2312
  """
2313
2313
  Delete an asset from the repository
2314
2314
 
@@ -2319,7 +2319,7 @@ class EntityAPI(AuthenticatedAPI):
2319
2319
 
2320
2320
  # check manager password is available:
2321
2321
  config = configparser.ConfigParser()
2322
- config.read('credentials.properties', encoding='utf-8')
2322
+ config.read(credentials_path, encoding='utf-8')
2323
2323
  try:
2324
2324
  manager_username = config['credentials']['manager.username']
2325
2325
  manager_password = config['credentials']['manager.password']
@@ -2373,7 +2373,7 @@ class EntityAPI(AuthenticatedAPI):
2373
2373
  headers=headers)
2374
2374
  elif request.status_code == requests.codes.unauthorized:
2375
2375
  self.token = self.__token__()
2376
- return self._delete_entity(entity, operator_comment, supervisor_comment)
2376
+ return self._delete_entity(entity, operator_comment, supervisor_comment, credentials_path)
2377
2377
  if request.status_code == requests.codes.unprocessable:
2378
2378
  logger.error(request.content.decode('utf-8'))
2379
2379
  raise RuntimeError(request.status_code, "no active workflow context for full deletion exists in the system")
@@ -2385,4 +2385,4 @@ class EntityAPI(AuthenticatedAPI):
2385
2385
  exception = HTTPException(entity.reference, request.status_code, request.url,
2386
2386
  "_delete_entity", request.content.decode('utf-8'))
2387
2387
  logger.error(exception)
2388
- raise exception
2388
+ 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
 
@@ -13,20 +13,22 @@ import shutil
13
13
  import tempfile
14
14
  import uuid
15
15
  import xml
16
- from datetime import datetime, timedelta
16
+ from datetime import datetime, timedelta, timezone
17
17
  from time import sleep
18
18
  from xml.dom import minidom
19
19
  from xml.etree import ElementTree
20
20
  from xml.etree.ElementTree import Element, SubElement
21
21
 
22
22
  import boto3
23
+ import botocore
23
24
  import s3transfer.tasks
24
25
  import s3transfer.upload
25
-
26
+ from botocore.session import get_session
26
27
  from boto3.s3.transfer import TransferConfig, S3Transfer
27
28
  from botocore.config import Config
28
29
  from botocore.credentials import RefreshableCredentials
29
- from botocore.exceptions import ClientError
30
+ from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
31
+ from dateutil.tz import tzlocal
30
32
  from s3transfer import S3UploadFailedError
31
33
  from tqdm import tqdm
32
34
 
@@ -1916,10 +1918,32 @@ class UploadAPI(AuthenticatedAPI):
1916
1918
  'mode': 'adaptive'
1917
1919
  }
1918
1920
 
1919
- s3_client = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=self.token,
1920
- aws_secret_access_key="NOT_USED",
1921
+ def new_credentials():
1922
+ metadata: dict = {}
1923
+ metadata['access_key'] = self.__token__()
1924
+ metadata['secret_key'] = "NOT_USED"
1925
+ metadata['token'] = ""
1926
+ metadata["expiry_time"] = (datetime.now(tzlocal()) + timedelta(minutes=12)).isoformat()
1927
+ logger.info("Refreshing credentials at: " + str(datetime.now(tzlocal())))
1928
+ return metadata
1929
+
1930
+ session = get_session()
1931
+
1932
+ session_credentials = RefreshableCredentials.create_from_metadata(
1933
+ metadata=new_credentials(),
1934
+ refresh_using=new_credentials,
1935
+ advisory_timeout = 4 * 60,
1936
+ mandatory_timeout = 12 * 60,
1937
+ method = 'Preservica'
1938
+ )
1939
+
1940
+ autorefresh_session = boto3.Session(botocore_session=session)
1941
+
1942
+ session._credentials = session_credentials
1943
+
1944
+ s3_client = autorefresh_session.client('s3', endpoint_url=endpoint,
1921
1945
  config=Config(s3={'addressing_style': 'path'}, read_timeout=120, connect_timeout=120,
1922
- retries=retries, tcp_keepalive=True))
1946
+ retries=retries, tcp_keepalive=True))
1923
1947
 
1924
1948
  metadata = {}
1925
1949
  if folder is not None:
@@ -1932,21 +1956,48 @@ class UploadAPI(AuthenticatedAPI):
1932
1956
  try:
1933
1957
  key_id = str(uuid.uuid4()) + ".zip"
1934
1958
 
1959
+
1960
+ # how big is the package
1961
+ package_size = os.path.getsize(path_to_zip_package)
1962
+ if package_size > 1 * GB:
1963
+ transfer_config.multipart_chunksize = 16 * MB ## Min 64 Chunks
1964
+ if package_size > 8 * GB:
1965
+ transfer_config.multipart_chunksize = 32 * MB ## Min 256 Chunks
1966
+ if package_size > 24 * GB:
1967
+ transfer_config.multipart_chunksize = 48 * MB ## Min 512 Chunks
1968
+ if package_size > 48 * GB:
1969
+ transfer_config.multipart_chunksize = 64 * MB
1970
+
1971
+ logger.info("Using Multipart Chunk Size: " + str(transfer_config.multipart_chunksize))
1972
+
1935
1973
  transfer = S3Transfer(client=s3_client, config=transfer_config)
1936
1974
 
1937
1975
  transfer.PutObjectTask = PutObjectTask
1938
1976
  transfer.CompleteMultipartUploadTask = CompleteMultipartUploadTask
1939
1977
  transfer.upload_file = upload_file
1940
1978
 
1941
- response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket, key=key_id,
1979
+
1980
+ response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket,
1981
+ key=key_id,
1942
1982
  extra_args=metadata,
1943
1983
  callback=callback)
1944
1984
 
1985
+
1945
1986
  if delete_after_upload:
1946
1987
  os.remove(path_to_zip_package)
1947
1988
 
1948
1989
  return response['ResponseMetadata']['HTTPHeaders']['preservica-progress-token']
1949
1990
 
1950
- except ClientError as e:
1951
- logger.error(e)
1952
- raise e
1991
+ except (NoCredentialsError, PartialCredentialsError) as ex:
1992
+ logger.error(ex)
1993
+ raise ex
1994
+
1995
+ except ClientError as ex:
1996
+ logger.error(ex)
1997
+ raise ex
1998
+
1999
+
2000
+
2001
+
2002
+
2003
+
@@ -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
1
  Metadata-Version: 2.1
2
2
  Name: pyPreservica
3
- Version: 2.9.4
3
+ Version: 3.0.1
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -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="2.9.4",
24
+ version="3.0.1",
25
25
  description="Python library for the Preservica API",
26
26
  long_description=README,
27
27
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes