pyPreservica 2.7.2__py3-none-any.whl → 3.3.4__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.
pyPreservica/__init__.py CHANGED
@@ -6,23 +6,35 @@ author: James Carr
6
6
  licence: Apache License 2.0
7
7
 
8
8
  """
9
+
9
10
  from .common import *
10
- from .contentAPI import ContentAPI
11
+ from .contentAPI import ContentAPI, Field, SortOrder
11
12
  from .entityAPI import EntityAPI
12
- from .uploadAPI import UploadAPI, simple_asset_package, complex_asset_package, cvs_to_xsd, cvs_to_xml, \
13
- cvs_to_cmis_xslt, csv_to_search_xml, generic_asset_package, upload_config, multi_asset_package
13
+ from .uploadAPI import (
14
+ UploadAPI,
15
+ simple_asset_package,
16
+ complex_asset_package,
17
+ cvs_to_xsd,
18
+ cvs_to_xml,
19
+ cvs_to_cmis_xslt,
20
+ csv_to_search_xml,
21
+ generic_asset_package,
22
+ upload_config,
23
+ multi_asset_package,
24
+ )
14
25
  from .workflowAPI import WorkflowAPI, WorkflowContext, WorkflowInstance
15
26
  from .retentionAPI import RetentionAPI, RetentionAssignment, RetentionPolicy
16
27
  from .parAPI import PreservationActionRegistry
17
28
  from .adminAPI import AdminAPI
18
29
  from .monitorAPI import MonitorAPI, MonitorCategory, MonitorStatus, MessageStatus
19
- from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler
30
+ from .webHooksAPI import WebHooksAPI, TriggerType, WebHookHandler, FlaskWebhookHandler
20
31
  from .authorityAPI import AuthorityAPI, Table
21
- from .mdformsAPI import MDFormsAPI
32
+ from .mdformsAPI import MetadataGroupsAPI, Group, GroupField, GroupFieldType
33
+ from .settingsAPI import SettingsAPI
22
34
 
23
35
  __author__ = "James Carr (drjamescarr@gmail.com)"
24
36
 
25
37
  # Version of the pyPreservica package
26
- __version__ = "2.7.2"
38
+ __version__ = "3.3.4"
27
39
 
28
40
  __license__ = "Apache License Version 2.0"
pyPreservica/adminAPI.py CHANGED
@@ -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)
@@ -251,8 +251,9 @@ class AdminAPI(AuthenticatedAPI):
251
251
  xml.etree.ElementTree.SubElement(xml_roles, "Role").text = role
252
252
  xml_request = xml.etree.ElementTree.tostring(xml_object, encoding='utf-8')
253
253
  logger.debug(xml_request)
254
+ params = {"source": "UX2"}
254
255
  request = self.session.post(f'{self.protocol}://{self.server}/api/admin/users', data=xml_request,
255
- headers=headers)
256
+ headers=headers, params=params)
256
257
  if request.status_code == requests.codes.created:
257
258
  return self.user_details(username)
258
259
  elif request.status_code == requests.codes.unauthorized:
@@ -444,7 +445,7 @@ class AdminAPI(AuthenticatedAPI):
444
445
  :param xml_data: The xml schema as a UTF-8 string or a file like object
445
446
  :type xml_data: Any
446
447
 
447
- :return: None
448
+ :return:
448
449
  :rtype: None
449
450
  """
450
451
 
@@ -463,7 +464,7 @@ class AdminAPI(AuthenticatedAPI):
463
464
  params=params,
464
465
  data=xml_data)
465
466
  if request.status_code == requests.codes.created:
466
- return
467
+ return None
467
468
  elif request.status_code == requests.codes.unauthorized:
468
469
  self.token = self.__token__()
469
470
  return self.add_xml_schema(name, description, originalName, xml_data)
@@ -493,7 +494,7 @@ class AdminAPI(AuthenticatedAPI):
493
494
  :param document_type: The type of the XML document, defaults to descriptive metadata templates
494
495
  :type document_type: str
495
496
 
496
- :return: None
497
+ :return:
497
498
  :rtype: None
498
499
 
