pyPreservica 3.2.0__py3-none-any.whl → 3.2.2__py3-none-any.whl

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.

pyPreservica/__init__.py CHANGED
@@ -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.0"
38
+ __version__ = "3.2.2"
39
39
 
40
40
  __license__ = "Apache License Version 2.0"
pyPreservica/common.py CHANGED
@@ -27,6 +27,7 @@ from requests import Session
27
27
  from urllib3.util import Retry
28
28
  import requests
29
29
  from requests.adapters import HTTPAdapter
30
+ from typing import TypeVar
30
31
 
31
32
  import pyPreservica
32
33
 
@@ -420,6 +421,26 @@ class Bitstream:
420
421
  return self.__str__()
421
422
 
422
423
 
424
+ class ExternIdentifier:
425
+ """
426
+ Class to represent the External Identifier Object in the Preservica data model
427
+ """
428
+
429
+ def __init__(self, identifier_type: str, identifier_value: str):
430
+ self.type = identifier_type
431
+ self.value = identifier_value
432
+ self.id = None
433
+
434
+ def __str__(self):
435
+ return f"""
436
+ Identifier: {self.id}
437
+ Identifier Type: {self.type}
438
+ Identifier Value: {self.value}
439
+ """
440
+
441
+ def __repr__(self):
442
+ return self.__str__()
443
+
423
444
  class Generation:
424
445
  """
425
446
  Class to represent the Generation Object in the Preservica data model
@@ -528,6 +549,9 @@ class ContentObject(Entity):
528
549
  self.tag = "ContentObject"
529
550
 
530
551
 
552
+ EntityT = TypeVar("EntityT", Folder, Asset, ContentObject, None)
553
+
554
+
531
555
  class Representation:
532
556
  """
533
557
  Class to represent the Representation Object in the Preservica data model
@@ -738,7 +762,7 @@ class AuthenticatedAPI:
738
762
  Return the edition of this tenancy
739
763
  """
740
764
  if self.major_version < 8 and self.minor_version < 3:
741
- raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
765
+ raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
742
766
 
743
767
  headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
744
768
 
@@ -777,6 +801,7 @@ class AuthenticatedAPI:
777
801
  self.sec_ns = f"{NS_SEC_ROOT}/v{self.major_version}.{self.minor_version}"
778
802
  self.admin_ns = f"{NS_ADMIN}/v{self.major_version}.{self.minor_version}"
779
803
 
804
+
780
805
  def __version_number__(self):
781
806
  """
782
807
  Determine the version number of the server
@@ -791,6 +816,10 @@ class AuthenticatedAPI:
791
816
  self.major_version = int(version_numbers[0])
792
817
  self.minor_version = int(version_numbers[1])
793
818
  self.patch_version = int(version_numbers[2])
819
+
820
+ if self.server == "preview.preservica.com":
821
+ self.minor_version = 1
822
+
794
823
  return version
795
824
  elif request.status_code == requests.codes.unauthorized:
796
825
  self.token = self.__token__()
@@ -801,7 +830,7 @@ class AuthenticatedAPI:
801
830
  RuntimeError(request.status_code, "version number failed")
802
831
 
803
832
  def __str__(self):
804
- return f"pyPreservica version: {pyPreservica.__version__} (Preservica 7.0 Compatible) " \
833
+ return f"pyPreservica version: {pyPreservica.__version__} (Preservica 8.0 Compatible) " \
805
834
  f"Connected to: {self.server} Preservica version: {self.version} as {self.username} " \
806
835
  f"in tenancy {self.tenant}"
807
836
 
pyPreservica/entityAPI.py CHANGED
@@ -149,7 +149,6 @@ class EntityAPI(AuthenticatedAPI):
149
149
 
150
150
  return storage_locations
151
151
 
152
-
153
152
  def bitstream_content(self, bitstream: Bitstream, filename: str, chunk_size: int = CHUNK_SIZE) -> Union[int, None]:
154
153
  """
155
154
  Download a file represented as a Bitstream to a local filename
@@ -486,6 +485,53 @@ class EntityAPI(AuthenticatedAPI):
486
485
  logger.error(request)
487
486
  raise RuntimeError(request.status_code, "delete_identifier failed")
488
487
 
488
+ def entity_identifiers(self, entity: Entity, external_identifier_type = None) -> set[ExternIdentifier]:
489
+ """
490
+ Get all external identifiers on an entity
491
+
492
+ Returns the set of external identifiers on the entity
493
+
494
+ :param entity: The Entity (Asset or Folder)
495
+ :param external_identifier_type: Optional identifier type to filter the results
496
+ :type entity: Entity
497
+ """
498
+ headers = {HEADER_TOKEN: self.token}
499
+ request = self.session.get(
500
+ f'{self.protocol}://{self.server}/api/entity/{entity.path}/{entity.reference}/identifiers',
501
+ headers=headers)
502
+ if request.status_code == requests.codes.ok:
503
+ xml_response = str(request.content.decode('utf-8'))
504
+ logger.debug(xml_response)
505
+ entity_response = xml.etree.ElementTree.fromstring(xml_response)
506
+ identifier_list = entity_response.findall(f'.//{{{self.xip_ns}}}Identifier')
507
+ result = set()
508
+ for identifier in identifier_list:
509
+ identifier_value = identifier_type = identifier_id = ""
510
+ for child in identifier:
511
+ if child.tag.endswith("Type"):
512
+ identifier_type = child.text
513
+ if child.tag.endswith("Value"):
514
+ identifier_value = child.text
515
+ if child.tag.endswith("ApiId"):
516
+ identifier_id = child.text
517
+ if external_identifier_type is None:
518
+ external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
519
+ external_id.identifier_id = identifier_id
520
+ result.add(external_id)
521
+ else:
522
+ if identifier_type == external_identifier_type:
523
+ external_id: ExternIdentifier = ExternIdentifier(identifier_type, identifier_value)
524
+ external_id.identifier_id = identifier_id
525
+ result.add(external_id)
526
+ return result
527
+ elif request.status_code == requests.codes.unauthorized:
528
+ self.token = self.__token__()
529
+ return self.entity_identifiers(entity)
530
+ else:
531
+ exception = HTTPException(entity.reference, request.status_code, request.url, "identifiers_for_entity",
532
+ request.content.decode('utf-8'))
533
+ logger.error(exception)
534
+ raise exception
489
535
 
490
536
  def identifiers_for_entity(self, entity: Entity) -> set[Tuple]:
491
537
  """
@@ -524,16 +570,14 @@ class EntityAPI(AuthenticatedAPI):
524
570
  logger.error(exception)
525
571
  raise exception
526
572
 
527
-
528
-
529
- def identifier(self, identifier_type: str, identifier_value: str) -> set[Entity]:
573
+ def identifier(self, identifier_type: str, identifier_value: str) -> set[EntityT]:
530
574
  """
531
- Get all entities which have the external identifier
575
+ Get all entities which have the external identifier
532
576
 
533
- Returns the set of entities which have the external identifier
577
+ Returns the set of entities which have the external identifier
534
578
 
535
- :param identifier_type: The identifier type
536
- :param identifier_value: The identifier value
579
+ :param identifier_type: The identifier type
580
+ :param identifier_value: The identifier value
537
581
  """
538
582
  headers = {HEADER_TOKEN: self.token}
539
583
  payload = {'type': identifier_type, 'value': identifier_value}
@@ -861,7 +905,7 @@ class EntityAPI(AuthenticatedAPI):
861
905
  logger.error(exception)
862
906
  raise exception
863
907
 
