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.
Files changed (48) hide show
  1. {pypreservica-3.0.0 → pypreservica-3.0.2}/PKG-INFO +13 -2
  2. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/__init__.py +1 -1
  3. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/common.py +5 -2
  4. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/contentAPI.py +4 -3
  5. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/entityAPI.py +50 -14
  6. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/mdformsAPI.py +3 -2
  7. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/parAPI.py +1 -37
  8. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/retentionAPI.py +3 -2
  9. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/uploadAPI.py +27 -2
  10. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/workflowAPI.py +2 -2
  11. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/PKG-INFO +13 -2
  12. {pypreservica-3.0.0 → pypreservica-3.0.2}/setup.py +2 -2
  13. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_bitstream.py +8 -0
  14. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_users.py +32 -4
  15. {pypreservica-3.0.0 → pypreservica-3.0.2}/LICENSE.txt +0 -0
  16. {pypreservica-3.0.0 → pypreservica-3.0.2}/README.md +0 -0
  17. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/adminAPI.py +0 -0
  18. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/authorityAPI.py +0 -0
  19. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/monitorAPI.py +0 -0
  20. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/opex.py +0 -0
  21. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica/webHooksAPI.py +0 -0
  22. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/SOURCES.txt +0 -0
  23. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/dependency_links.txt +0 -0
  24. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/requires.txt +0 -0
  25. {pypreservica-3.0.0 → pypreservica-3.0.2}/pyPreservica.egg-info/top_level.txt +0 -0
  26. {pypreservica-3.0.0 → pypreservica-3.0.2}/setup.cfg +0 -0
  27. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_authority_records.py +0 -0
  28. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_children.py +0 -0
  29. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_content_api.py +0 -0
  30. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_crawl_fs.py +0 -0
  31. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_delete.py +0 -0
  32. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_download.py +0 -0
  33. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_entity.py +0 -0
  34. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_export_opex.py +0 -0
  35. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_groups.py +0 -0
  36. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_identifier.py +0 -0
  37. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_ingest.py +0 -0
  38. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_integrity_check.py +0 -0
  39. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_metadata.py +0 -0
  40. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_par.py +0 -0
  41. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_replace.py +0 -0
  42. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_retention.py +0 -0
  43. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_schema.py +0 -0
  44. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_security.py +0 -0
  45. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_thumbnail.py +0 -0
  46. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_upload.py +0 -0
  47. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_workflow.py +0 -0
  48. {pypreservica-3.0.0 → pypreservica-3.0.2}/tests/test_xml_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: pyPreservica
3
- Version: 3.0.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.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('credentials.properties', encoding='utf-8')
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, 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:
@@ -8,7 +8,7 @@ author: James Carr
8
8
  licence: Apache License 2.0
9
9
 
10
10
  """
11
- import hashlib
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
- bitstream_list.append(self.bitstream(bit.text))
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('credentials.properties', encoding='utf-8')
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
1
+ Metadata-Version: 2.2
2
2
  Name: pyPreservica
3
- Version: 3.0.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 setup.py sdist bdist_wheel')
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.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 test_get_all_users():
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