pyPreservica 3.2.2__tar.gz → 3.2.4__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.

Potentially problematic release.


This version of pyPreservica might be problematic. Click here for more details.

Files changed (50) hide show
  1. {pypreservica-3.2.2 → pypreservica-3.2.4}/PKG-INFO +1 -1
  2. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/__init__.py +1 -1
  3. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/adminAPI.py +19 -13
  4. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/authorityAPI.py +1 -1
  5. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/common.py +10 -7
  6. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/entityAPI.py +86 -30
  7. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/mdformsAPI.py +20 -0
  8. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/uploadAPI.py +46 -0
  9. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/webHooksAPI.py +1 -0
  10. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica.egg-info/PKG-INFO +1 -1
  11. {pypreservica-3.2.2 → pypreservica-3.2.4}/setup.py +1 -1
  12. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_export_opex.py +29 -4
  13. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_schema.py +11 -11
  14. {pypreservica-3.2.2 → pypreservica-3.2.4}/LICENSE.txt +0 -0
  15. {pypreservica-3.2.2 → pypreservica-3.2.4}/README.md +0 -0
  16. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/contentAPI.py +0 -0
  17. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/monitorAPI.py +0 -0
  18. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/opex.py +0 -0
  19. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/parAPI.py +0 -0
  20. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/retentionAPI.py +0 -0
  21. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/settingsAPI.py +0 -0
  22. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica/workflowAPI.py +0 -0
  23. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica.egg-info/SOURCES.txt +0 -0
  24. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica.egg-info/dependency_links.txt +0 -0
  25. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica.egg-info/requires.txt +0 -0
  26. {pypreservica-3.2.2 → pypreservica-3.2.4}/pyPreservica.egg-info/top_level.txt +0 -0
  27. {pypreservica-3.2.2 → pypreservica-3.2.4}/setup.cfg +0 -0
  28. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_authority_records.py +0 -0
  29. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_bitstream.py +0 -0
  30. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_children.py +0 -0
  31. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_content_api.py +0 -0
  32. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_crawl_fs.py +0 -0
  33. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_delete.py +0 -0
  34. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_download.py +0 -0
  35. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_entity.py +0 -0
  36. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_groups.py +0 -0
  37. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_identifier.py +0 -0
  38. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_ingest.py +0 -0
  39. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_integrity_check.py +0 -0
  40. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_metadata.py +0 -0
  41. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_par.py +0 -0
  42. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_replace.py +0 -0
  43. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_retention.py +0 -0
  44. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_security.py +0 -0
  45. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_settings.py +0 -0
  46. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_thumbnail.py +0 -0
  47. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_upload.py +0 -0
  48. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_users.py +0 -0
  49. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_workflow.py +0 -0
  50. {pypreservica-3.2.2 → pypreservica-3.2.4}/tests/test_xml_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyPreservica
3
- Version: 3.2.2
3
+ Version: 3.2.4
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -35,6 +35,6 @@ from .settingsAPI import SettingsAPI
35
35
  __author__ = "James Carr (drjamescarr@gmail.com)"
36
36
 
37
37
  # Version of the pyPreservica package
38
- __version__ = "3.2.2"
38
+ __version__ = "3.2.4"
39
39
 
40
40
  __license__ = "Apache License Version 2.0"