864
- def delete_metadata(self, entity: Entity, schema: str) -> Entity:
908
+ def delete_metadata(self, entity: EntityT, schema: str) -> EntityT:
865
909
  """
866
910
  Deletes all the metadata fragments on an entity which match the schema URI
867
911
 
@@ -887,7 +931,7 @@ class EntityAPI(AuthenticatedAPI):
887
931
 
888
932
  return self.entity(entity.entity_type, entity.reference)
889
933
 
890
- def update_metadata(self, entity: Entity, schema: str, data: Any) -> Entity:
934
+ def update_metadata(self, entity: EntityT, schema: str, data: Any) -> EntityT:
891
935
  """
892
936
  Update all existing metadata fragments which match the schema
893
937
 
@@ -933,7 +977,9 @@ class EntityAPI(AuthenticatedAPI):
933
977
  raise exception
934
978
  return self.entity(entity.entity_type, entity.reference)
935
979
 
936
- def add_metadata_as_fragment(self, entity: Entity, schema: str, xml_fragment: str) -> Entity:
980
+ def add_metadata_as_fragment(
981
+ self, entity: EntityT, schema: str, xml_fragment: str
982
+ ) -> EntityT:
937
983
  """
938
984
  Add a metadata fragment with a given namespace URI to an Entity
939
985
 
@@ -969,8 +1015,7 @@ class EntityAPI(AuthenticatedAPI):
969
1015
  logger.error(exception)
970
1016
  raise exception
971
1017
 
972
-
973
- def add_metadata(self, entity: Entity, schema: str, data) -> Entity:
1018
+ def add_metadata(self, entity: EntityT, schema: str, data) -> EntityT:
974
1019
  """
975
1020
  Add a metadata fragment with a given namespace URI
976
1021
 
@@ -1011,7 +1056,7 @@ class EntityAPI(AuthenticatedAPI):
1011
1056
  logger.error(exception)
1012
1057
  raise exception
1013
1058
 
1014
- def save(self, entity: Entity) -> Entity:
1059
+ def save(self, entity: EntityT) -> EntityT:
1015
1060
  """
1016
1061
  Save the title and description of an entity
1017
1062
 
@@ -1109,7 +1154,6 @@ class EntityAPI(AuthenticatedAPI):
1109
1154
  def get_progress(self, pid: str) -> AsyncProgress:
1110
1155
  return AsyncProgress[self.get_async_progress(pid)]
1111
1156
 
1112
-
1113
1157
  def get_async_progress(self, pid: str) -> str:
1114
1158
  headers = {HEADER_TOKEN: self.token, 'Content-Type': 'text/plain'}
1115
1159
  request = self.session.get(f"{self.protocol}://{self.server}/api/entity/progress/{pid}", headers=headers)
@@ -1129,7 +1173,7 @@ class EntityAPI(AuthenticatedAPI):
1129
1173
  logger.error(exception)
1130
1174
  raise exception
1131
1175
 
1132
- def move_sync(self, entity: Entity, dest_folder: Folder) -> Entity:
1176
+ def move_sync(self, entity: EntityT, dest_folder: Folder) -> EntityT:
1133
1177
  """
1134
1178
  Move an Entity (Asset or Folder) to a new Folder
1135
1179
  If dest_folder is None then the entity must be a Folder and will be moved to the root of the repository
@@ -1169,7 +1213,7 @@ class EntityAPI(AuthenticatedAPI):
1169
1213
  logger.error(exception)
1170
1214
  raise exception
1171
1215
 
1172
- def move(self, entity: Entity, dest_folder: Folder) -> Entity:
1216
+ def move(self, entity: EntityT, dest_folder: Folder) -> EntityT:
1173
1217
  """
1174
1218
  Move an Entity (Asset or Folder) to a new Folder
1175
1219
  If dest_folder is None then the entity must be a Folder and will be moved to the root of the repository
@@ -1274,7 +1318,7 @@ class EntityAPI(AuthenticatedAPI):
1274
1318
  else:
1275
1319
  return xml_object.find(tag).text
1276
1320
 
1277
- def security_tag_sync(self, entity: Entity, new_tag: str):
1321
+ def security_tag_sync(self, entity: EntityT, new_tag: str) -> EntityT:
1278
1322
  """
1279
1323
  Change the security tag for a folder or asset
