pyPreservica 2.7.3__tar.gz → 2.8.0__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 (49) hide show
  1. {pypreservica-2.7.3 → pypreservica-2.8.0}/PKG-INFO +1 -1
  2. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/__init__.py +2 -2
  3. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/common.py +30 -4
  4. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/contentAPI.py +9 -5
  5. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/entityAPI.py +7 -3
  6. pypreservica-2.8.0/pyPreservica/mdformsAPI.py +300 -0
  7. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/retentionAPI.py +5 -3
  8. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/uploadAPI.py +100 -48
  9. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/workflowAPI.py +7 -3
  10. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica.egg-info/PKG-INFO +1 -1
  11. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica.egg-info/SOURCES.txt +1 -0
  12. {pypreservica-2.7.3 → pypreservica-2.8.0}/setup.py +1 -1
  13. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_crawl_fs.py +1 -1
  14. pypreservica-2.8.0/tests/test_groups.py +8 -0
  15. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_ingest.py +1 -1
  16. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_workflow.py +2 -2
  17. pypreservica-2.7.3/pyPreservica/mdformsAPI.py +0 -100
  18. {pypreservica-2.7.3 → pypreservica-2.8.0}/LICENSE.txt +0 -0
  19. {pypreservica-2.7.3 → pypreservica-2.8.0}/README.md +0 -0
  20. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/adminAPI.py +0 -0
  21. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/authorityAPI.py +0 -0
  22. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/monitorAPI.py +0 -0
  23. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/opex.py +0 -0
  24. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/parAPI.py +0 -0
  25. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica/webHooksAPI.py +0 -0
  26. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica.egg-info/dependency_links.txt +0 -0
  27. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica.egg-info/requires.txt +0 -0
  28. {pypreservica-2.7.3 → pypreservica-2.8.0}/pyPreservica.egg-info/top_level.txt +0 -0
  29. {pypreservica-2.7.3 → pypreservica-2.8.0}/setup.cfg +0 -0
  30. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_authority_records.py +0 -0
  31. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_bitstream.py +0 -0
  32. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_children.py +0 -0
  33. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_content_api.py +0 -0
  34. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_delete.py +0 -0
  35. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_download.py +0 -0
  36. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_entity.py +0 -0
  37. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_export_opex.py +0 -0
  38. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_identifier.py +0 -0
  39. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_integrity_check.py +0 -0
  40. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_metadata.py +0 -0
  41. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_par.py +0 -0
  42. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_replace.py +0 -0
  43. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_retention.py +0 -0
  44. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_schema.py +0 -0
  45. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_security.py +0 -0
  46. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_thumbnail.py +0 -0
  47. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_upload.py +0 -0
  48. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_users.py +0 -0
  49. {pypreservica-2.7.3 → pypreservica-2.8.0}/tests/test_xml_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyPreservica
3
- Version: 2.7.3
3
+ Version: 2.8.0
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -18,11 +18,11 @@ from .adminAPI import AdminAPI
18
18
  from .monitorAPI import MonitorAPI, MonitorCategory, MonitorStatus, MessageStatus
19
19
  from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler
20
20
  from .authorityAPI import AuthorityAPI, Table
21
- from .mdformsAPI import MDFormsAPI
21
+ from .mdformsAPI import MetadataGroupsAPI, GroupFieldType, GroupField, Group
22
22
 
23
23
  __author__ = "James Carr (drjamescarr@gmail.com)"
24
24
 
25
25
  # Version of the pyPreservica package
26
- __version__ = "2.7.3"
26
+ __version__ = "2.8.0"
27
27
 
28
28
  __license__ = "Apache License Version 2.0"
@@ -477,10 +477,10 @@ class Entity:
477
477
  def __repr__(self):
478
478
  return self.__str__()
479
479
 
480
- def has_metadata(self):
480
+ def has_metadata(self) -> bool:
481
481
  return bool(self.metadata)
482
482
 
483
- def metadata_namespaces(self):
483
+ def metadata_namespaces(self) -> list:
484
484
  return list(self.metadata.values())
485
485
 
486
486
 
@@ -714,11 +714,33 @@ class AuthenticatedAPI:
714
714
 
715
715
  return entity_dict
716
716
 
717
+ def edition(self) -> str:
718
+ """
719
+ Return the edition of this tenancy
720
+ """
721
+ if self.major_version < 8 and self.minor_version < 3:
722
+ raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
723
+
724
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
725
+
726
+ response = self.session.get(f'{self.protocol}://{self.server}/api/entitlement/edition', headers=headers)
727
+
728
+ if response.status_code == requests.codes.ok:
729
+ return response.json()['edition']
730
+ elif response.status_code == requests.codes.unauthorized:
731
+ self.token = self.__token__()
732
+ return self.edition()
733
+ else:
734
+ exception = HTTPException("", response.status_code, response.url,
735
+ "edition", response.content.decode('utf-8'))
736
+ logger.error(exception)
737
+ raise exception
738
+
717
739
  def __version_namespace__(self):
718
740
  """
719
741
  Generate version specific namespaces from the server version
720
742
  """
721
- if self.major_version == 7:
743
+ if self.major_version > 6:
722
744
  self.xip_ns = f"{NS_XIP_ROOT}v{self.major_version}.{self.minor_version}"
723
745
  self.entity_ns = f"{NS_ENTITY_ROOT}v{self.major_version}.{self.minor_version}"
724
746
  self.rm_ns = f"{NS_RM_ROOT}v{6}.{2}"
@@ -857,12 +879,16 @@ class AuthenticatedAPI:
857
879
  raise RuntimeError(response.status_code, msg)
858
880
 
859
881
  def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
860
- use_shared_secret: bool = False, two_fa_secret_key: str = None, protocol: str = "https"):
882
+ use_shared_secret: bool = False, two_fa_secret_key: str = None,
883
+ protocol: str = "https", request_hook=None):
861
884
 
862
885
  config = configparser.ConfigParser(interpolation=configparser.Interpolation())
863
886
  config.read('credentials.properties', encoding='utf-8')
864
887
  self.session: Session = requests.Session()
865
888
 