@@ -10,7 +10,7 @@ licence: Apache License 2.0
10
10
  """
11
11
  import csv
12
12
  import xml.etree.ElementTree
13
- from typing import List, Any
13
+ from typing import List, Any, Union
14
14
 
15
15
  from pyPreservica.common import *
16
16
 
@@ -36,7 +36,7 @@ class AdminAPI(AuthenticatedAPI):
36
36
  request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/security/roles/{role_name}',
37
37
  headers=headers)
38
38
  if request.status_code == requests.codes.no_content:
39
- return
39
+ return None
40
40
  elif request.status_code == requests.codes.unauthorized:
41
41
  self.token = self.__token__()
42
42
  return self.delete_system_role(role_name)
@@ -61,7 +61,7 @@ class AdminAPI(AuthenticatedAPI):
61
61
  request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/security/tags/{tag_name}',
62
62
  headers=headers)
63
63
  if request.status_code == requests.codes.no_content:
64
- return
64
+ return None
65
65
  elif request.status_code == requests.codes.unauthorized:
66
66
  self.token = self.__token__()
67
67
  return self.delete_security_tag(tag_name)
@@ -211,7 +211,7 @@ class AdminAPI(AuthenticatedAPI):
211
211
  headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
212
212
  request = self.session.delete(f'{self.protocol}://{self.server}/api/admin/users/{username}', headers=headers)
213
213
  if request.status_code == requests.codes.no_content:
214
- return
214
+ return None
215
215
  elif request.status_code == requests.codes.unauthorized:
216
216
  self.token = self.__token__()
217
217
  return self.delete_user(username)
@@ -463,7 +463,7 @@ class AdminAPI(AuthenticatedAPI):
463
463
  params=params,
464
464
  data=xml_data)
465
465
  if request.status_code == requests.codes.created:
466
- return
466
+ return None
467
467
  elif request.status_code == requests.codes.unauthorized:
468
468
  self.token = self.__token__()
469
469
  return self.add_xml_schema(name, description, originalName, xml_data)
@@ -513,7 +513,7 @@ class AdminAPI(AuthenticatedAPI):
513
513
  params=params,
514
514
  data=xml_data)
515
515
  if request.status_code == requests.codes.created:
516
- return
516
+ return None
517
517
  elif request.status_code == requests.codes.unauthorized:
518
518
  self.token = self.__token__()
519
519
  return self.add_xml_document(name, xml_data, document_type)
@@ -543,13 +543,14 @@ class AdminAPI(AuthenticatedAPI):
543
543
  f"{self.protocol}://{self.server}/api/admin/documents/{document['ApiId']}",
544
544
  headers=headers)
545
545
  if request.status_code == requests.codes.no_content:
546
- return
546
+ return None
547
547
  elif request.status_code == requests.codes.unauthorized:
548
548
  self.token = self.__token__()
549
549
  return self.delete_xml_document(uri)
550
550
  else:
551
551
  logger.error(request.content.decode('utf-8'))
552
552
  raise RuntimeError(request.status_code, "delete_xml_document failed")
553
+ return None
553
554
 
554
555
  def delete_xml_schema(self, uri: str):
555
556
  """
@@ -572,15 +573,16 @@ class AdminAPI(AuthenticatedAPI):
572
573
  request = self.session.delete(f"{self.protocol}://{self.server}/api/admin/schemas/{schema['ApiId']}",
573
574
  headers=headers)
574
575
  if request.status_code == requests.codes.no_content:
575
- return
576
+ return None
576
577
  elif request.status_code == requests.codes.unauthorized:
577
578
  self.token = self.__token__()
578
579
  return self.delete_xml_schema(uri)
579
580
  else:
580
581
  logger.error(request.content.decode('utf-8'))
581
582
  raise RuntimeError(request.status_code, "delete_xml_schema failed")
583
+ return None
582
584
 
583
- def xml_schema(self, uri: str) -> str:
585
+ def xml_schema(self, uri: str) -> Union[str, None]:
584
586
  """
585
587
  Fetch the metadata schema XSD document as a string by its URI
586
588
 
@@ -607,8 +609,9 @@ class AdminAPI(AuthenticatedAPI):
607
609
  else:
608
610
  logger.error(request.content.decode('utf-8'))
609
611
  raise RuntimeError(request.status_code, "xml_schema failed")
612
+ return None
610
613
 
611
- def xml_document(self, uri: str) -> str:
614
+ def xml_document(self, uri: str) -> Union[str, None]:
612
615
  """
613
616
  fetch the metadata XML document as a string by its URI
614
617
 
@@ -634,6 +637,7 @@ class AdminAPI(AuthenticatedAPI):
634
637
  else:
635
638
  logger.error(request.content.decode('utf-8'))
636
639
  raise RuntimeError(request.status_code, "xml_document failed")
640
+ return None
637
641
 
638
642
  def xml_documents(self) -> List:
639
643
  """
@@ -754,7 +758,7 @@ class AdminAPI(AuthenticatedAPI):
754
758
  logger.error(request.content.decode('utf-8'))
755
759
  raise RuntimeError(request.status_code, "xml_transforms failed")
756
760
 
757
- def xml_transform(self, input_uri: str, output_uri: str) -> str:
761
+ def xml_transform(self, input_uri: str, output_uri: str) -> Union[str, None]:
758
762
  """
