pangea-sdk 3.0.0__py3-none-any.whl → 3.2.0__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.
pangea/response.py CHANGED
@@ -12,6 +12,17 @@ from pydantic import BaseModel
12
12
  T = TypeVar("T")
13
13
 
14
14
 
15
+ class TransferMethod(str, enum.Enum):
16
+ DIRECT = "direct"
17
+ MULTIPART = "multipart"
18
+
19
+ def __str__(self):
20
+ return str(self.value)
21
+
22
+ def __repr__(self):
23
+ return str(self.value)
24
+
25
+
15
26
  # API response should accept arbitrary fields to make them accept possible new parameters
16
27
  class APIResponseModel(BaseModel):
17
28
  class Config:
@@ -59,6 +70,15 @@ class ErrorField(APIResponseModel):
59
70
  return self.__repr__()
60
71
 
61
72
 
73
+ class AcceptedStatus(APIResponseModel):
74
+ upload_url: str = ""
75
+ upload_details: Dict[str, Any] = {}
76
+
77
+
78
+ class AcceptedResult(PangeaResponseResult):
79
+ accepted_status: Optional[AcceptedStatus] = None
80
+
81
+
62
82
  class PangeaError(PangeaResponseResult):
63
83
  errors: List[ErrorField] = []
64
84
 
@@ -109,6 +129,7 @@ class PangeaResponse(Generic[T], ResponseHeader):
109
129
  raw_response: Optional[Union[requests.Response, aiohttp.ClientResponse]] = None
110
130
  result: Optional[T] = None
111
131
  pangea_error: Optional[PangeaError] = None
132
+ accepted_result: Optional[AcceptedResult] = None
112
133
  result_class: Type[PangeaResponseResult] = PangeaResponseResult
113
134
  _json: Any
114
135
 
@@ -120,13 +141,14 @@ class PangeaResponse(Generic[T], ResponseHeader):
120
141
  self.result_class = result_class
121
142
  self.result = (
122
143
  self.result_class(**self.raw_result)
123
- if self.raw_result is not None
124
- and issubclass(self.result_class, PangeaResponseResult)
125
- and self.status == ResponseStatus.SUCCESS.value
144
+ if self.raw_result is not None and issubclass(self.result_class, PangeaResponseResult) and self.success
126
145
  else None
127
146
  )
128
147
  if not self.success:
129
- self.pangea_error = PangeaError(**self.raw_result) if self.raw_result is not None else None
148
+ if self.http_status == 202:
149
+ self.accepted_result = AcceptedResult(**self.raw_result) if self.raw_result is not None else None
150
+ else:
151
+ self.pangea_error = PangeaError(**self.raw_result) if self.raw_result is not None else None
130
152
 
131
153
  @property
132
154
  def success(self) -> bool:
@@ -2,14 +2,18 @@
2
2
  # Author: Pangea Cyber Corporation
3
3
  import datetime
4
4
  import json
5
- from typing import Any, Dict, Optional, Union
5
+ from typing import Any, Dict, List, Optional, Union
6
6
 
7
+ import pangea.exceptions as pexc
7
8
  from pangea.response import PangeaResponse
8
9
  from pangea.services.audit.exceptions import AuditException, EventCorruption