889
+ if request_hook is not None:
890
+ self.session.hooks['response'].append(request_hook)
891
+
866
892
  retries = Retry(
867
893
  total=3,
868
894
  backoff_factor=0.1,
@@ -10,7 +10,7 @@ licence: Apache License 2.0
10
10
  """
11
11
 
12
12
  import csv
13
- from typing import Generator
13
+ from typing import Generator, Callable
14
14
  from pyPreservica.common import *
15
15
 
16
16
  logger = logging.getLogger(__name__)
@@ -19,8 +19,10 @@ logger = logging.getLogger(__name__)
19
19
  class ContentAPI(AuthenticatedAPI):
20
20
 
21
21
  def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False,
22
- two_fa_secret_key: str = None, protocol: str = "https"):
23
- super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
22
+ two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
23
+
24
+ super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
25
+ protocol, request_hook)
24
26
  self.callback = None
25
27
 
26
28
  class SearchResult:
@@ -130,7 +132,8 @@ class ContentAPI(AuthenticatedAPI):
130
132
  logger.error(f"indexed_fields failed with error code: {results.status_code}")
131
133
  raise RuntimeError(results.status_code, f"indexed_fields failed with error code: {results.status_code}")
132
134
 
133
- def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv", list_indexes: list = None):
135
+ def simple_search_csv(self, query: str = "%", page_size: int = 50, csv_file="search.csv",
136
+ list_indexes: list = None):
134
137
  if list_indexes is None or len(list_indexes) == 0:
135
138
  metadata_fields = ["xip.reference", "xip.title", "xip.description", "xip.document_type",
136
139
  "xip.parent_ref", "xip.security_descriptor"]
@@ -193,7 +196,8 @@ class ContentAPI(AuthenticatedAPI):
193
196
  logger.error(f"search failed with error code: {results.status_code}")
194
197
  raise RuntimeError(results.status_code, f"simple_search failed with error code: {results.status_code}")
195
198
 
196
- def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50, filter_values: dict = None,
199
+ def search_index_filter_csv(self, query: str = "%", csv_file="search.csv", page_size: int = 50,
200
+ filter_values: dict = None,
197
201
  sort_values: dict = None):
198
202
  if filter_values is None:
199
203
  filter_values = {}
@@ -15,7 +15,7 @@ import xml.etree.ElementTree
15
15
  from datetime import datetime, timedelta, timezone
16
16
  from io import BytesIO
17
17
  from time import sleep
18
- from typing import Any, Generator, Tuple, Iterable, Union
18
+ from typing import Any, Generator, Tuple, Iterable, Union, Callable
19
19
 
20
20
  from pyPreservica.common import *
21
21
 
@@ -34,8 +34,12 @@ class EntityAPI(AuthenticatedAPI):
34
34
  """
35
35
 
36
36
  def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
37
- use_shared_secret: bool = False, two_fa_secret_key: str = None, protocol: str = "https"):
38
- super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
37
+ use_shared_secret: bool = False, two_fa_secret_key: str = None,
38
+ protocol: str = "https", request_hook: Callable = None):
39
+
40
+ super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
41
+ protocol, request_hook)
42
+
39
43
  xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
40
44
  xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
41
45
 