759
763
  fetch the XML transform as a string by its URIs
760
764
 
@@ -782,6 +786,7 @@ class AdminAPI(AuthenticatedAPI):
782
786
  else:
783
787
  logger.error(request.content.decode('utf-8'))
784
788
  raise RuntimeError(request.status_code, "xml_transform failed")
789
+ return None
785
790
 
786
791
  def delete_xml_transform(self, input_uri: str, output_uri: str):
787
792
  """
@@ -808,13 +813,14 @@ class AdminAPI(AuthenticatedAPI):
808
813
  f"{self.protocol}://{self.server}/api/admin/transforms/{transform['ApiId']}",
809
814
  headers=headers)
810
815
  if request.status_code == requests.codes.no_content:
811
- return
816
+ return None
812
817
  elif request.status_code == requests.codes.unauthorized:
813
818
  self.token = self.__token__()
814
819
  return self.delete_xml_transform(input_uri, output_uri)
815
820
  else:
816
821
  logger.error(request.content.decode('utf-8'))
817
822
  raise RuntimeError(request.status_code, "delete_xml_transform failed")
823
+ return None
818
824
 
819
825
  def add_xml_transform(self, name: str, input_uri: str, output_uri: str, purpose: str, originalName: str,
820
826
  xml_data: Any):
@@ -860,7 +866,7 @@ class AdminAPI(AuthenticatedAPI):
860
866
  params=params,
861
867
  data=xml_data)
862
868
  if request.status_code == requests.codes.created:
863
- return
869
+ return None
864
870
 
865
871
  if request.status_code == requests.codes.unauthorized:
866
872
  self.token = self.__token__()
@@ -54,7 +54,7 @@ class AuthorityAPI(AuthenticatedAPI):
54
54
  self.token = self.__token__()
55
55
  return self.delete_record(reference)
56
56
  if response.status_code == requests.codes.no_content:
57
- return
57
+ return None
58
58
  else:
59
59
  exception = HTTPException("", response.status_code, response.url, "delete_record",
60
60
  response.content.decode('utf-8'))
@@ -673,19 +673,24 @@ class AuthenticatedAPI:
673
673
  logger.error(f"The AdminAPI requires the user to have ROLE_SDB_MANAGER_USER")
674
674
  raise RuntimeError(f"The API requires the user to have at least the ROLE_SDB_MANAGER_USER")
675
675
 
676
- def _find_user_roles_(self) -> list:
676
+ def _find_user_roles_(self) -> list[str]:
677
677
  """
678
678
  Get a list of roles for the user
679
679
  :return list of roles:
680
680
  """
681
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
681
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
682
682
  request = self.session.get(f"{self.protocol}://{self.server}/api/user/details", headers=headers)
683
+ logger.debug(request.headers)
683
684
  if request.status_code == requests.codes.ok:
684
- roles = json.loads(str(request.content.decode('utf-8')))['roles']
685
+ json_document = str(request.content.decode('utf-8'))
686
+ logger.debug(json_document)
687
+ roles: list[str] = json.loads(json_document)['roles']
685
688
  return roles
686
689
  elif request.status_code == requests.codes.unauthorized:
687
690
  self.token = self.__token__()
688
691
  return self._find_user_roles_()
692
+ return []
693
+
689
694
 
690
695
  def security_tags_base(self, with_permissions: bool = False) -> dict:
691
696
  """
@@ -817,8 +822,6 @@ class AuthenticatedAPI:
817
822
  self.minor_version = int(version_numbers[1])
818
823
  self.patch_version = int(version_numbers[2])
819
824
 
820
- if self.server == "preview.preservica.com":
821
- self.minor_version = 1
822
825
 
823
826
  return version
824
827
  elif request.status_code == requests.codes.unauthorized:
@@ -847,7 +850,7 @@ class AuthenticatedAPI:
847
850
  with open('credentials.properties', 'wt', encoding="utf-8") as configfile:
848
851
  config.write(configfile)
849
852
 
850
- def manager_token(self, username: str, password: str):
853
+ def manager_token(self, username: str, password: str) -> str:
851
854
  data = {'username': username, 'password': password, 'tenant': self.tenant}