499
500
  """
@@ -513,7 +514,7 @@ class AdminAPI(AuthenticatedAPI):
513
514
  params=params,
514
515
  data=xml_data)
515
516
  if request.status_code == requests.codes.created:
516
- return
517
+ return None
517
518
  elif request.status_code == requests.codes.unauthorized:
518
519
  self.token = self.__token__()
519
520
  return self.add_xml_document(name, xml_data, document_type)
@@ -523,12 +524,12 @@ class AdminAPI(AuthenticatedAPI):
523
524
 
524
525
  def delete_xml_document(self, uri: str):
525
526
  """
526
- Delete a XML document from Preservica
527
+ Delete an XML document from Preservica's XML document store
527
528
 
528
529
  :param uri: The URI of the xml document to delete
529
530
  :type uri: str
530
531
 
531
- :return: None
532
+ :return:
532
533
  :rtype: None
533
534
 
534
535
  """
@@ -543,13 +544,14 @@ class AdminAPI(AuthenticatedAPI):
543
544
  f"{self.protocol}://{self.server}/api/admin/documents/{document['ApiId']}",
544
545
  headers=headers)
545
546
  if request.status_code == requests.codes.no_content:
546
- return
547
+ return None
547
548
  elif request.status_code == requests.codes.unauthorized:
548
549
  self.token = self.__token__()
549
550
  return self.delete_xml_document(uri)
550
551
  else:
551
552
  logger.error(request.content.decode('utf-8'))
552
553
  raise RuntimeError(request.status_code, "delete_xml_document failed")
554
+ return None
553
555
 
554
556
  def delete_xml_schema(self, uri: str):
555
557
  """
@@ -558,7 +560,7 @@ class AdminAPI(AuthenticatedAPI):
558
560
  :param uri: The URI of the xml schema to delete
559
561
  :type uri: str
560
562
 
561
- :return: None
563
+ :return:
562
564
  :rtype: None
563
565
 
564
566
  """
@@ -572,17 +574,18 @@ class AdminAPI(AuthenticatedAPI):
572
574
  request = self.session.delete(f"{self.protocol}://{self.server}/api/admin/schemas/{schema['ApiId']}",
573
575
  headers=headers)
574
576
  if request.status_code == requests.codes.no_content:
575
- return
577
+ return None
576
578
  elif request.status_code == requests.codes.unauthorized:
577
579
  self.token = self.__token__()
578
580
  return self.delete_xml_schema(uri)
579
581
  else:
580
582
  logger.error(request.content.decode('utf-8'))
581
583
  raise RuntimeError(request.status_code, "delete_xml_schema failed")
584
+ return None
582
585
 
583
- def xml_schema(self, uri: str) -> str:
586
+ def xml_schema(self, uri: str) -> Union[str, None]:
584
587
  """
585
- fetch the metadata schema XSD document as a string by its URI
588
+ Fetch the metadata schema XSD document as a string by its URI
586
589
 
587
590
  :param uri: The URI of the xml schema
588
591
  :type uri: str
@@ -607,8 +610,9 @@ class AdminAPI(AuthenticatedAPI):
607
610
  else:
608
611
  logger.error(request.content.decode('utf-8'))
609
612
  raise RuntimeError(request.status_code, "xml_schema failed")
613
+ return None
610
614
 
611
- def xml_document(self, uri: str) -> str:
615
+ def xml_document(self, uri: str) -> Union[str, None]:
612
616
  """
613
617
  fetch the metadata XML document as a string by its URI
614
618
 
@@ -634,6 +638,7 @@ class AdminAPI(AuthenticatedAPI):
634
638
  else:
635
639
  logger.error(request.content.decode('utf-8'))
636
640
  raise RuntimeError(request.status_code, "xml_document failed")
641
+ return None
637
642
 
638
643
  def xml_documents(self) -> List:
639
644
  """
@@ -754,7 +759,7 @@ class AdminAPI(AuthenticatedAPI):
754
759
  logger.error(request.content.decode('utf-8'))
755
760
  raise RuntimeError(request.status_code, "xml_transforms failed")
756
761
 