@@ -0,0 +1,300 @@
1
+ """
2
+ pyPreservica MDFormsAPI module definition
3
+
4
+ A client library for the Preservica Repository web services Metadata API
5
+ https://demo.preservica.com/api/metadata/documentation.html
6
+
7
+ author: James Carr
8
+ licence: Apache License 2.0
9
+
10
+ """
11
+ import json
12
+ import xml.etree.ElementTree
13
+ from typing import Callable, List
14
+
15
+ from pyPreservica.common import *
16
+
17
+
18
+ class GroupFieldType(Enum):
19
+ STRING = "STRING"
20
+ LONG_STRING = "LONGSTRING"
21
+ DATE = "DATE"
22
+ NUMBER = "NUMBER"
23
+
24
+
25
+ class GroupField:
26
+ field_id: str
27
+ name: str
28
+ field_type: GroupFieldType
29
+ maxLength: int
30
+ default: str
31
+ visible: bool
32
+ editable: bool
33
+ minOccurs: int
34
+ maxOccurs: int
35
+ values: List[str]
36
+ indexed: bool
37
+
38
+ def __init__(self, field_id: str, name: str, field_type: GroupFieldType = GroupFieldType.STRING,
39
+ maxLength: int = -1, default: str = "", visible: bool = True, editable: bool = True,
40
+ minOccurs: int = 0, maxOccurs: int = 1, indexed: bool = True, values: List = None):
41
+ self.field_id = field_id
42
+ self.name = name
43
+ self.field_type = field_type
44
+ self.maxLength = maxLength
45
+ self.default = default
46
+ self.visible = visible
47
+ self.editable = editable
48
+ self.minOccurs = minOccurs
49
+ self.maxOccurs = maxOccurs
50
+ self.values = values
51
+ self.indexed = indexed
52
+
53
+ def __str__(self):
54
+ return (f"Field ID: {self.field_id}\n" + f"Field Name: {self.name}\n" + f"Field Type: {self.field_type}\n" +
55
+ f"Field Visible: {self.visible}\n" + f"Field Editable: {self.editable}\n")
56
+
57
+
58
+ class Group:
59
+ group_id: str
60
+ name: str
61
+ description: str
62
+ schemaUri: str
63
+ fields: List[GroupField] = []
64
+
65
+ def __str__(self):
66
+ return (f"Group ID: {self.group_id}\n" + f"Group Name: {self.name}\n" +
67
+ f"Group Description: {self.description}\n" + f"Group Schema URI: {self.schemaUri}")
68
+
69
+
70
+ class MetadataGroupsAPI(AuthenticatedAPI):
71
+ def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
72
+ use_shared_secret: bool = False, two_fa_secret_key: str = None,
73
+ protocol: str = "https", request_hook: Callable = None):
74
+
75
+ super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
76
+ protocol, request_hook)
77
+
78
+ xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
79
+ xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
80
+
81
+ def delete_group_namespace(self, schema: str):
82
+ """
83
+ Delete a new Metadata Group using its schema URI
84
+
85
+ :param schema: The Group namespace schema URI
86
+ :type schema: str
87
+
88
+ :return: None
89
+ :rtype: None
90
+
91
+ """
92
+ for group in self.groups():
93
+ if group.schemaUri == schema:
94
+ self.delete_group(group.group_id)
95
+
96
+ def delete_group(self, group_id: str):
97
+ """
98
+ Delete a new Metadata Group using its ID
99
+
100
+ :param group_id: Group ID
101
+ :type group_id: str
102
+
103
+ :return: None
104
+ :rtype: None
105
+
106
+ """
107
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
108
+ url = f'{self.protocol}://{self.server}/api/metadata/groups/{group_id}'
109
+ with self.session.delete(url, headers=headers) as request:
110
+ if request.status_code == requests.codes.unauthorized:
111
+ self.token = self.__token__()
112
+ return self.delete_group(group_id)
113
+ elif request.status_code == requests.codes.no_content:
114
+ return None
115
+ else:
116
+ exception = HTTPException(None, request.status_code, request.url, "delete_group",
117
+ request.content.decode('utf-8'))
118
+ logger.error(exception)
119
+ raise exception
120
+
121
+ def add_group(self, group_name: str, group_description: str, fields: List[GroupField]):
122
+ """
123
+ Create a new Metadata Group GroupFields
124
+
125
+ :param group_name: The name of the new Group
126
+ :type group_name: str
127
+
128
+ :param group_description: The description of the new Group
129
+ :type group_description: str
130
+
131
+ :param fields: The list of fields
132
+ :type fields: List[GroupField]
133
+
134
+ :return: JSON document
135
+ :rtype: str
136
+
137
+ """
138
+
139
+ fields_str_list = []
140
+ for field in fields:
141
+
142
+ values = ""
143
+ if (field.values is not None) and (len(field.values) > 0):
144
+ values = f"""\n"values" : [ {",".join('"' + item + '"' for item in field.values)} ],
145
+ """
146
+
147
+ fields_str = f"""{{
148
+ "id" : "{field.field_id}",
149
+ "name": "{field.name}",
150
+ "type": "{field.field_type.value}",
151
+ "minOccurs": "{field.minOccurs}",
152
+ "maxOccurs": "{field.maxOccurs}",
153
+ "visible": "{field.visible}", {values}
154
+ "editable": "{field.editable}",
155
+ "defaultValue": "{field.default}",
156
+ "indexed": "{field.indexed}"
157
+ }}"""
158
+
159
+ fields_str_list.append(fields_str)
160
+
161
+ json_doc = f"""{{
162
+ "name" : "{group_name}",
163
+ "description" : "{group_description}",
164
+ "fields" : [ {",".join(fields_str_list)} ]
165
+ }}"""
166
+
167
+ print(json_doc)
168
+
169
+ json_response = self.add_group_json(json_doc)
170
+ group_id = json_response['id']
171
+ return self.group(group_id)
172
+
173
+ def add_group_json(self, json_document: str) -> str:
174
+ """
175
+ Create a new Metadata Group using a JSON document
176
+
177
+ :param json_document: JSON document
178
+ :type json_document: str
179
+
180
+ :return: JSON document
181
+ :rtype: str
182
+
183
+ """
184
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
185
+ url = f'{self.protocol}://{self.server}/api/metadata/groups/'
186
+ with self.session.post(url, headers=headers, data=json_document) as request:
187
+ if request.status_code == requests.codes.unauthorized:
188
+ self.token = self.__token__()
189
+ return self.add_group_json(json_document)
190
+ elif request.status_code == requests.codes.created:
191
+ return json.loads(str(request.content.decode('utf-8')))
192
+ else:
193
+ exception = HTTPException(None, request.status_code, request.url, "add_group_json",
194
+ request.content.decode('utf-8'))
195
+ logger.error(exception)
196
+ raise exception
197
+
198
+ def group_json(self, group_id: str) -> str:
199
+ """
200
+ Return a Group as a JSON document
201
+
202
+ :param group_id: The Group id
203
+ :type group_id: str
204
+
205
+ :return: JSON document
206
+ :rtype: str
207
+
208
+ """
209
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
210
+ url = f'{self.protocol}://{self.server}/api/metadata/groups/{group_id}'
211
+ with self.session.get(url, headers=headers) as request:
212
+ if request.status_code == requests.codes.unauthorized:
213
+ self.token = self.__token__()
214
+ return self.group_json(group_id)
215
+ elif request.status_code == requests.codes.ok:
216
+ return str(request.content.decode('utf-8'))
217
+ else:
218
+ exception = HTTPException(None, request.status_code, request.url, "group_json",
219
+ request.content.decode('utf-8'))
220
+ logger.error(exception)
221
+ raise exception
222
+
223
+ def group(self, group_id: str) -> Group:
224
+ """
225
+ Return a Group object by its id
226
+
227
+ :param group_id: The Group id
228
+ :type group_id: str
229
+
230
+ :return: The Group
231
+ :rtype: Group
232
+
233
+ """
234
+ json_group = json.loads(self.group_json(group_id))
235
+ group: Group = Group()
236
+ group.group_id = json_group['id']
237
+ group.name = json_group['name']
238
+ group.description = json_group['description']
239
+ group.schemaUri = json_group['schemaUri']
240
+
241
+ for f in json_group['fields']:
242
+ gf: GroupField = GroupField(f['id'], f['name'])
243
+ if 'type' in f:
244
+ gf.field_type = f['type']
245
+ if 'visible' in f:
246
+ gf.visible = f['visible']
247
+ if 'editable' in f:
248
+ gf.editable = f['editable']
249
+ if 'indexed' in f:
250
+ gf.indexed = f['indexed']
251
+ if 'maxOccurs' in f:
252
+ gf.maxOccurs = f['maxOccurs']
253
+ if 'minOccurs' in f:
254
+ gf.minOccurs = f['minOccurs']
255
+ if 'values' in f:
256
+ gf.values = f['values']
257
+ group.fields.append(gf)
258
+ return group
259
+
260
+ def groups_json(self) -> str:
261
+ """
262
+ Return all the groups in the tenancy as a single json document
263
+
264
+ :return: JSON document
265
+ :rtype: str
266
+
267
+ """
268
+
269
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
270
+ url = f'{self.protocol}://{self.server}/api/metadata/groups'
271
+ with self.session.get(url, headers=headers) as request:
272
+ if request.status_code == requests.codes.unauthorized:
273
+ self.token = self.__token__()
274
+ return self.groups_json()
275
+ elif request.status_code == requests.codes.ok:
276
+ return str(request.content.decode('utf-8'))
277
+ else:
278
+ exception = HTTPException(None, request.status_code, request.url, "groups_json",
279
+ request.content.decode('utf-8'))
280
+ logger.error(exception)
281
+ raise exception
282
+
283
+ def groups(self) -> List[Group]:
284
+ """
285
+ Return all the groups in the tenancy
286
+
287
+ :return: list of Groups
288
+ :rtype: List[Group]
289
+
290
+ """
291
+ groups = json.loads(self.groups_json())['groups']
292
+ return_groups = []
293
+ for g in groups:
294
+ group: Group = Group()
295
+ group.group_id = g['id']
296
+ group.name = g['name']
297
+ group.description = g['description']
298
+ group.schemaUri = g['schemaUri']
299
+ return_groups.append(group)
300
+ return return_groups
@@ -11,7 +11,7 @@ licence: Apache License 2.0
11
11
 