852
855
  response = self.session.post(f'{self.protocol}://{self.server}/api/accesstoken/login', data=data)
853
856
  if response.status_code == requests.codes.ok:
@@ -859,7 +862,7 @@ class AuthenticatedAPI:
859
862
  logger.error(str(response.content))
860
863
  RuntimeError(response.status_code, "Could not generate valid manager approval token")
861
864
 
862
- def __token__(self):
865
+ def __token__(self) -> str:
863
866
  """
864
867
  Generate am API token to use to authenticate calls
865
868
  :return: API Token
@@ -82,28 +82,28 @@ class EntityAPI(AuthenticatedAPI):
82
82
 
83
83
  def bitstream_bytes(self, bitstream: Bitstream, chunk_size: int = CHUNK_SIZE) -> Union[BytesIO, None]:
84
84
  """
85
- Download a file represented as a Bitstream to a byteIO array
85
+ Download a file represented as a Bitstream to a byteIO array
86
86
 
87
- Returns the byteIO
88
- Returns None if the file does not contain the correct number of bytes (default 2k)
87
+ Returns the byteIO
88
+ Returns None if the file does not contain the correct number of bytes (default 2k)
89
89
 
90
- :param chunk_size: The buffer copy chunk size in bytes default
91
- :param bitstream: A Bitstream object
92
- :type bitstream: Bitstream
90
+ :param chunk_size: The buffer copy chunk size in bytes default
91
+ :param bitstream: A Bitstream object
92
+ :type bitstream: Bitstream
93
93
 
94
- :return: The file in bytes
95
- :rtype: byteIO
94
+ :return: The file in bytes
95
+ :rtype: byteIO
96
96
  """
97
97
  if not isinstance(bitstream, Bitstream):
98
98
  logger.error("bitstream_content argument is not a Bitstream object")
99
99
  raise RuntimeError("bitstream_bytes argument is not a Bitstream object")
100
- with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as request:
101
- if request.status_code == requests.codes.unauthorized:
100
+ with self.session.get(bitstream.content_url, headers={HEADER_TOKEN: self.token}, stream=True) as response:
101
+ if response.status_code == requests.codes.unauthorized:
102
102
  self.token = self.__token__()
103
103
  return self.bitstream_bytes(bitstream)
104
- elif request.status_code == requests.codes.ok:
104
+ elif response.status_code == requests.codes.ok:
105
105
  file_bytes = BytesIO()
106
- for chunk in request.iter_content(chunk_size=chunk_size):
106
+ for chunk in response.iter_content(chunk_size=chunk_size):
107
107
  file_bytes.write(chunk)
108
108
  file_bytes.seek(0)
109
109
  if file_bytes.getbuffer().nbytes == bitstream.length:
@@ -113,8 +113,8 @@ class EntityAPI(AuthenticatedAPI):
113
113
  logger.error("Downloaded file size did not match the Preservica held value")
114
114
  return None
115
115
  else:
116
- exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_content",
117
- request.content.decode('utf-8'))
116
+ exception = HTTPException(bitstream.filename, response.status_code, response.url, "bitstream_content",
117
+ response.content.decode('utf-8'))
118
118
  logger.error(exception)
119
119
  raise exception
120
120
 
@@ -131,23 +131,25 @@ class EntityAPI(AuthenticatedAPI):
131
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
132
 
133
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:
134
+ if request.status_code == requests.codes.ok:
138
135
  xml_response = str(request.content.decode('utf-8'))
139
136
  entity_response = xml.etree.ElementTree.fromstring(xml_response)
140
137
  logger.debug(xml_response)
141
138
  locations = entity_response.find(f'.//{{{self.entity_ns}}}StorageLocation')
142
139
  for adapter in locations:
143
140
  storage_locations.append(adapter.attrib['name'])
141
+ return storage_locations
142
+
143
+ if request.status_code == requests.codes.unauthorized:
144
+ self.token = self.__token__()
145
+ return self.bitstream_location(bitstream)
144
146
  else:
145
147
  exception = HTTPException(bitstream.filename, request.status_code, request.url, "bitstream_location",
146
148
  request.content.decode('utf-8'))
147
149
  logger.error(exception)
148
150
  raise exception
149
151
 
150
- return storage_locations
152
+
151
153
 
152
154
  def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
153
155
  """
