pangea-sdk 3.0.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)