757
- def xml_transform(self, input_uri: str, output_uri: str) -> str:
762
+ def xml_transform(self, input_uri: str, output_uri: str) -> Union[str, None]:
758
763
  """
759
764
  fetch the XML transform as a string by its URIs
760
765
 
@@ -782,6 +787,7 @@ class AdminAPI(AuthenticatedAPI):
782
787
  else:
783
788
  logger.error(request.content.decode('utf-8'))
784
789
  raise RuntimeError(request.status_code, "xml_transform failed")
790
+ return None
785
791
 
786
792
  def delete_xml_transform(self, input_uri: str, output_uri: str):
787
793
  """
@@ -793,7 +799,7 @@ class AdminAPI(AuthenticatedAPI):
793
799
  :param output_uri: The URI of the output XML document
794
800
  :type output_uri: str
795
801
 
796
- :return: None
802
+ :return:
797
803
  :rtype: None
798
804
 
799
805
  """
@@ -808,13 +814,14 @@ class AdminAPI(AuthenticatedAPI):
808
814
  f"{self.protocol}://{self.server}/api/admin/transforms/{transform['ApiId']}",
809
815
  headers=headers)
810
816
  if request.status_code == requests.codes.no_content:
811
- return
817
+ return None
812
818
  elif request.status_code == requests.codes.unauthorized:
813
819
  self.token = self.__token__()
814
820
  return self.delete_xml_transform(input_uri, output_uri)
815
821
  else:
816
822
  logger.error(request.content.decode('utf-8'))
817
823
  raise RuntimeError(request.status_code, "delete_xml_transform failed")
824
+ return None
818
825
 
819
826
  def add_xml_transform(self, name: str, input_uri: str, output_uri: str, purpose: str, originalName: str,
820
827
  xml_data: Any):
@@ -839,7 +846,7 @@ class AdminAPI(AuthenticatedAPI):
839
846
  :param xml_data: The transform xml as a string or file like object
840
847
  :type xml_data: Any
841
848
 
842
- :return: None
849
+ :return:
843
850
  :rtype: None
844
851
 
845
852
  """
@@ -860,7 +867,7 @@ class AdminAPI(AuthenticatedAPI):
860
867
  params=params,
861
868
  data=xml_data)
862
869
  if request.status_code == requests.codes.created:
863
- return
870
+ return None
864
871
 
865
872
  if request.status_code == requests.codes.unauthorized:
866
873
  self.token = self.__token__()
@@ -8,9 +8,8 @@ author: James Carr
8
8
  licence: Apache License 2.0
9
9
 
10
10
  """
11
- import json
11
+
12
12
  import csv
13
- import requests
14
13
  from typing import List, Set
15
14
 
16
15
  from pyPreservica.common import *
@@ -55,7 +54,7 @@ class AuthorityAPI(AuthenticatedAPI):
55
54
  self.token = self.__token__()
56
55
  return self.delete_record(reference)
57
56
  if response.status_code == requests.codes.no_content:
58
- return
57
+ return None
59
58
  else:
60
59
  exception = HTTPException("", response.status_code, response.url, "delete_record",
61
60
  response.content.decode('utf-8'))
@@ -92,7 +91,7 @@ class AuthorityAPI(AuthenticatedAPI):
92
91
  :param table: The Table to add the record to
93
92
  :type: table: Table
94
93
 
95
- :param record: The record
94
+ :param record: The record as a dictionary
96
95
  :type: record: dict
97
96
 
98
97
  :return: A single record
@@ -123,7 +122,7 @@ class AuthorityAPI(AuthenticatedAPI):
123
122
  """
124
123
  Return a record by its reference
125
124
 
126
- :param reference: The record reference
125
+ :param reference: The reference of the record
127
126
  :type: reference: str
128
127
 
129
128
  :return: A single record
@@ -149,7 +148,7 @@ class AuthorityAPI(AuthenticatedAPI):
149
148
  """
150
149
  Return all records from a table
151
150
 
152
- :param table: The authority table
151
+ :param table: The authority table to return the records from
153
152
  :type: table: Table
154
153
 
155
154
  :return: List of records
@@ -178,7 +177,7 @@ class AuthorityAPI(AuthenticatedAPI):
178
177
  :param reference: The reference for the authority table
179
178
  :type: reference: str
180
179
 
181
- :return: An authority table
180
+ :return: An authority table of interest
182
181
  :rtype: Table
183
182
 
184
183
  """