@@ -760,7 +762,7 @@ class EntityAPI(AuthenticatedAPI):
760
762
  end_point = f"{entity.path}/{entity.reference}/links/{relationship.api_id}"
761
763
  request = self.session.delete(f'{self.protocol}://{self.server}/api/entity/{end_point}', headers=headers)
762
764
  if request.status_code == requests.codes.no_content:
763
- print(relationship)
765
+ return None
764
766
  elif request.status_code == requests.codes.unauthorized:
765
767
  self.token = self.__token__()
766
768
  return self.__delete_relationship(relationship)
@@ -779,7 +781,7 @@ class EntityAPI(AuthenticatedAPI):
779
781
  :type: page_size: int
780
782
 
781
783
  :param entity: The Source Entity
782
- :type: entity: Entity
784
+ :type: entity: An Entity type such as Asset, Folder etc
783
785
 
784
786
  :return: Generator
785
787
  :rtype: Relationship
@@ -847,7 +849,7 @@ class EntityAPI(AuthenticatedAPI):
847
849
 
848
850
  return PagedSet(results, has_more, int(total_hits.text), url)
849
851
  elif request.status_code == requests.codes.unauthorized:
850
- self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
852
+ return self.__relationships__(entity=entity, maximum=maximum, next_page=next_page)
851
853
  else:
852
854
  exception = HTTPException(entity.reference, request.status_code, request.url, "relationships",
853
855
  request.content.decode('utf-8'))
@@ -1042,7 +1044,6 @@ class EntityAPI(AuthenticatedAPI):
1042
1044
  xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
1043
1045
  end_point = f"/{entity.path}/{entity.reference}/metadata"
1044
1046
  logger.debug(xml_request)
1045
- print(xml_request)
1046
1047
  request = self.session.post(f'{self.protocol}://{self.server}/api/entity{end_point}', data=xml_request,
1047
1048
  headers=headers)
1048
1049
  if request.status_code == requests.codes.ok:
@@ -1108,6 +1109,7 @@ class EntityAPI(AuthenticatedAPI):
1108
1109
  if 'CustomType' in response:
1109
1110
  content_object.custom_type = response['CustomType']
1110
1111
  return content_object
1112
+ return None
1111
1113
  elif request.status_code == requests.codes.unauthorized:
1112
1114
  self.token = self.__token__()
1113
1115
  return self.save(entity)
@@ -1296,9 +1298,9 @@ class EntityAPI(AuthenticatedAPI):
1296
1298
  for uri, schema_name in entity.metadata.items():
1297
1299
  if schema == schema_name:
1298
1300
  return self.metadata(uri)
1299
- return
1301
+ return None
1300
1302
 
1301
- def metadata_tag_for_entity(self, entity: Entity, schema: str, tag: str, isXpath: bool = False) -> str:
1303
+ def metadata_tag_for_entity(self, entity: Entity, schema: str, tag: str, isXpath: bool = False) -> Union[str, None]:
1302
1304
  """
1303
1305
  Retrieve the first value of the tag from a metadata template given by schema
1304
1306
 
@@ -1313,10 +1315,11 @@ class EntityAPI(AuthenticatedAPI):
1313
1315
  xml_doc = self.metadata_for_entity(entity, schema)
1314
1316
  if xml_doc:
1315
1317
  xml_object = xml.etree.ElementTree.fromstring(xml_doc)
1316
- if isXpath is False:
1318
+ if not isXpath:
1317
1319
  return xml_object.find(f'.//{{*}}{tag}').text
1318
1320
  else:
1319
1321
  return xml_object.find(tag).text
1322
+ return None
1320
1323
 
1321
1324
  def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
1322
1325
  """
@@ -1413,6 +1416,7 @@ class EntityAPI(AuthenticatedAPI):
1413
1416
  return self.folder(reference)
1414
1417
  if entity_type is EntityType.ASSET:
1415
1418
  return self.asset(reference)
1419
+ return None
1416
1420
 
1417
1421
  def add_physical_asset(self, title: str, description: str, parent: Folder, security_tag: str = "open") -> Asset:
1418
1422
  """
@@ -1460,11 +1464,62 @@ class EntityAPI(AuthenticatedAPI):
1460
1464
  logger.error(exception)
1461
1465
  raise exception
1462
1466
 
1463
- def merge_folder(self, folder: Folder)-> str:
1467
+ def merge_assets(self, assets: list[Asset], title: str, description: str) -> str:
1468
+ """
1469
+ Create a new Asset with the content from each Asset in supplied list
1470
+ This call will create a new multipart Asset which contains all the content from list of Assets.
1471
+
1472
+ The return value is the progress status of the merge operation.
1473
+ """
1474
+
1475
+ headers = {
1476
+ HEADER_TOKEN: self.token,
1477
+ "Content-Type": "application/xml;charset=UTF-8",
1478
+ "accept": "text/plain;charset=UTF-8",
1479
+ }
1480
+
1481
+ merge_object = xml.etree.ElementTree.Element("MergeAction", {"xmlns": self.entity_ns, "xmlns:xip": self.xip_ns})
1482
+ xml.etree.ElementTree.SubElement(merge_object, "Title").text = str(title)
1483
+ xml.etree.ElementTree.SubElement(merge_object, "Description").text = str(description)
1484
+ for a in assets:
1485
+ xml.etree.ElementTree.SubElement(merge_object, "Entity", {
1486
+ "excludeIdentifiers": "true",
1487
+ "excludeLinks": "true",
1488
+ "excludeMetadata": "true",
1489
+ "ref": a.reference,
1490
+ "type": EntityType.ASSET.value}
1491
+ )
1492
+ # order_object = xml.etree.ElementTree.SubElement(merge_object, "Order")
1493
+ # for a in assets:
1494
+ # xml.etree.ElementTree.SubElement(order_object, "Entity", {
1495
+ # "ref": a.reference,
1496
+ # "type": EntityType.CONTENT_OBJECT.value}
1497
+ # )
1498
+ xml_request = xml.etree.ElementTree.tostring(merge_object, encoding="utf-8")
1499
+ print(xml_request)
1500
+ request = self.session.post(
1501
+ f"{self.protocol}://{self.server}/api/entity/actions/merges", data=xml_request, headers=headers)
1502
+ if request.status_code == requests.codes.accepted:
1503
+ return request.content.decode('utf-8')
1504
+ elif request.status_code == requests.codes.unauthorized:
1505
+ self.token = self.__token__()
1506
+ return self.merge_assets(assets, title, description)
1507
+ else:
1508
+ exception = HTTPException(
1509
+ "",
1510
+ request.status_code,
1511
+ request.url,
1512
+ "merge_assets",
1513
+ request.content.decode("utf-8"),
1514
+ )
1515
+ logger.error(exception)
1516
+ raise exception
1517
+
1518
+ def merge_folder(self, folder: Folder) -> str:
1464
1519
  """
1465
1520
  Create a new Asset with the content from each Asset in the Folder
1466
1521
 
1467
- This call will create a new multi-part Asset which contains all the content from the Folder.
1522
+ This call will create a new multipart Asset which contains all the content from the Folder.
1468
1523
 
1469
1524
  The new Asset which is created will have the same title, description and parent as the Folder.
1470
1525
 
@@ -1495,7 +1550,6 @@ class EntityAPI(AuthenticatedAPI):
1495
1550
  logger.error(exception)
1496
1551
  raise exception
1497
1552
 
1498
-
1499
1553
  def asset(self, reference: str) -> Asset:
1500
1554
  """
1501
1555
  Retrieve an Asset by its reference
@@ -2118,7 +2172,7 @@ class EntityAPI(AuthenticatedAPI):
2118
2172
  params=params, headers=headers)
2119
2173
 
2120
2174
  if request.status_code == requests.codes.ok:
2121
- pass
2175
+ return None
2122
2176
  elif request.status_code == requests.codes.unauthorized:
2123
2177
  self.token = self.__token__()
2124
2178
  return self._event_actions(entity, maximum=maximum)
@@ -2262,6 +2316,7 @@ class EntityAPI(AuthenticatedAPI):
2262
2316
  else:
2263
2317
  url = next_url.text
2264
2318
  return PagedSet(result_list, has_more, int(total_hits.text), url)
2319
+ return None
2265
2320
 
2266
2321
  def entity_from_event(self, event_id: str) -> Generator:
2267
2322
  self.token = self.__token__()
@@ -2592,3 +2647,4 @@ class EntityAPI(AuthenticatedAPI):
2592
2647
  "_delete_entity", request.content.decode('utf-8'))
2593
2648
  logger.error(exception)
2594
2649
  raise exception
2650
+
@@ -451,6 +451,26 @@ class MetadataGroupsAPI(AuthenticatedAPI):
451
451
  raise exception
452
452
 
453
453
 
454
+ def delete_form(self, form_id: str):
455
+ """
456
+ Delete a form by its ID
457
+ """
458
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
459
+ url = f'{self.protocol}://{self.server}/api/metadata/forms/{form_id}'
460
+ with self.session.delete(url, headers=headers) as request:
461
+ if request.status_code == requests.codes.unauthorized:
462
+ self.token = self.__token__()
463
+ return self.delete_form(form_id)
464
+ elif request.status_code == requests.codes.no_content:
465
+ return None
466
+ else:
467
+ exception = HTTPException(None, request.status_code, request.url, "delete_form",
468
+ request.content.decode('utf-8'))
469
+ logger.error(exception)
470
+ raise exception
471
+
472
+
473
+
454
474
  def form(self, form_id: str) -> dict:
455
475
  """
456
476
  Return a Form as a JSON dict object
@@ -1633,6 +1633,52 @@ class UploadAPI(AuthenticatedAPI):
1633
1633
  logger.error(exception)
1634
1634
  raise exception
1635
1635
 
1636
+ def clean_upload_bucket(self, bucket_name: str, older_than_days: int = 90):
1637
+ """
1638
+ Clean up objects in an upload bucket which are older than older_than_days.
1639
+
1640
+ """
1641
+ from azure.storage.blob import ContainerClient
1642
+
1643
+ for location in self.upload_locations():
1644
+ if location['containerName'] == bucket_name:
1645
+
1646
+ if location['type'] != 'AWS':
1647
+ credentials = self.upload_credentials(location['apiId'])
1648
+ account_key = credentials['key']
1649
+ session_token = credentials['sessionToken']
1650
+ sas_url = f"https://{account_key}.blob.core.windows.net/{bucket_name}"
1651
+ container = ContainerClient.from_container_url(container_url=sas_url, credential=session_token)
1652
+ now = datetime.now(timezone.utc)
1653
+ for blob in container.list_blobs():
1654
+ if abs((blob.last_modified - now).days) > older_than_days:
1655
+ logger.debug(f"Deleting expired object {blob.name}")
1656
+ container.delete_blob(blob.name)
1657
+
1658
+ if location['type'] == 'AWS':
1659
+ credentials = self.upload_credentials(location['apiId'])
1660
+ access_key = credentials['key']
1661
+ secret_key = credentials['secret']
1662
+ session_token = credentials['sessionToken']
1663
+ session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key,
1664
+ aws_session_token=session_token)
1665
+ s3_client = session.client("s3")
1666
+ paginator = s3_client.get_paginator('list_objects_v2')
1667
+ now = datetime.now(timezone.utc)
1668
+ for page in paginator.paginate(Bucket=bucket_name):
1669
+ if 'Contents' in page:
1670
+ for key in page['Contents']:
1671
+ last_modified = key["LastModified"]
1672
+ if abs((last_modified - now).days) > older_than_days:
1673
+ logger.debug(f"Deleting expired object {key["Key"]}")
1674
+ s3_client.delete_object(Bucket=bucket_name, Key=key["Key"])
1675
+
1676
+
1677
+
1678
+
1679
+
1680
+
1681
+
1636
1682
  def upload_locations(self):