12
12
 
13
13
  import xml.etree.ElementTree
14
- from typing import Set
14
+ from typing import Set, Callable
15
15
 
16
16
  from pyPreservica.common import *
17
17
 
@@ -59,8 +59,10 @@ class RetentionPolicy:
59
59
  class RetentionAPI(AuthenticatedAPI):
60
60
 
61
61
  def __init__(self, username=None, password=None, tenant=None, server=None, use_shared_secret=False,
62
- two_fa_secret_key: str = None, protocol: str = "https"):
63
- super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
62
+ two_fa_secret_key: str = None, protocol: str = "https", request_hook: Callable = None):
63
+
64
+ super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
65
+ protocol, request_hook)
64
66
  if self.major_version < 7 and self.minor_version < 2:
65
67
  raise RuntimeError("Retention API is only available when connected to a v6.2 System")
66
68
 
@@ -80,7 +80,8 @@ class PutObjectTask(s3transfer.tasks.Task):
80
80
  class CompleteMultipartUploadTask(s3transfer.tasks.Task):
81
81
  # Copied from s3transfer/tasks.py, changed to return a result.
82
82
  def _main(self, client, bucket, key, upload_id, parts, extra_args):
83
- return client.complete_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id, MultipartUpload={"Parts": parts},
83
+ return client.complete_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id,
84
+ MultipartUpload={"Parts": parts},
84
85
  **extra_args, )
85
86
 
86
87
 
@@ -224,7 +225,8 @@ def __make_representation_multiple_co__(xip, rep_name, rep_type, rep_files, io_r
224
225
  return refs_dict
225
226
 
226
227
 
227
- def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None, additional_namespaces=None):
228
+ def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
229
+ additional_namespaces=None):
228
230
  """
229
231
  Create a custom CMIS transform to display metadata within UA.
230
232
 
@@ -242,7 +244,8 @@ def cvs_to_cmis_xslt(csv_file, xml_namespace, root_element, title="Metadata Titl
242
244
 
243
245
  namespaces = {"version": "2.0", "xmlns:xsl": "http://www.w3.org/1999/XSL/Transform",
244
246
  "xmlns:fn": "http://www.w3.org/2005/xpath-functions", "xmlns:xs": "http://www.w3.org/2001/XMLSchema",
245
- "xmlns:csv": xml_namespace, "xmlns": "http://www.tessella.com/sdb/cmis/metadata", "exclude-result-prefixes": "csv"}
247
+ "xmlns:csv": xml_namespace, "xmlns": "http://www.tessella.com/sdb/cmis/metadata",
248
+ "exclude-result-prefixes": "csv"}
246
249
 
247
250
  if additional_namespaces is not None:
248
251
  for prefix, uri in additional_namespaces.items():
@@ -311,7 +314,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
311
314
  headers.add(xml_tag)
312
315
  break
313
316
 
314
- namespaces = {"xmlns:xs": "http://www.w3.org/2001/XMLSchema", "attributeFormDefault": "unqualified", "elementFormDefault": "qualified",
317
+ namespaces = {"xmlns:xs": "http://www.w3.org/2001/XMLSchema", "attributeFormDefault": "unqualified",
318
+ "elementFormDefault": "qualified",
315
319
  "targetNamespace": xml_namespace}
316
320
 
317
321
  if additional_namespaces is not None:
@@ -333,7 +337,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
333
337
  prefix, sep, tag = header.partition(":")
334
338
  try:
335
339
  namespace = additional_namespaces[prefix]
336
- xml.etree.ElementTree.SubElement(xml_sequence, "xs:element", {"ref": header, "xmlns:" + prefix: namespace})
340
+ xml.etree.ElementTree.SubElement(xml_sequence, "xs:element",
341
+ {"ref": header, "xmlns:" + prefix: namespace})
337
342
  except KeyError:
338
343
  xml.etree.ElementTree.SubElement(xml_sequence, "xs:element", {"type": "xs:string", "name": header})
339
344
  else:
@@ -350,7 +355,8 @@ def cvs_to_xsd(csv_file, xml_namespace, root_element, export_folder=None, additi
350
355
  return xsd_file
351
356
 
352
357
 
353
- def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None, additional_namespaces=None):
358
+ def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Title", export_folder=None,
359
+ additional_namespaces=None):
354
360
  """
355
361
  Create a custom Preservica search index based on the columns in a csv file
356
362
 
@@ -400,7 +406,8 @@ def csv_to_search_xml(csv_file, xml_namespace, root_element, title="Metadata Tit
400
406
  return search_xml
401
407
 
402
408
 
403
- def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename", export_folder=None, additional_namespaces=None):
409
+ def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename", export_folder=None,
410
+ additional_namespaces=None):
404
411
  """
405
412
  Export the rows of a CSV file as XML metadata documents which can be added to Preservica assets
406
413
 
@@ -451,7 +458,8 @@ def cvs_to_xml(csv_file, xml_namespace, root_element, file_name_column="filename
451
458
  yield name
452
459
 
453
460
 
454
- def generic_asset_package(preservation_files_dict=None, access_files_dict=None, export_folder=None, parent_folder=None, compress=True,
461
+ def generic_asset_package(preservation_files_dict=None, access_files_dict=None, export_folder=None, parent_folder=None,
462
+ compress=True,
455
463
  **kwargs):
456
464
  # some basic validation
457
465
  if export_folder is None:
@@ -501,8 +509,10 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
501
509
  if has_preservation_files:
502
510
  for representation_name in preservation_files_dict.keys():
503
511
  preservation_files_list = preservation_files_dict[representation_name]
504
- preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name, rep_type="Preservation",
505
- rep_files=preservation_files_list, io_ref=io_ref)
512
+ preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
513
+ rep_type="Preservation",
514
+ rep_files=preservation_files_list,
515
+ io_ref=io_ref)
506
516
  preservation_representation_refs_dict[representation_name] = preservation_refs_dict
507
517
 
508
518
  if has_access_files:
@@ -519,13 +529,16 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
519
529
  default_content_objects_title = os.path.splitext(os.path.basename(filename))[0]
520
530
 
521
531
  preservation_content_title = kwargs.get('Preservation_Content_Title', default_content_objects_title)
522
- preservation_content_description = kwargs.get('Preservation_Content_Description', default_content_objects_title)
532
+ preservation_content_description = kwargs.get('Preservation_Content_Description',
533
+ default_content_objects_title)
523
534
 
524
535
  if isinstance(preservation_content_title, dict):
525
- preservation_content_title = preservation_content_title.get("filename", default_content_objects_title)
536
+ preservation_content_title = preservation_content_title.get("filename",
537
+ default_content_objects_title)
526
538
 
527
539
  if isinstance(preservation_content_description, dict):
528
- preservation_content_description = preservation_content_description.get("filename", default_content_objects_title)
540
+ preservation_content_description = preservation_content_description.get("filename",
541
+ default_content_objects_title)
529
542
 
530
543
  __make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag,
531
544
  preservation_content_description, content_type)
@@ -545,7 +558,8 @@ def generic_asset_package(preservation_files_dict=None, access_files_dict=None,
545
558
  if isinstance(access_content_description, dict):
546
559
  access_content_description = access_content_title.get("filename", default_content_objects_title)
547
560
 
548
- __make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag, access_content_description,
561
+ __make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
562
+ access_content_description,
549
563
  content_type)
550
564
 
551
565
  if has_preservation_files:
@@ -816,7 +830,8 @@ def multi_asset_package(asset_file_list=None, export_folder=None, parent_folder=
816
830
  return top_level_folder + ".zip"
817
831
 
818
832
 
819
- def complex_asset_package(preservation_files_list=None, access_files_list=None, export_folder=None, parent_folder=None, compress=True,
833
+ def complex_asset_package(preservation_files_list=None, access_files_list=None, export_folder=None, parent_folder=None,
834
+ compress=True,
820
835
  **kwargs):
821
836
  """