pyPreservica/common.py CHANGED
@@ -23,10 +23,13 @@ import xml.etree.ElementTree
23
23
  from enum import Enum
24
24
  from pathlib import Path
25
25
  import pyotp
26
- from requests import Response, Session
26
+ 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
31
+ from datetime import datetime
32
+ import dateutil
30
33
 
31
34
  import pyPreservica
32
35
 
@@ -79,9 +82,9 @@ class FileHash:
79
82
 
80
83
  def identifiersToDict(identifiers: set) -> dict:
81
84
  """
82
- Convert a set of tuples to a dict
83
- :param identifiers:
84
- :return:
85
+ Convert a set of tuples to a dict
86
+ :param identifiers:
87
+ :return:
85
88
  """
86
89
  result = {}
87
90
  for identifier_tuple in identifiers:
@@ -405,6 +408,9 @@ class Bitstream:
405
408
  self.length = int(length)
406
409
  self.fixity = fixity
407
410
  self.content_url = content_url
411
+ self.bs_index = None
412
+ self.gen_index = None
413
+ self.co_ref = None
408
414
 
409
415
  def __str__(self):
410
416
  return f"""
@@ -417,6 +423,26 @@ class Bitstream:
417
423
  return self.__str__()
418
424
 
419
425
 
426
+ class ExternIdentifier:
427
+ """
428
+ Class to represent the External Identifier Object in the Preservica data model
429
+ """
430
+
431
+ def __init__(self, identifier_type: str, identifier_value: str):
432
+ self.type = identifier_type
433
+ self.value = identifier_value
434
+ self.id = None
435
+
436
+ def __str__(self):
437
+ return f"""
438
+ Identifier: {self.id}
439
+ Identifier Type: {self.type}
440
+ Identifier Value: {self.value}
441
+ """
442
+
443
+ def __repr__(self):
444
+ return self.__str__()
445
+
420
446
  class Generation:
421
447
  """
422
448
  Class to represent the Generation Object in the Preservica data model
@@ -477,10 +503,10 @@ class Entity:
477
503
  def __repr__(self):
478
504
  return self.__str__()
479
505
 
480
- def has_metadata(self):
506
+ def has_metadata(self) -> bool:
481
507
  return bool(self.metadata)
482
508
 
483
- def metadata_namespaces(self):
509
+ def metadata_namespaces(self) -> list:
484
510
  return list(self.metadata.values())
485
511
 
486
512
 
@@ -525,6 +551,9 @@ class ContentObject(Entity):
525
551
  self.tag = "ContentObject"
526
552
 
527
553
 
554
+ EntityT = TypeVar("EntityT", Folder, Asset, ContentObject, None)
555
+
556
+
528
557
  class Representation:
529
558
  """
530
559
  Class to represent the Representation Object in the Preservica data model
@@ -565,12 +594,28 @@ class Thumbnail(Enum):
565
594
  LARGE = "large"
566
595
 
567
596
 
597
+ class AsyncProgress(Enum):
598
+ """
599
+ Enumeration of the possible status of an asynchronous process
600
+ """
601
+ ABORTED = "ABORTED"
602
+ ACTIVE = "ACTIVE"
603
+ COMPLETED = "COMPLETED"
604
+ PENDING = "PENDING"
605
+ SUSPENDING = "SUSPENDING"
606
+ SUSPENDED = "SUSPENDED"
607
+ UNKNOWN = "UNKNOWN"
608
+ FAILED = "FAILED"
609
+ FINISHED_MIXED_OUTCOME = "FINISHED_MIXED_OUTCOME"
610
+ CANCELLED = "CANCELLED"
611
+
612
+
568
613
  def sanitize(filename) -> str:
569
614
  """
570
615
  Return a fairly safe version of the filename.
571
616
 
572
617
  We don't limit ourselves to ascii, because we want to keep municipality
573
- names, etc, but we do want to get rid of anything potentially harmful,
618
+ names, etc., but we do want to get rid of anything potentially harmful,
574
619
  and make sure we do not exceed Windows filename length limits.
575
620
  Hence, a less safe blacklist, rather than a whitelist.