1280
1324
 
@@ -1354,7 +1398,7 @@ class EntityAPI(AuthenticatedAPI):
1354
1398
  logger.error(exception)
1355
1399
  raise exception
1356
1400
 
1357
- def entity(self, entity_type: EntityType, reference: str) -> Entity:
1401
+ def entity(self, entity_type: EntityType, reference: str) -> EntityT:
1358
1402
  """
1359
1403
  Retrieve an entity by its type and reference
1360
1404
 
@@ -1416,6 +1460,42 @@ class EntityAPI(AuthenticatedAPI):
1416
1460
  logger.error(exception)
1417
1461
  raise exception
1418
1462
 
1463
+ def merge_folder(self, folder: Folder)-> str:
1464
+ """
1465
+ Create a new Asset with the content from each Asset in the Folder
1466
+
1467
+ This call will create a new multi-part Asset which contains all the content from the Folder.
1468
+
1469
+ The new Asset which is created will have the same title, description and parent as the Folder.
1470
+
1471
+ The return value is the progress status of the merge operation.
1472
+ """
1473
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8', 'accept': 'text/plain;charset=UTF-8'}
1474
+ payload = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1475
+ <MergeAction xmlns="{self.entity_ns}" xmlns:xip="{self.xip_ns}">
1476
+ <Title>{folder.title}</Title>
1477
+ <Description>{folder.description}</Description>
1478
+ <Entity excludeIdentifiers="true" excludeLinks="true" excludeMetadata="true" ref="{folder.reference}" type="SO"/>
1479
+ </MergeAction>"""
1480
+ request = self.session.post(
1481
+ f"{self.protocol}://{self.server}/api/entity/actions/merges", data=payload, headers=headers)
1482
+ if request.status_code == requests.codes.accepted:
1483
+ return request.content.decode('utf-8')
1484
+ elif request.status_code == requests.codes.unauthorized:
1485
+ self.token = self.__token__()
1486
+ return self.merge_folder(folder)
1487
+ else:
1488
+ exception = HTTPException(
1489
+ folder.reference,
1490
+ request.status_code,
1491
+ request.url,
1492
+ "merge_folder",
1493
+ request.content.decode("utf-8"),
1494
+ )
1495
+ logger.error(exception)
1496
+ raise exception
1497
+
1498
+
1419
1499
  def asset(self, reference: str) -> Asset:
1420
1500
  """
1421
1501
  Retrieve an Asset by its reference
@@ -2209,7 +2289,6 @@ class EntityAPI(AuthenticatedAPI):
2209
2289
  if "username" in kwargs:
2210
2290
  params["username"] = kwargs.get("username")
2211
2291
 
2212
-
2213
2292
  if next_page is None:
2214
2293
  request = self.session.get(f'{self.protocol}://{self.server}/api/entity/events', params=params,
2215
2294
  headers=headers)
@@ -2512,4 +2591,4 @@ class EntityAPI(AuthenticatedAPI):
2512
2591
  exception = HTTPException(entity.reference, request.status_code, request.url,
2513
2592
  "_delete_entity", request.content.decode('utf-8'))
2514
2593
  logger.error(exception)
2515
- raise exception
2594
+ raise exception
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyPreservica
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: Python library for the Preservica API
5
5
  Home-page: https://pypreservica.readthedocs.io/
6
6
  Author: James Carr
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Operating System :: OS Independent
20
21
  Classifier: Topic :: System :: Archiving
21
22
  Description-Content-Type: text/markdown
@@ -23,8 +24,8 @@ License-File: LICENSE.txt
23
24
  Requires-Dist: requests
24
25
  Requires-Dist: urllib3
25
26
  Requires-Dist: certifi
26
- Requires-Dist: boto3
27
- Requires-Dist: botocore
27
+ Requires-Dist: boto3>=1.38.0
28
+ Requires-Dist: botocore>=1.38.0
28
29
  Requires-Dist: s3transfer
29
30
  Requires-Dist: azure-storage-blob
30
31
  Requires-Dist: tqdm
@@ -1,9 +1,9 @@
1
- pyPreservica/__init__.py,sha256=erL11_X3f9F3yfHhgtVxRdAH1m2LIOsYkTvReMS3em4,1250
1
+ pyPreservica/__init__.py,sha256=tLUJ6UX2RCmqbq_evT5ZlorC4T9sI7Opv83-E7Rvzj4,1250
2
2
  pyPreservica/adminAPI.py,sha256=aMN2twcUZOFoGx2yapC6GVtBTdYHUJFA-5bdWVkCwS8,37773
3
3
  pyPreservica/authorityAPI.py,sha256=jpf_m9i-IakyNVooi2yELuKt4yhX73hWqQNbPRHZx2g,9206
4
- pyPreservica/common.py,sha256=iEeF4Kg51d4Vug-Dv8TeqS1lP2zcfM7YtBO8oANEcCU,38273
4
+ pyPreservica/common.py,sha256=CLfHI0Fec_wp1zngqw7-iIl2Yp3hG0ohjuhdl-K84hU,39030
5
5
  pyPreservica/contentAPI.py,sha256=ZvX2aGQEaksmw-m-oEUI6daVSqFe_IcE1cGwCNbSCDQ,22286
6
- pyPreservica/entityAPI.py,sha256=cIuipvOlFUNgaLBOXtevTYQrT0zh1NG2y5gXz5sr_k0,125673
6
+ pyPreservica/entityAPI.py,sha256=mELG2TxnFBCuDtsrZ2eRNc8EF-D6dI-dumzTfK976TQ,129974
7
7
  pyPreservica/mdformsAPI.py,sha256=_hBjT4-OzgLQGDfYX7b_01P27wc-RmsCEu57VtyAdh8,19173
8
8
  pyPreservica/monitorAPI.py,sha256=LJOUrynBOWKlNiYpZ1iH8qB1oIIuKX1Ms1SRBcuXohA,6274
9
9
  pyPreservica/opex.py,sha256=ccra1S4ojUXS3PlbU8WfxajOkJrwG4OykBnNrYP_jus,4875
@@ -13,8 +13,8 @@ pyPreservica/settingsAPI.py,sha256=jXnMOCq3mimta6E-Os3J1I1if2pYsjLpOazAx8L-ZQI,1
13
13
  pyPreservica/uploadAPI.py,sha256=uX67mW-2q7FmjtXQ759GwHPL6Zs7R-iE8-86PBApvbY,99823
14
14
  pyPreservica/webHooksAPI.py,sha256=B3C6PV_3JLlJrr9PtsTzL-21M0msx8Mnj18Xb3Bv4RE,6814
15
15
  pyPreservica/workflowAPI.py,sha256=OcOiiUdrQerbPllrkj1lWpmuW0jTuyyV0urwPSYcd_U,17561
16
- pypreservica-3.2.0.dist-info/licenses/LICENSE.txt,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
17
- pypreservica-3.2.0.dist-info/METADATA,sha256=x3FR1zZKihN0pfSN_JdvM10pfP_IsfY1GWV8t3Yd_bo,3009
18
- pypreservica-3.2.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
19
- pypreservica-3.2.0.dist-info/top_level.txt,sha256=iIBh6NAznYQHOV8mv_y_kGKSDITek9rANyFDwJsbU-c,13
20
- pypreservica-3.2.0.dist-info/RECORD,,
16
+ pypreservica-3.2.2.dist-info/licenses/LICENSE.txt,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
17
+ pypreservica-3.2.2.dist-info/METADATA,sha256=_sr1wxUFF5oIuFQB6A90sDUAw88cWb11jhQ-kJGSBrk,3077
18
+ pypreservica-3.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ pypreservica-3.2.2.dist-info/top_level.txt,sha256=iIBh6NAznYQHOV8mv_y_kGKSDITek9rANyFDwJsbU-c,13
20
+ pypreservica-3.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5