1637
1683
  """
1638
1684
  Upload locations are configured on the Sources page as 'SIP Upload'.
@@ -74,6 +74,7 @@ class TriggerType(Enum):
74
74
  INDEXED = "FULL_TEXT_INDEXED"
75
75
  SECURITY_CHANGED = "CHANGED_SECURITY_DESCRIPTOR"
76
76
  INGEST_FAILED = "INGEST_FAILED"
77
+ CHANGE_ASSET_VISIBILITY = "CHANGE_ASSET_VISIBILITY"
77
78
 
78
79
 
79
80
  class WebHooksAPI(AuthenticatedAPI):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyPreservica
3
- Version: 3.2.2
3
+ Version: 3.2.4
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="3.2.2",
24
+ version="3.2.4",
25
25
  description="Python library for the Preservica API",
26
26
  long_description=README,
27
27
  long_description_content_type="text/markdown",
@@ -1,24 +1,48 @@
1
+ from os.path import isfile
2
+
1
3
  import pytest
2
4
 
3
5
  from zipfile import ZipFile
4
6
  from pyPreservica import *
5
7
 
6
8
 
7
- def test_export_file_wait():
9
+
10
+
11
+ def setup():
12
+ pass
13
+
14
+
15
+ def tear_down(zip_files):
16
+ for f in zip_files:
17
+ if os.path.exists(f):
18
+ os.remove(f)
19
+
20
+
21
+ @pytest.fixture
22
+ def setup_data():
23
+ print("\nSetting up resources...")
24
+ zip_files = []
25
+ setup()
26
+ yield zip_files
27
+ print(f"\nTearing down resources...")
28
+ tear_down(zip_files)
29
+
30
+ def test_export_file_wait(setup_data):
8
31
  client = EntityAPI()
9
32
  asset = client.asset("683f9db7-ff81-4859-9c03-f68cfa5d9c3d")
10
- zip_file = client.export_opex_sync(asset, IncludeContent='true')
33
+ zip_file = client.export_opex_sync(asset, IncludeContent='true', IncludeMetadata='true')
11
34
  assert os.path.exists(zip_file)
35
+ setup_data.append(zip_file)
12
36
  assert 1066650 < os.stat(zip_file).st_size
13
37
  with ZipFile(zip_file, 'r') as zipObj:
14
38
  assert len(zipObj.namelist()) == 6
15
39
  os.remove(zip_file)
16
40
 
17
41
 
18
- def test_export_file_no_wait():
42
+ def test_export_file_no_wait(setup_data):
19
43
  client = EntityAPI()
20
44
  asset = client.asset("683f9db7-ff81-4859-9c03-f68cfa5d9c3d")
21
- pid = client.export_opex_async(asset)
45
+ pid = client.export_opex_async(asset, IncludeContent='true', IncludeMetadata='true')
22
46
  status = "ACTIVE"
23
47
 
24
48
  while status == "ACTIVE":
@@ -28,6 +52,7 @@ def test_export_file_no_wait():
28
52
 
29
53
  zip_file = client.download_opex(pid)
30
54
  assert os.path.exists(zip_file)
55
+ setup_data.append(zip_file)
31
56
  assert 1066650 < os.stat(zip_file).st_size
32
57
  with ZipFile(zip_file, 'r') as zipObj:
33
58
  assert len(zipObj.namelist()) == 6
@@ -38,11 +38,11 @@ def test_get_xml_documents():
38
38
  xml_documents = client.xml_documents()
39
39
  assert type(xml_documents) is list
40
40
  assert len(xml_documents) > 10
41
- for xml in xml_documents:
42
- assert type(xml) is dict
43
- assert "Name" in xml
44
- assert "SchemaUri" in xml
45
- assert "ApiId" in xml
41
+ for x in xml_documents:
42
+ assert type(x) is dict
43
+ assert "Name" in x
44
+ assert "SchemaUri" in x
45
+ assert "ApiId" in x
46
46
 
47
47
 
48
48
  def test_get_xml_document_by_uri():
@@ -73,12 +73,12 @@ def test_get_xml_transforms():
73
73
  xml_transforms = client.xml_transforms()
74
74
  assert type(xml_transforms) is list
75
75
  assert len(xml_transforms) > 10
76
- for xml in xml_transforms:
77
- assert type(xml) is dict
78
- assert "Name" in xml
79
- assert "FromSchemaUri" in xml
80
- assert "ToSchemaUri" in xml
81
- assert "ApiId" in xml
76
+ for x in xml_transforms:
77
+ assert type(x) is dict
78
+ assert "Name" in x
79
+ assert "FromSchemaUri" in x
80
+ assert "ToSchemaUri" in x
81
+ assert "ApiId" in x
82
82
 
83
83
 
84
84
  def test_get_xml_transform_by_uri():
File without changes
File without changes
File without changes