576
621
  """
@@ -630,19 +675,24 @@ class AuthenticatedAPI:
630
675
  logger.error(f"The AdminAPI requires the user to have ROLE_SDB_MANAGER_USER")
631
676
  raise RuntimeError(f"The API requires the user to have at least the ROLE_SDB_MANAGER_USER")
632
677
 
633
- def _find_user_roles_(self) -> list:
678
+ def _find_user_roles_(self) -> list[str]:
634
679
  """
635
680
  Get a list of roles for the user
636
681
  :return list of roles:
637
682
  """
638
- headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/xml;charset=UTF-8'}
683
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
639
684
  request = self.session.get(f"{self.protocol}://{self.server}/api/user/details", headers=headers)
685
+ logger.debug(request.headers)
640
686
  if request.status_code == requests.codes.ok:
641
- roles = json.loads(str(request.content.decode('utf-8')))['roles']
687
+ json_document = str(request.content.decode('utf-8'))
688
+ logger.debug(json_document)
689
+ roles: list[str] = json.loads(json_document)['roles']
642
690
  return roles
643
691
  elif request.status_code == requests.codes.unauthorized:
644
692
  self.token = self.__token__()
645
693
  return self._find_user_roles_()
694
+ return []
695
+
646
696
 
647
697
  def security_tags_base(self, with_permissions: bool = False) -> dict:
648
698
  """
@@ -714,11 +764,33 @@ class AuthenticatedAPI:
714
764
 
715
765
  return entity_dict
716
766
 
767
+ def edition(self) -> str:
768
+ """
769
+ Return the edition of this tenancy
770
+ """
771
+ if self.major_version < 8 and self.minor_version < 3:
772
+ raise RuntimeError("Entitlement API is only available when connected to a v7.3 System")
773
+
774
+ headers = {HEADER_TOKEN: self.token, 'Content-Type': 'application/json'}
775
+
776
+ response = self.session.get(f'{self.protocol}://{self.server}/api/entitlement/edition', headers=headers)
777
+
778
+ if response.status_code == requests.codes.ok:
779
+ return response.json()['edition']
780
+ elif response.status_code == requests.codes.unauthorized:
781
+ self.token = self.__token__()
782
+ return self.edition()
783
+ else:
784
+ exception = HTTPException("", response.status_code, response.url,
785
+ "edition", response.content.decode('utf-8'))
786
+ logger.error(exception)
787
+ raise exception
788
+
717
789
  def __version_namespace__(self):
718
790
  """
719
791
  Generate version specific namespaces from the server version
720
792
  """
721
- if self.major_version == 7:
793
+ if self.major_version > 6:
722
794
  self.xip_ns = f"{NS_XIP_ROOT}v{self.major_version}.{self.minor_version}"
723
795
  self.entity_ns = f"{NS_ENTITY_ROOT}v{self.major_version}.{self.minor_version}"
724
796
  self.rm_ns = f"{NS_RM_ROOT}v{6}.{2}"
@@ -736,6 +808,8 @@ class AuthenticatedAPI:
736
808
  self.sec_ns = f"{NS_SEC_ROOT}/v{self.major_version}.{self.minor_version}"
737
809
  self.admin_ns = f"{NS_ADMIN}/v{self.major_version}.{self.minor_version}"
738
810
 
811
+ xml.etree.ElementTree.register_namespace("xip", f"{self.xip_ns}")
812
+
739
813
  def __version_number__(self):