822
837
 
@@ -909,13 +924,15 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
909
924
  if has_preservation_files:
910
925
  # add the content objects
911
926
  representation_name = kwargs.get('Preservation_Representation_Name', "Preservation")
912
- preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name, rep_type="Preservation",
927
+ preservation_refs_dict = __make_representation_multiple_co__(xip, rep_name=representation_name,
928
+ rep_type="Preservation",
913
929
  rep_files=preservation_files_list, io_ref=io_ref)
914
930
 
915
931
  if has_access_files:
916
932
  # add the content objects
917
933
  access_name = kwargs.get('Access_Representation_Name', "Access")
918
- access_refs_dict = __make_representation_multiple_co__(xip, rep_name=access_name, rep_type="Access", rep_files=access_files_list,
934
+ access_refs_dict = __make_representation_multiple_co__(xip, rep_name=access_name, rep_type="Access",
935
+ rep_files=access_files_list,
919
936
  io_ref=io_ref)
920
937
 
921
938
  if has_preservation_files:
@@ -923,7 +940,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
923
940
  for content_ref, filename in preservation_refs_dict.items():
924
941
  default_content_objects_title = os.path.splitext(os.path.basename(filename))[0]
925
942
  preservation_content_title = kwargs.get('Preservation_Content_Title', default_content_objects_title)
926
- preservation_content_description = kwargs.get('Preservation_Content_Description', default_content_objects_title)
943
+ preservation_content_description = kwargs.get('Preservation_Content_Description',
944
+ default_content_objects_title)
927
945
 
928
946
  if isinstance(preservation_content_title, dict):
929
947
  preservation_content_title = preservation_content_title[filename]
@@ -931,7 +949,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
931
949
  if isinstance(preservation_content_description, dict):
932
950
  preservation_content_description = preservation_content_description[filename]
933
951
 
934
- __make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag, preservation_content_description,
952
+ __make_content_objects__(xip, preservation_content_title, content_ref, io_ref, security_tag,
953
+ preservation_content_description,
935
954
  content_type)
936
955
 
937
956
  if has_access_files:
@@ -948,7 +967,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
948
967
  if isinstance(access_content_description, dict):
949
968
  access_content_title = access_content_title[filename]
950
969
 
951
- __make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag, access_content_description, content_type)
970
+ __make_content_objects__(xip, access_content_title, content_ref, io_ref, security_tag,
971
+ access_content_description, content_type)
952
972
 
953
973
  if has_preservation_files:
954
974
 
@@ -956,7 +976,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
956
976
 
957
977
  for content_ref, filename in preservation_refs_dict.items():
958
978
  preservation_file_name = os.path.basename(filename)
959
- __make_generation__(xip, preservation_file_name, content_ref, preservation_generation_label, PRESERVATION_CONTENT_FOLDER)
979
+ __make_generation__(xip, preservation_file_name, content_ref, preservation_generation_label,
980
+ PRESERVATION_CONTENT_FOLDER)
960
981
 
961
982
  if has_access_files:
962
983
 
@@ -1071,7 +1092,8 @@ def complex_asset_package(preservation_files_list=None, access_files_list=None,
1071
1092
  return top_level_folder + ".zip"
1072
1093
 
1073
1094
 
1074
- def simple_asset_package(preservation_file=None, access_file=None, export_folder=None, parent_folder=None, compress=True, **kwargs):
1095
+ def simple_asset_package(preservation_file=None, access_file=None, export_folder=None, parent_folder=None,
1096
+ compress=True, **kwargs):
1075
1097
  """
1076
1098
  Create a Preservica package containing a single Asset from a single preservation file
1077
1099
  and an optional access file.
@@ -1131,7 +1153,8 @@ def _unpad(s):
1131
1153
 
1132
1154
  class UploadAPI(AuthenticatedAPI):
1133
1155
 
1134
- def ingest_tweet(self, twitter_user=None, tweet_id: int = 0, twitter_consumer_key=None, twitter_secret_key=None, folder=None,
1156
+ def ingest_tweet(self, twitter_user=None, tweet_id: int = 0, twitter_consumer_key=None, twitter_secret_key=None,
1157
+ folder=None,
1135
1158
  callback=None, **kwargs):
1136
1159
 
1137
1160
  """
@@ -1175,7 +1198,8 @@ class UploadAPI(AuthenticatedAPI):
1175
1198
  video_name_document_.close()
1176
1199
  return video_name_, True
1177
1200
 
1178
- entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server, tenant=self.tenant)
1201
+ entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
1202
+ tenant=self.tenant)
1179
1203
  if hasattr(folder, "reference"):
1180
1204
  folder = entity_client.folder(folder.reference)
1181
1205
  else:
@@ -1313,7 +1337,8 @@ class UploadAPI(AuthenticatedAPI):
1313
1337
  os.remove(ob)
1314
1338
  os.remove("metadata.xml")
1315
1339
 