9
10
  from pangea.services.audit.models import (
10
11
  Event,
11
12
  EventEnvelope,
12
13
  EventVerification,
14
+ LogBulkRequest,
15
+ LogBulkResult,
16
+ LogEvent,
13
17
  LogRequest,
14
18
  LogResult,
15
19
  PublishedRoot,
@@ -56,7 +60,29 @@ class AuditBase:
56
60
  self.prev_unpublished_root_hash: Optional[str] = None
57
61
  self.tenant_id = tenant_id
58
62
 
59
- def _pre_log_process(self, event: dict, sign_local: bool, verify: bool, verbose: bool) -> LogRequest:
63
+ def _get_log_request(
64
+ self, events: Union[dict, List[dict]], sign_local: bool, verify: bool, verbose: Optional[bool]
65
+ ) -> Union[LogRequest, LogBulkResult]:
66
+ if isinstance(events, list):
67
+ request_events: List[LogEvent] = []
68
+ for e in events:
69
+ request_events.append(self._process_log(e, sign_local=sign_local))
70
+
71
+ input = LogBulkRequest(events=request_events, verbose=verbose)
72
+
73
+ elif isinstance(events, dict):
74
+ log = self._process_log(events, sign_local=sign_local)
75
+ input = LogRequest(event=log.event, signature=log.signature, public_key=log.public_key)
76
+ input.verbose = True if verify else verbose
77
+ if verify and self.prev_unpublished_root_hash:
78
+ input.prev_root = self.prev_unpublished_root_hash
79
+
80
+ else:
81
+ raise AttributeError(f"events should be a dict or a list[dict] and it is {type(events)}")
82
+
83
+ return input
84
+
85
+ def _process_log(self, event: dict, sign_local: bool) -> LogEvent:
60
86
  if event.get("tenant_id", None) is None and self.tenant_id:
61
87
  event["tenant_id"] = self.tenant_id
62
88
 
@@ -66,70 +92,60 @@ class AuditBase:
66
92
  if sign_local is True and self.signer is None:
67
93
  raise AuditException("Error: the `signing` parameter set, but `signer` is not configured")
68
94
 
69
- input = LogRequest(event=event, verbose=verbose)
70
-
95
+ signature = None
96
+ pki = None
71
97
  if sign_local is True:
72
98
  data2sign = canonicalize_event(event)
73
99
  signature = self.signer.sign(data2sign)
74
- if signature is not None:
75
- input.signature = signature
76
- else:
100
+ if signature is None:
77
101
  raise AuditException("Error: failure signing message")
78
102
 
79
103
  # Add public key value to public key info and serialize
80
- self._set_public_key(input, self.signer, self.public_key_info)
81
-
82
- if verify:
83
- input.verbose = True
84
- if self.prev_unpublished_root_hash:
85
- input.prev_root = self.prev_unpublished_root_hash
86
-
87
- return input
104
+ pki = self._get_public_key_info(self.signer, self.public_key_info)
88
105
 
89
- def handle_log_response(self, response: PangeaResponse, verify: bool) -> PangeaResponse[LogResult]:
90
- if not response.success:
91
- return response
106
+ return LogEvent(event=event, signature=signature, public_key=pki)
92
107
 
93
- new_unpublished_root_hash = response.result.unpublished_root
108
+ def _process_log_result(self, result: LogResult, verify: bool):
109
+ new_unpublished_root_hash = result.unpublished_root
94
110
 
95
111
  if verify:
96
- if response.result.envelope:
112
+ if result.envelope:
97
113
  # verify event hash
98
- if response.result.hash and not verify_envelope_hash(response.result.envelope, response.result.hash):
114
+ if result.hash and not verify_envelope_hash(result.envelope, result.hash):
99
115
  # it's a extreme case, it's OK to raise an exception
100
- raise EventCorruption("Error: Event hash failed.", response.result.envelope)
116
+ raise EventCorruption("Error: Event hash failed.", result.envelope)
101
117
 
102
- response.result.signature_verification = self.verify_signature(response.result.envelope)
118
+ result.signature_verification = self.verify_signature(result.envelope)
103
119
 
104
120
  if new_unpublished_root_hash:
105
- if response.result.membership_proof is not None:
121
+ if result.membership_proof is not None:
106
122
  # verify membership proofs
107
- membership_proof = decode_membership_proof(response.result.membership_proof)
123
+ membership_proof = decode_membership_proof(result.membership_proof)
108
124
  if verify_membership_proof(
109
- node_hash=decode_hash(response.result.hash),
125
+ node_hash=decode_hash(result.hash),
110
126
  root_hash=decode_hash(new_unpublished_root_hash),
111
127
  proof=membership_proof,
112
128
  ):
113
- response.result.membership_verification = EventVerification.PASS
129
+ result.membership_verification = EventVerification.PASS
114
130
  else:
115
- response.result.membership_verification = EventVerification.FAIL
131
+ result.membership_verification = EventVerification.FAIL
116
132
 
117
133
  # verify consistency proofs (following events)
118
- if response.result.consistency_proof is not None and self.prev_unpublished_root_hash:
119
- consistency_proof = decode_consistency_proof(response.result.consistency_proof)
134
+ if result.consistency_proof is not None and self.prev_unpublished_root_hash:
135
+ consistency_proof = decode_consistency_proof(result.consistency_proof)
120
136
  if verify_consistency_proof(
121
137
  new_root=decode_hash(new_unpublished_root_hash),
122
138
  prev_root=decode_hash(self.prev_unpublished_root_hash),
123
139
  proof=consistency_proof,
124
140
  ):
125
- response.result.consistency_verification = EventVerification.PASS
141
+ result.consistency_verification = EventVerification.PASS
126
142
  else:
127
- response.result.consistency_verification = EventVerification.FAIL
143
+ result.consistency_verification = EventVerification.FAIL
128
144
 
129
145
  # Update prev unpublished root
130
146
  if new_unpublished_root_hash:
131
147
  self.prev_unpublished_root_hash = new_unpublished_root_hash
132
- return response
148
+ return
133
149
 
134
150
  def handle_results_response(
135
151
  self, response: PangeaResponse[SearchResultOutput], verify_consistency: bool = False, verify_events: bool = True
@@ -348,12 +364,10 @@ class AuditBase:
348
364
  else:
349
365
  return EventVerification.NONE
350
366
 
351
- def _set_public_key(self, input: LogRequest, signer: Signer, public_key_info: Dict[str, str]):
367
+ def _get_public_key_info(self, signer: Signer, public_key_info: Dict[str, str]):
352
368
  public_key_info["key"] = signer.get_public_key_PEM()
353
369
  public_key_info["algorithm"] = signer.get_algorithm()
354
- input.public_key = json.dumps(
355
- public_key_info, ensure_ascii=False, allow_nan=False, separators=(",", ":"), sort_keys=True
356
- )
370
+ return json.dumps(public_key_info, ensure_ascii=False, allow_nan=False, separators=(",", ":"), sort_keys=True)
357
371
 
358
372
 
359
373
  class Audit(ServiceBase, AuditBase):
@@ -444,16 +458,13 @@ class Audit(ServiceBase, AuditBase):
444
458
  A PangeaResponse where the hash of event data and optional verbose
445
459
  results are returned in the response.result field.
446
460
  Available response fields can be found in our
447
- [API documentation](https://pangea.cloud/docs/api/audit#log-an-entry).
461
+ [API documentation](https://pangea.cloud/docs/api/audit#/v1/log).
448
462
 
449
463
  Examples:
450
- try:
451
- log_response = audit.log(message="Hello world", verbose=False)
452
- print(f"Response. Hash: {log_response.result.hash}")
453
- except pe.PangeaAPIException as e:
454
- print(f"Request Error: {e.response.summary}")
455
- for err in e.errors:
456
- print(f"\\t{err.detail} \\n")
464
+ log_response = audit.log(
465
+ message="hello world",
466
+ verbose=True,
467
+ )
457
468
  """
458
469
 
459
470
  event = Event(
@@ -481,6 +492,7 @@ class Audit(ServiceBase, AuditBase):
481
492
  Log an entry
482
493
 
483
494
  Create a log entry in the Secure Audit Log.
495
+
484
496
  Args:
485
497
  event (dict[str, Any]): event to be logged
486
498
  verify (bool, optional): True to verify logs consistency after response.
@@ -493,11 +505,11 @@ class Audit(ServiceBase, AuditBase):
493
505
  Returns:
494
506
  A PangeaResponse where the hash of event data and optional verbose
495
507
  results are returned in the response.result field.
496
- Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#log-an-entry).
508
+ Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v1/log).
497
509
 
498
510
  Examples:
499
511
  try:
500
- log_response = audit.log({"message"="Hello world"}, verbose=False)
512
+ log_response = audit.log({"message": "hello world"}, verbose=True)
501
513
  print(f"Response. Hash: {log_response.result.hash}")
502
514
  except pe.PangeaAPIException as e:
503
515
  print(f"Request Error: {e.response.summary}")
@@ -505,9 +517,98 @@ class Audit(ServiceBase, AuditBase):
505
517
  print(f"\\t{err.detail} \\n")
506
518
  """
507
519
 
508
- input = self._pre_log_process(event, sign_local=sign_local, verify=verify, verbose=verbose)
520
+ input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
509
521
  response = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
510
- return self.handle_log_response(response, verify=verify)
522
+ if response.success:
523
+ self._process_log_result(response.result, verify=verify)
524
+ return response
525
+
526
+ def log_bulk(
527
+ self,
528
+ events: List[Dict[str, Any]],
529
+ sign_local: bool = False,
530
+ verbose: Optional[bool] = None,
531
+ ) -> PangeaResponse[LogBulkResult]:
532
+ """
533
+ Log multiple entries
534
+
535
+ Create multiple log entries in the Secure Audit Log.
536
+
537
+ OperationId: audit_post_v2_log
538
+
539
+ Args:
540
+ events (List[dict[str, Any]]): events to be logged
541
+ sign_local (bool, optional): True to sign event with local key.
542
+ verbose (bool, optional): True to get a more verbose response.
543
+ Raises:
544
+ AuditException: If an audit based api exception happens
545
+ PangeaAPIException: If an API Error happens
546
+
547
+ Returns:
548
+ A PangeaResponse where the hash of event data and optional verbose
549
+ results are returned in the response.result field.
550
+ Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v2/log).
551
+
552
+ Examples:
553
+ log_response = audit.log_bulk(
554
+ events=[{"message": "hello world"}],
555
+ verbose=True,
556
+ )
557
+ """
558
+
559
+ input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
560
+ response = self.request.post("v2/log", LogBulkResult, data=input.dict(exclude_none=True))
561
+
562
+ if response.success:
563
+ for result in response.result.results:
564
+ self._process_log_result(result, verify=True)
565
+ return response
566
+
567
+ def log_bulk_async(
568
+ self,
569
+ events: List[Dict[str, Any]],
570
+ sign_local: bool = False,
571
+ verbose: Optional[bool] = None,
572
+ ) -> PangeaResponse[LogBulkResult]:
573
+ """
574
+ Log multiple entries asynchronously
575
+
576
+ Asynchronously create multiple log entries in the Secure Audit Log.
577
+
578
+ Args:
579
+ events (List[dict[str, Any]]): events to be logged
580
+ sign_local (bool, optional): True to sign event with local key.
581
+ verbose (bool, optional): True to get a more verbose response.
582
+ Raises:
583
+ AuditException: If an audit based api exception happens
584
+ PangeaAPIException: If an API Error happens
585
+
586
+ Returns:
587
+ A PangeaResponse where the hash of event data and optional verbose
588
+ results are returned in the response.result field.
589
+ Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v2/log_async).
590
+
591
+ Examples:
592
+ log_response = audit.log_bulk_async(
593
+ events=[{"message": "hello world"}],
594
+ verbose=True,
595
+ )
596
+ """
597
+
598
+ input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
599
+
600
+ try:
601
+ # Calling to v2 methods will return always a 202.
602
+ response = self.request.post(
603
+ "v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
604
+ )
605
+ except pexc.AcceptedRequestException as e:
606
+ return e.response
607
+
608
+ if response.success:
609
+ for result in response.result.results:
610
+ self._process_log_result(result, verify=True)
611
+ return response
511
612
 
512
613
  def search(
513
614
  self,
@@ -558,11 +659,17 @@ class Audit(ServiceBase, AuditBase):
558
659
 
559
660
  Returns:
560
661
  A PangeaResponse[SearchOutput] where the first page of matched events is returned in the
561
- response.result field. Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#search-for-events).
562
- Pagination can be found in the [search results endpoint](https://pangea.cloud/docs/api/audit#search-results).
662
+ response.result field. Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v1/search).
663
+ Pagination can be found in the [search results endpoint](https://pangea.cloud/docs/api/audit#/v1/results).
563
664
 
564
665
  Examples:
565
- response: PangeaResponse[SearchOutput] = audit.search(query="message:test", search_restriction={'source': ["monitor"]}, limit=1, verify_consistency=True, verify_events=True)
666
+ response = audit.search(
667
+ query="message:test",
668
+ search_restriction={'source': ["monitor"]},
669
+ limit=1,
670
+ verify_consistency=True,
671
+ verify_events=True,
672
+ )
566
673
  """
567
674
 
568
675
  if verify_consistency:
@@ -609,17 +716,11 @@ class Audit(ServiceBase, AuditBase):
609
716
  PangeaAPIException: If an API Error happens
610
717
 
611
718
  Examples:
612
- search_res: PangeaResponse[SearchOutput] = audit.search(
613
- query="message:test",
614
- search_restriction={'source': ["monitor"]},
615
- limit=100,
616
- verify_consistency=True,
617
- verify_events=True)
618
-
619
- result_res: PangeaResponse[SearchResultsOutput] = audit.results(
620
- id=search_res.result.id,
719
+ response = audit.results(
720
+ id="pas_sqilrhruwu54uggihqj3aie24wrctakr",
621
721
  limit=10,
622
- offset=0)
722
+ offset=0,
723
+ )
623
724
  """
624
725
 
625
726
  if limit <= 0:
@@ -146,6 +146,34 @@ class LogRequest(APIRequestModel):
146
146
  prev_root: Optional[str] = None
147
147
 
148
148
 
149
+ class LogEvent(APIRequestModel):
150
+ """
151
+ Event to perform a log action
152
+
153
+ Arguments:
154
+ event -- A structured event describing an auditable activity.
155
+ signature -- An optional client-side signature for forgery protection.
156
+ public_key -- The base64-encoded ed25519 public key used for the signature, if one is provided.
157
+ """
158
+
159
+ event: Dict[str, Any]
160
+ signature: Optional[str] = None
161
+ public_key: Optional[str] = None
162
+
163
+
164
+ class LogBulkRequest(APIRequestModel):
165
+ """
166
+ Request to perform a bulk log action
167
+
168
+ Arguments:
169
+ events -- A list structured events describing an auditable activity.
170
+ verbose -- If true, be verbose in the response; include membership proof, unpublished root and consistency proof, etc.
171
+ """
172
+
173
+ events: List[LogEvent]
174
+ verbose: Optional[bool] = None
175
+
176
+
149
177
  class LogResult(PangeaResponseResult):
150
178
  """
151
179
  Result class after an audit log action
@@ -167,6 +195,10 @@ class LogResult(PangeaResponseResult):
167
195
  signature_verification: EventVerification = EventVerification.NONE
168
196
 
169
197
 
198
+ class LogBulkResult(PangeaResponseResult):
199
+ results: List[LogResult] = []
200
+
201
+
170
202
  class SearchRestriction(APIResponseModel):
171
203
  """
172
204
  Set of restrictions when perform an audit search action
@@ -152,7 +152,7 @@ class UserListOrderBy(enum.Enum):
152
152
  class Authenticator(APIResponseModel):
153
153
  id: str
154
154
  type: str
155
- enable: bool
155
+ enabled: bool
156
156
  provider: Optional[str] = None
157
157
  rpid: Optional[str] = None
158
158
  phase: Optional[str] = None
@@ -399,7 +399,7 @@ class UserAuthenticatorsListRequest(APIRequestModel):
399
399
 
400
400
 
401
401
  class UserAuthenticatorsListResult(PangeaResponseResult):
402
- authenticators: List[Authenticator]
402
+ authenticators: List[Authenticator] = []
403
403
 
404
404
 
405
405
  class FlowCompleteRequest(APIRequestModel):
@@ -3,7 +3,8 @@
3
3
  import io
4
4
  from typing import Dict, List, Optional
5
5
 
6
- from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult
6
+ from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult, TransferMethod
7
+ from pangea.utils import get_presigned_url_upload_params
7
8
 
8
9
  from .base import ServiceBase
9
10
 
@@ -20,6 +21,10 @@ class FileScanRequest(APIRequestModel):
20
21
  verbose: Optional[bool] = None
21
22
  raw: Optional[bool] = None
22
23
  provider: Optional[str] = None
24
+ transfer_size: Optional[int] = None
25
+ transfer_crc32c: Optional[str] = None
26
+ transfer_sha256: Optional[str] = None
27
+ transfer_method: TransferMethod = TransferMethod.DIRECT
23
28
 
24
29
 
25
30
  class FileScanData(PangeaResponseResult):
@@ -74,6 +79,7 @@ class FileScan(ServiceBase):
74
79
  raw: Optional[bool] = None,
75
80
  provider: Optional[str] = None,
76
81
  sync_call: bool = True,
82
+ transfer_method: TransferMethod = TransferMethod.DIRECT,
77
83
  ) -> PangeaResponse[FileScanResult]:
78
84
  """
79
85
  Scan
@@ -108,14 +114,26 @@ class FileScan(ServiceBase):
108
114
  for err in e.errors:
109
115
  print(f"\\t{err.detail} \\n")
110
116
  """
111
- input = FileScanRequest(verbose=verbose, raw=raw, provider=provider)
112
117
 
113
118
  if file or file_path:
114
119
  if file_path:
115
120
  file = open(file_path, "rb")
116
- files = [("upload", ("filename.exe", file, "application/octet-stream"))]
121
+ if transfer_method == TransferMethod.DIRECT:
122
+ crc, sha, size, _ = get_presigned_url_upload_params(file)
123
+ else:
124
+ crc, sha, size = None, None, None
125
+ files = [("upload", ("filename", file, "application/octet-stream"))]
117
126
  else:
118
127
  raise ValueError("Need to set file_path or file arguments")
119
128
 
129
+ input = FileScanRequest(
130
+ verbose=verbose,
131
+ raw=raw,
132
+ provider=provider,
133
+ transfer_crc32c=crc,
134
+ transfer_sha256=sha,
135
+ transfer_size=size,
136
+ transfer_method=transfer_method,
137
+ )
120
138
  data = input.dict(exclude_none=True)
121
139
  return self.request.post("v1/scan", FileScanResult, data=data, files=files, poll_result=sync_call)
@@ -129,6 +129,7 @@ class ItemVersionState(str, enum.Enum):
129
129
  SUSPENDED = "suspended"
130
130
  COMPROMISED = "compromised"
131
131
  DESTROYED = "destroyed"
132
+ INHERITED = "inherited"
132
133
 
133
134
  def __str__(self):
134
135
  return str(self.value)
@@ -217,9 +218,16 @@ class ItemData(PangeaResponseResult):
217
218
  purpose: Optional[str] = None
218
219
 
219
220
 
221
+ class InheritedSettings(PangeaResponseResult):
222
+ rotation_frequency: Optional[str] = None
223
+ rotation_state: Optional[str] = None
224
+ rotation_grace_period: Optional[str] = None
225
+
226
+
220
227
  class GetResult(ItemData):
221
228
  versions: List[ItemVersionData] = []
222
229
  rotation_grace_period: Optional[str] = None
230
+ inherited_settings: Optional[InheritedSettings] = None
223
231
 
224
232
 
225
233
  class ListItemData(ItemData):
@@ -360,6 +368,9 @@ class FolderCreateRequest(APIRequestModel):
360
368
  folder: str
361
369
  metadata: Optional[Metadata] = None
362
370
  tags: Optional[Tags] = None
371
+ rotation_frequency: Optional[str] = None
372
+ rotation_state: Optional[ItemVersionState] = None
373
+ rotation_grace_period: Optional[str] = None
363
374
 
364
375
 
365
376
  class FolderCreateResult(PangeaResponseResult):
pangea/utils.py CHANGED
@@ -1,11 +1,14 @@
1
1
  import base64
2
2
  import copy
3
3
  import datetime
4
+ import io
4
5
  import json
5
6
  from binascii import hexlify
6
7
  from collections import OrderedDict
7
8
  from hashlib import new, sha1, sha256, sha512
8
9
 
10
+ from google_crc32c import Checksum as CRC32C
11
+
9
12
 
10
13
  def format_datetime(dt: datetime.datetime) -> str:
11
14
  """
@@ -22,7 +25,6 @@ def default_encoder(obj) -> str:
22
25
  if isinstance(obj, datetime.date):
23
26
  return str(obj)
24
27
  if isinstance(obj, dict):
25
- print("encoder canonicalize obj")
26
28
  return canonicalize(obj)
27
29
  else:
28
30
  return str(obj)
@@ -96,3 +98,24 @@ def hash_ntlm(data: str):
96
98
 
97
99
  def get_prefix(hash: str, len: int = 5):
98
100
  return hash[0:len]
101
+
102
+
103
+ def get_presigned_url_upload_params(file: io.BufferedReader):
104
+ if "b" not in file.mode:
105
+ raise AttributeError("File need to be open in binary mode")
106
+
107
+ file.seek(0) # restart reading
108
+ crc = CRC32C()
109
+ size = 0
110
+ sha = sha256()
111
+
112
+ while True:
113
+ chunk = file.read(1024 * 1024)
114
+ if not chunk:
115
+ break
116
+ crc.update(chunk)
117
+ sha.update(chunk)
118
+ size += len(chunk)
119
+
120
+ file.seek(0) # restart reading
121
+ return crc.hexdigest().decode("utf-8"), sha.hexdigest(), size, file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pangea-sdk
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Pangea API SDK
5
5
  License: MIT
6
6
  Keywords: Pangea,SDK,Audit
@@ -20,6 +20,7 @@ Requires-Dist: alive-progress (>=2.4.1,<3.0.0)
20
20
  Requires-Dist: asyncio (>=3.4.3,<4.0.0)
21
21
  Requires-Dist: cryptography (==41.0.3)
22
22
  Requires-Dist: deprecated (>=1.2.13,<2.0.0)
23
+ Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
23
24
  Requires-Dist: pydantic (>=1.10.2,<2.0.0)
24
25
  Requires-Dist: pytest (>=7.2.0,<8.0.0)
25
26
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)