740
814
  """
741
815
  Determine the version number of the server
@@ -750,6 +824,7 @@ class AuthenticatedAPI:
750
824
  self.major_version = int(version_numbers[0])
751
825
  self.minor_version = int(version_numbers[1])
752
826
  self.patch_version = int(version_numbers[2])
827
+
753
828
  return version
754
829
  elif request.status_code == requests.codes.unauthorized:
755
830
  self.token = self.__token__()
@@ -758,9 +833,12 @@ class AuthenticatedAPI:
758
833
  logger.error(f"version number failed with http response {request.status_code}")
759
834
  logger.error(str(request.content))
760
835
  RuntimeError(request.status_code, "version number failed")
836
+ return None
837
+
838
+
761
839
 
762
840
  def __str__(self):
763
- return f"pyPreservica version: {pyPreservica.__version__} (Preservica 7.0 Compatible) " \
841
+ return f"pyPreservica version: {pyPreservica.__version__} (Preservica 8.0 Compatible) " \
764
842
  f"Connected to: {self.server} Preservica version: {self.version} as {self.username} " \
765
843
  f"in tenancy {self.tenant}"
766
844
 
@@ -777,7 +855,7 @@ class AuthenticatedAPI:
777
855
  with open('credentials.properties', 'wt', encoding="utf-8") as configfile:
778
856
  config.write(configfile)
779
857
 
780
- def manager_token(self, username: str, password: str):
858
+ def manager_token(self, username: str, password: str) -> str:
781
859
  data = {'username': username, 'password': password, 'tenant': self.tenant}
782
860
  response = self.session.post(f'{self.protocol}://{self.server}/api/accesstoken/login', data=data)
783
861
  if response.status_code == requests.codes.ok:
@@ -789,7 +867,7 @@ class AuthenticatedAPI:
789
867
  logger.error(str(response.content))
790
868
  RuntimeError(response.status_code, "Could not generate valid manager approval token")
791
869
 
792
- def __token__(self):
870
+ def __token__(self) -> str:
793
871
  """
794
872
  Generate am API token to use to authenticate calls
795
873
  :return: API Token
@@ -812,20 +890,23 @@ class AuthenticatedAPI:
812
890
  if self.tenant is None:
813
891
  self.tenant = response.json()['tenant']
814
892
  if self.two_fa_secret_key:
893
+ logger.debug("Found Two Factor Token")
815
894
  totp = pyotp.TOTP(self.two_fa_secret_key)
816
895
  data = {'username': self.username,
817
896
  'continuationToken': response.json()['continuationToken'],
818
897
  'tenant': self.tenant, 'twoFactorToken': totp.now()}
898
+
899
+ header = {'Content-Type': 'application/x-www-form-urlencoded'}
819
900
  response_2fa = self.session.post(
820
901
  f'{self.protocol}://{self.server}/api/accesstoken/complete-2fa',
821
- data=data)
902
+ data=data, headers=header)
822
903
  if response_2fa.status_code == requests.codes.ok:
823
904
  return response_2fa.json()['token']
824
905
  else:
825
906
  msg = "Failed to create a 2FA authentication token. Check your credentials are correct"
826
907
  logger.error(msg)
827
- logger.error(str(response.content))
828
- raise RuntimeError(response.status_code, msg)
908
+ logger.error(str(response_2fa.content))
909
+ raise RuntimeError(response_2fa.status_code, msg)
829
910
  else:
830
911
  msg = "2FA twoFactorToken required to authenticate against this account using 2FA"
831
912
  logger.error(msg)
@@ -857,12 +938,16 @@ class AuthenticatedAPI:
857
938
  raise RuntimeError(response.status_code, msg)
858
939
 
859
940
  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"):
941
+ use_shared_secret: bool = False, two_fa_secret_key: str = None,
942
+ protocol: str = "https", request_hook=None, credentials_path: str = 'credentials.properties'):
861
943
 
862
944
  config = configparser.ConfigParser(interpolation=configparser.Interpolation())
863
- config.read('credentials.properties', encoding='utf-8')
945
+ config.read(os.path.relpath(credentials_path), encoding='utf-8')
864
946
  self.session: Session = requests.Session()
865
947
 
948
+ if request_hook is not None:
949
+ self.session.hooks['response'].append(request_hook)
950
+
866
951
  retries = Retry(
867
952
  total=3,
868
953
  backoff_factor=0.1,
@@ -950,3 +1035,15 @@ class AuthenticatedAPI:
950
1035
 
951
1036
  logger.debug(self.xip_ns)
952
1037
  logger.debug(self.entity_ns)
1038
+
1039
+ def parse_date_to_iso(date):
1040
+ try:
1041
+ date = datetime.datetime.fromisoformat(date.replace('Z','+0000'))
1042
+ if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
1043
+ date = date.replace(tzinfo=datetime.timezone.utc)
1044
+ date = date.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
1045
+ except ValueError:
1046
+ date = dateutil.parser.parse(date)
1047
+ if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
1048
+ date = date.replace(tzinfo=datetime.timezone.utc)
1049
+ date = date.strftime('%Y-%m-%dT%H:%M:%S.%f%z')