1316
- def ingest_twitter_feed(self, twitter_user=None, num_tweets: int = 25, twitter_consumer_key=None, twitter_secret_key=None, folder=None,
1340
+ def ingest_twitter_feed(self, twitter_user=None, num_tweets: int = 25, twitter_consumer_key=None,
1341
+ twitter_secret_key=None, folder=None,
1317
1342
  callback=None, **kwargs):
1318
1343
 
1319
1344
  """
@@ -1358,7 +1383,8 @@ class UploadAPI(AuthenticatedAPI):
1358
1383
  video_name_document_.flush()
1359
1384
  return video_name_, True
1360
1385
 
1361
- entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server, tenant=self.tenant)
1386
+ entity_client = pyPreservica.EntityAPI(username=self.username, password=self.password, server=self.server,
1387
+ tenant=self.tenant)
1362
1388
  if hasattr(folder, "reference"):
1363
1389
  folder = entity_client.folder(folder.reference)
1364
1390
  else:
@@ -1489,7 +1515,8 @@ class UploadAPI(AuthenticatedAPI):
1489
1515
  asset_title = kwargs.get("Title", text)
1490
1516
  asset_description = kwargs.get("Description", full_text)
1491
1517
 
1492
- p = complex_asset_package(preservation_files_list=content_objects, parent_folder=folder, Title=asset_title,
1518
+ p = complex_asset_package(preservation_files_list=content_objects, parent_folder=folder,
1519
+ Title=asset_title,
1493
1520
  Description=asset_description, CustomType="Tweet", Identifiers=identifiers,
1494
1521
  Asset_Metadata=asset_metadata, SecurityTag=security_tag)
1495
1522
  self.upload_zip_package(p, folder=folder, callback=callback)
@@ -1573,7 +1600,8 @@ class UploadAPI(AuthenticatedAPI):
1573
1600
  duration = meta.get('duration')
1574
1601
 
1575
1602
  package = simple_asset_package(preservation_file=f"{vid_id}.mp4", parent_folder=parent_folder, Title=title,
1576
- Description=description, Identifiers=identifier_map, Asset_Metadata=descriptive_metadata,
1603
+ Description=description, Identifiers=identifier_map,
1604
+ Asset_Metadata=descriptive_metadata,
1577
1605
  Preservation_Content_Title=title, SecurityTag=security_tag)
1578
1606
 
1579
1607
  self.upload_zip_package(path_to_zip_package=package, folder=parent_folder, callback=callback)
@@ -1594,7 +1622,8 @@ class UploadAPI(AuthenticatedAPI):
1594
1622
  self.token = self.__token__()
1595
1623
  return self.upload_credentials(location_id)
1596
1624
  else:
1597
- exception = HTTPException(location_id, request.status_code, request.url, "upload_credentials", request.content.decode('utf-8'))
1625
+ exception = HTTPException(location_id, request.status_code, request.url, "upload_credentials",
1626
+ request.content.decode('utf-8'))
1598
1627
  logger.error(exception)
1599
1628
  raise exception
1600
1629
 
@@ -1613,7 +1642,8 @@ class UploadAPI(AuthenticatedAPI):
1613
1642
  self.token = self.__token__()
1614
1643
  return self.upload_locations()
1615
1644
  else:
1616
- exception = HTTPException("", request.status_code, request.url, "upload_locations", request.content.decode('utf-8'))
1645
+ exception = HTTPException("", request.status_code, request.url, "upload_locations",
1646
+ request.content.decode('utf-8'))
1617
1647
  logger.error(exception)
1618
1648
  raise exception
1619
1649
 
@@ -1625,7 +1655,8 @@ class UploadAPI(AuthenticatedAPI):
1625
1655
  """
1626
1656
  return self.upload_locations()
1627
1657
 
1628
- def crawl_filesystem(self, filesystem_path, bucket_name, preservica_parent, callback: bool = False, security_tag: str = "open",
1658
+ def crawl_filesystem(self, filesystem_path, bucket_name, preservica_parent, callback: bool = False,
1659
+ security_tag: str = "open",
1629
1660
  delete_after_upload: bool = True, max_MB_ingested: int = -1):
1630
1661
 
1631
1662
  def get_parent(client, identifier, parent_reference):
@@ -1652,8 +1683,10 @@ class UploadAPI(AuthenticatedAPI):
1652
1683
  return folder
1653
1684
 
1654
1685
  from pyPreservica import EntityAPI
1655
- entity_client = EntityAPI(username=self.username, password=self.password, server=self.server, tenant=self.tenant,
1656
- two_fa_secret_key=self.two_fa_secret_key, use_shared_secret=self.shared_secret, protocol=self.protocol)
1686
+ entity_client = EntityAPI(username=self.username, password=self.password, server=self.server,
1687
+ tenant=self.tenant,
1688
+ two_fa_secret_key=self.two_fa_secret_key, use_shared_secret=self.shared_secret,
1689
+ protocol=self.protocol)
1657
1690
 
1658
1691
  if preservica_parent:
1659
1692
  parent = entity_client.folder(preservica_parent)
@@ -1697,8 +1730,14 @@ class UploadAPI(AuthenticatedAPI):
1697
1730
  else:
1698
1731
  progress_display = None
1699
1732
 
1700
- self.upload_zip_package_to_S3(path_to_zip_package=package, bucket_name=bucket_name, callback=progress_display,
1701
- delete_after_upload=delete_after_upload)
1733
+ if bucket_name is None:
1734
+ self.upload_zip_package(path_to_zip_package=package, callback=progress_display,
1735
+ delete_after_upload=delete_after_upload)
1736
+ else:
1737
+ self.upload_zip_package_to_S3(path_to_zip_package=package, bucket_name=bucket_name,
1738
+ callback=progress_display,
1739
+ delete_after_upload=delete_after_upload)
1740
+
1702
1741
  logger.info(f"Uploaded " + "{:.1f}".format(bytes_ingested / (1024 * 1024)) + " MB")
1703
1742
 
1704
1743
  if max_MB_ingested > 0:
@@ -1706,7 +1745,8 @@ class UploadAPI(AuthenticatedAPI):
1706
1745
  logger.info(f"Reached Max Upload Limit")
1707
1746
  break
1708
1747
 
1709
- def upload_zip_to_Source(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False, show_progress=False):
1748
+ def upload_zip_to_Source(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
1749
+ show_progress=False):
1710
1750
 
1711
1751
  """
1712
1752
  Uploads a zip file package to either an Azure container or S3 bucket
@@ -1727,13 +1767,17 @@ class UploadAPI(AuthenticatedAPI):
1727
1767
  callback = None
1728
1768
  if show_progress:
1729
1769
  callback = UploadProgressConsoleCallback(path_to_zip_package)
1730
- self.upload_zip_package_to_S3(path_to_zip_package=path_to_zip_package, bucket_name=container_name, folder=folder,
1770
+ self.upload_zip_package_to_S3(path_to_zip_package=path_to_zip_package, bucket_name=container_name,
1771
+ folder=folder,
1731
1772
  callback=callback, delete_after_upload=delete_after_upload)
1732
1773
  else:
1733
- self.upload_zip_package_to_Azure(path_to_zip_package=path_to_zip_package, container_name=container_name, folder=folder,
1734
- delete_after_upload=delete_after_upload, show_progress=show_progress)
1774
+ self.upload_zip_package_to_Azure(path_to_zip_package=path_to_zip_package,
1775
+ container_name=container_name, folder=folder,
1776
+ delete_after_upload=delete_after_upload,
1777
+ show_progress=show_progress)
1735
1778
 
1736
- def upload_zip_package_to_Azure(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False, show_progress=False):
1779
+ def upload_zip_package_to_Azure(self, path_to_zip_package, container_name, folder=None, delete_after_upload=False,
1780
+ show_progress=False):
1737
1781
 
1738
1782
  """
1739
1783
  Uploads a zip file package to an Azure container connected to a Preservica Cloud System
@@ -1746,7 +1790,8 @@ class UploadAPI(AuthenticatedAPI):
1746
1790
  """
1747
1791
 
1748
1792
  if (self.major_version < 7) and (self.minor_version < 5):
1749
- raise RuntimeError("This call [upload_zip_package_to_Azure] is only available against v6.5 systems and above")
1793
+ raise RuntimeError(
1794
+ "This call [upload_zip_package_to_Azure] is only available against v6.5 systems and above")
1750
1795
 
1751
1796
  from azure.storage.blob import ContainerClient
1752
1797
 
@@ -1774,11 +1819,13 @@ class UploadAPI(AuthenticatedAPI):
1774
1819
 
1775
1820
  if show_progress:
1776
1821
  with tqdm.wrapattr(open(path_to_zip_package, 'rb'), "read", total=len_bytes) as data:
1777
- blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata, length=len_bytes)
1822
+ blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
1823
+ length=len_bytes)
1778
1824
  properties = blob_client.get_blob_properties()
1779
1825
  else:
1780
1826
  with open(path_to_zip_package, "rb") as data:
1781
- blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata, length=len_bytes)
1827
+ blob_client = container.upload_blob(name=upload_key, data=data, metadata=metadata,
1828
+ length=len_bytes)
1782
1829
  properties = blob_client.get_blob_properties()
1783
1830
 
1784
1831
  if delete_after_upload:
@@ -1786,7 +1833,8 @@ class UploadAPI(AuthenticatedAPI):
1786
1833
 
1787
1834
  return properties
1788
1835
 
1789
- def upload_zip_package_to_S3(self, path_to_zip_package, bucket_name, folder=None, callback=None, delete_after_upload=False):
1836
+ def upload_zip_package_to_S3(self, path_to_zip_package, bucket_name, folder=None, callback=None,
1837
+ delete_after_upload=False):
1790
1838
 
1791
1839
  """
1792
1840
  Uploads a zip file package to an S3 bucket connected to a Preservica Cloud System
@@ -1815,7 +1863,8 @@ class UploadAPI(AuthenticatedAPI):
1815
1863
  session_token = credentials['sessionToken']
1816
1864
  endpoint = credentials['endpoint']
1817
1865
 
1818
- session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key, aws_session_token=session_token)
1866
+ session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key,
1867
+ aws_session_token=session_token)
1819
1868
  s3 = session.resource(service_name="s3")
1820
1869
 
1821
1870
  logger.debug(f"S3 Session: {s3}")
@@ -1834,7 +1883,8 @@ class UploadAPI(AuthenticatedAPI):
1834
1883
 
1835
1884
  metadata_map = {'Metadata': metadata}
1836
1885
 
1837
- s3_object.upload_file(path_to_zip_package, Callback=callback, ExtraArgs=metadata_map, Config=transfer_config)
1886
+ s3_object.upload_file(path_to_zip_package, Callback=callback, ExtraArgs=metadata_map,
1887
+ Config=transfer_config)
1838
1888
 
1839
1889
  if delete_after_upload:
1840
1890
  os.remove(path_to_zip_package)
@@ -1860,7 +1910,8 @@ class UploadAPI(AuthenticatedAPI):
1860
1910
  endpoint = f'{self.protocol}://{self.server}/api/s3/buckets'
1861
1911
  self.token = self.__token__()
1862
1912
 
1863
- s3_client = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=self.token, aws_secret_access_key="NOT_USED",
1913
+ s3_client = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=self.token,
1914
+ aws_secret_access_key="NOT_USED",
1864
1915
  config=Config(s3={'addressing_style': 'path'}))
1865
1916
 
1866
1917
  metadata = {}
@@ -1880,7 +1931,8 @@ class UploadAPI(AuthenticatedAPI):
1880
1931
  transfer.CompleteMultipartUploadTask = CompleteMultipartUploadTask
1881
1932
  transfer.upload_file = upload_file
1882
1933
 
1883
- response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket, key=key_id, extra_args=metadata,
1934
+ response = transfer.upload_file(self=transfer, filename=path_to_zip_package, bucket=bucket, key=key_id,
1935
+ extra_args=metadata,
1884
1936
  callback=callback)
1885
1937
 
1886
1938
  if delete_after_upload:
@@ -11,6 +11,7 @@ licence: Apache License 2.0
11
11
 
12
12
  import uuid
13
13
  import datetime
14
+ from typing import Callable
14
15
  from xml.etree import ElementTree
15
16
 
16
17
  from pyPreservica.common import *
@@ -21,7 +22,7 @@ logger = logging.getLogger(__name__)
21
22
  class WorkflowInstance:
22
23
  """
23
24
  Defines a workflow Instance.
24
- The workflow Instance is context which has been executed
25
+ The workflow Instance is a context which has been executed
25
26
  """
26
27
 
27
28
  def __init__(self, instance_id: int):
@@ -79,8 +80,11 @@ class WorkflowAPI(AuthenticatedAPI):
79
80
  workflow_types = ['Ingest', 'Access', 'Transformation', 'DataManagement']
80
81
 
81
82
  def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
82
- use_shared_secret: bool = False, two_fa_secret_key: str = None, protocol: str = "https"):
83
- super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
83
+ use_shared_secret: bool = False, two_fa_secret_key: str = None,
84
+ protocol: str = "https", request_hook: Callable = None):
85
+
86
+ super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key,
87
+ protocol, request_hook)
84
88
  self.base_url = "sdb/rest/workflow"
85
89
 
86
90
  def get_workflow_contexts_by_type(self, workflow_type: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyPreservica
3
- Version: 2.7.3
3
+ Version: 2.8.0
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -29,6 +29,7 @@ tests/test_delete.py
29
29
  tests/test_download.py
30
30
  tests/test_entity.py
31
31
  tests/test_export_opex.py
32
+ tests/test_groups.py
32
33
  tests/test_identifier.py
33
34
  tests/test_ingest.py
34
35
  tests/test_integrity_check.py
@@ -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.7.3",
24
+ version="2.8.0",
25
25
  description="Python library for the Preservica API",
26
26
  long_description=README,
27
27
  long_description_content_type="text/markdown",
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
  from pyPreservica import *
3
3
 
4
-
4
+ @pytest.mark.filterwarnings("ignore:datetime.datetime.utcnow")
5
5
  def test_crawl_fs():
6
6
  path = "./test_data/"
7
7
  bucket = "com.preservica.dev.preview.sales.autoupload"
@@ -0,0 +1,8 @@
1
+ import pytest
2
+ from pyPreservica import *
3
+
4
+
5
+ def test_cam_get_all_groups():
6
+ client = MetadataGroupsAPI()
7
+ for g in client.groups():
8
+ assert isinstance(g, Group)
@@ -81,7 +81,7 @@ def test_ingest_access_file():
81
81
 
82
82
  client.delete_asset(asset, "operator comment", "supervisor")
83
83
 
84
-
84
+ @pytest.mark.filterwarnings("ignore:datetime.datetime.utcnow")
85
85
  def test_s3_upload():
86
86
  client = EntityAPI()
87
87
  upload = UploadAPI()
@@ -11,7 +11,7 @@ def test_get_workflow_contexts():
11
11
  def test_get_workflow_contexts2():
12
12
  workflow = WorkflowAPI()
13
13
  workflows = workflow.get_workflow_contexts("com.preservica.core.workflow.ingest")
14
- assert len(workflows) == 3
14
+ assert len(workflows) == 1
15
15
 
16
16
 
17
17
  def test_get_workflow_contexts3():
@@ -24,7 +24,7 @@ def test_get_workflow_contexts_type():
24
24
  workflow = WorkflowAPI()
25
25
 
26
26
  workflows = workflow.get_workflow_contexts_by_type("Ingest")
27
- assert len(workflows) == 8
27
+ assert len(workflows) == 6
28
28
 
29
29
  workflows = workflow.get_workflow_contexts_by_type("Access")
30
30
  assert len(workflows) == 5
@@ -1,100 +0,0 @@
1
- """
2
- pyPreservica MDFormsAPI module definition
3
-
4
- A client library for the Preservica Repository web services Metadata API
5
- https://demo.preservica.com/api/metadata/documentation.html
6
-
7
- author: James Carr
8
- licence: Apache License 2.0
9
-
10
- """
11
- import json
12
- import xml.etree.ElementTree
13
-
14
- from pyPreservica.common import *
15
-
16
-
17
- class MDFormsAPI(AuthenticatedAPI):
18
- def __init__(self, username: str = None, password: str = None, tenant: str = None, server: str = None,
19
- use_shared_secret: bool = False, two_fa_secret_key: str = None, protocol: str = "https"):
20
- super().__init__(username, password, tenant, server, use_shared_secret, two_fa_secret_key, protocol)
21
- xml.etree.ElementTree.register_namespace("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/")
22
- xml.etree.ElementTree.register_namespace("ead", "urn:isbn:1-931666-22-9")
23
-
24
- def delete_group(self, id: str):
25
- """
26
- Delete a group
27
- :param id: Group ID
28
- :return:
29
- """
30
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
31
- url = f'{self.protocol}://{self.server}/api/metadata/groups/{id}'
32
- with self.session.delete(url, headers=headers) as request:
33
- if request.status_code == requests.codes.unauthorized:
34
- self.token = self.__token__()
35
- return self.delete_group(id)
36
- elif request.status_code == requests.codes.no_content:
37
- return None
38
- else:
39
- exception = HTTPException(None, request.status_code, request.url, "delete_group",
40
- request.content.decode('utf-8'))
41
- logger.error(exception)
42
- raise exception
43
-
44
- def add_group(self, document):
45
- """
46
- Add a new group
47
- :return:
48
- """
49
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
50
- url = f'{self.protocol}://{self.server}/api/metadata/groups/'
51
- with self.session.post(url, headers=headers, json=document) as request:
52
- if request.status_code == requests.codes.unauthorized:
53
- self.token = self.__token__()
54
- return self.add_group(document)
55
- elif request.status_code == requests.codes.created:
56
- return json.loads(str(request.content.decode('utf-8')))
57
- else:
58
- exception = HTTPException(None, request.status_code, request.url, "group",
59
- request.content.decode('utf-8'))
60
- logger.error(exception)
61
- raise exception
62
-
63
- def group(self, id: str):
64
- """
65
- Fetch a metadata Group by its id
66
- :param id: The group ID
67
- :return: JSON Document
68
- """
69
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
70
- url = f'{self.protocol}://{self.server}/api/metadata/groups/{id}'
71
- with self.session.get(url, headers=headers) as request:
72
- if request.status_code == requests.codes.unauthorized:
73
- self.token = self.__token__()
74
- return self.group(id)
75
- elif request.status_code == requests.codes.ok:
76
- return json.loads(str(request.content.decode('utf-8')))
77
- else:
78
- exception = HTTPException(None, request.status_code, request.url, "group",
79
- request.content.decode('utf-8'))
80
- logger.error(exception)
81
- raise exception
82
-
83
- def groups(self):
84
- """
85
- Fetch all the Metadata Groups as JSON
86
- :return: JSON Document
87
- """
88
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json;charset=UTF-8'}
89
- url = f'{self.protocol}://{self.server}/api/metadata/groups'
90
- with self.session.get(url, headers=headers) as request:
91
- if request.status_code == requests.codes.unauthorized:
92
- self.token = self.__token__()
93
- return self.groups()
94
- elif request.status_code == requests.codes.ok:
95
- return json.loads(str(request.content.decode('utf-8')))['groups']
96
- else:
97
- exception = HTTPException(None, request.status_code, request.url, "groups",
98
- request.content.decode('utf-8'))
99
- logger.error(exception)
100
- raise exception
File without changes
File without changes
File without changes