pangea-sdk 3.7.0__py3-none-any.whl → 3.7.1__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.
@@ -2,12 +2,15 @@
2
2
  # Author: Pangea Cyber Corporation
3
3
  import datetime
4
4
  import json
5
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union
6
6
 
7
7
  import pangea.exceptions as pexc
8
8
  from pangea.response import PangeaResponse
9
9
  from pangea.services.audit.exceptions import AuditException, EventCorruption
10
10
  from pangea.services.audit.models import (
11
+ DownloadFormat,
12
+ DownloadRequest,
13
+ DownloadResult,
11
14
  Event,
12
15
  EventEnvelope,
13
16
  EventVerification,
@@ -50,7 +53,7 @@ class AuditBase:
50
53
  def __init__(
51
54
  self, private_key_file: str = "", public_key_info: Dict[str, str] = {}, tenant_id: Optional[str] = None
52
55
  ):
53
- self.pub_roots: Dict[int, Root] = {}
56
+ self.pub_roots: Dict[int, PublishedRoot] = {}
54
57
  self.buffer_data: Optional[str] = None
55
58
  self.signer: Optional[Signer] = Signer(private_key_file) if private_key_file else None
56
59
  self.public_key_info = public_key_info
@@ -182,8 +185,6 @@ class AuditBase:
182
185
  unpublished_root = response.result.unpublished_root # type: ignore[union-attr]
183
186
 
184
187
  if verify_consistency:
185
- self.update_published_roots(response.result) # type: ignore[arg-type]
186
-
187
188
  for search_event in response.result.events: # type: ignore[union-attr]
188
189
  # verify membership proofs
189
190
  if self.can_verify_membership_proof(search_event):
@@ -201,22 +202,9 @@ class AuditBase:
201
202
 
202
203
  return response
203
204
 
204
- def update_published_roots(self, result: SearchOutput):
205
- """Fetches series of published root hashes from Arweave
206
-
207
- This is used for subsequent calls to verify_consistency_proof(). Root hashes
208
- are published on [Arweave](https://arweave.net).
209
-
210
- Args:
211
- result (SearchOutput): PangeaResponse object from previous call to audit.search()
212
-
213
- Raises:
214
- AuditException: If an audit based api exception happens
215
- PangeaAPIException: If an API Error happens
216
- """
217
-
205
+ def _get_tree_sizes_and_roots(self, result: SearchResultOutput) -> Tuple[Set[int], Dict[int, PublishedRoot]]:
218
206
  if not result.root:
219
- return
207
+ return set(), {}
220
208
 
221
209
  tree_sizes = set()
222
210
  for search_event in result.events:
@@ -230,22 +218,11 @@ class AuditBase:
230
218
  tree_sizes.difference_update(self.pub_roots.keys())
231
219
 
232
220
  if tree_sizes:
233
- arweave_roots = get_arweave_published_roots(result.root.tree_name, list(tree_sizes)) # + [result.count])
221
+ arweave_roots = get_arweave_published_roots(result.root.tree_name, list(tree_sizes))
234
222
  else:
235
223
  arweave_roots = {}
236
224
 
237
- # fill the missing roots from the server (if allowed)
238
- for tree_size in tree_sizes:
239
- pub_root = None
240
- if tree_size in arweave_roots:
241
- pub_root = PublishedRoot(**arweave_roots[tree_size].dict(exclude_none=True))
242
- pub_root.source = RootSource.ARWEAVE
243
- elif self.allow_server_roots:
244
- resp = self.root(tree_size=tree_size) # type: ignore[attr-defined]
245
- if resp.success:
246
- pub_root = PublishedRoot(**resp.result.data.dict(exclude_none=True))
247
- pub_root.source = RootSource.PANGEA
248
- self.pub_roots[tree_size] = pub_root # type: ignore[assignment]
225
+ return tree_sizes, arweave_roots
249
226
 
250
227
  def can_verify_membership_proof(self, event: SearchEvent) -> bool:
251
228
  """
@@ -303,7 +280,7 @@ class AuditBase:
303
280
  """
304
281
  return event.published and event.leaf_index is not None and event.leaf_index >= 0 # type: ignore[return-value]
305
282
 
306
- def verify_consistency_proof(self, pub_roots: Dict[int, Root], event: SearchEvent) -> bool:
283
+ def verify_consistency_proof(self, pub_roots: Dict[int, PublishedRoot], event: SearchEvent) -> bool:
307
284
  """
308
285
  Verify consistency proof
309
286
 
@@ -329,7 +306,7 @@ class AuditBase:
329
306
  return False
330
307
 
331
308
  if not self.allow_server_roots and (
332
- curr_root.source != RootSource.ARWEAVE or prev_root.source != RootSource.ARWEAVE # type: ignore[attr-defined]
309
+ curr_root.source != RootSource.ARWEAVE or prev_root.source != RootSource.ARWEAVE
333
310
  ):
334
311
  return False
335
312
 
@@ -518,8 +495,8 @@ class Audit(ServiceBase, AuditBase):
518
495
  """
519
496
 
520
497
  input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
521
- response = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
522
- if response.success:
498
+ response: PangeaResponse[LogResult] = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
499
+ if response.success and response.result is not None:
523
500
  self._process_log_result(response.result, verify=verify)
524
501
  return response
525
502
 
@@ -557,9 +534,11 @@ class Audit(ServiceBase, AuditBase):
557
534
  """
558
535
 
559
536
  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))
537
+ response: PangeaResponse[LogBulkResult] = self.request.post(
538
+ "v2/log", LogBulkResult, data=input.dict(exclude_none=True)
539
+ )
561
540
 
562
- if response.success:
541
+ if response.success and response.result is not None:
563
542
  for result in response.result.results:
564
543
  self._process_log_result(result, verify=True)
565
544
  return response
@@ -599,13 +578,13 @@ class Audit(ServiceBase, AuditBase):
599
578
 
600
579
  try:
601
580
  # Calling to v2 methods will return always a 202.
602
- response = self.request.post(
581
+ response: PangeaResponse[LogBulkResult] = self.request.post(
603
582
  "v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
604
583
  )
605
584
  except pexc.AcceptedRequestException as e:
606
585
  return e.response
607
586
 
608
- if response.success:
587
+ if response.success and response.result is not None:
609
588
  for result in response.result.results:
610
589
  self._process_log_result(result, verify=True)
611
590
  return response
@@ -619,7 +598,7 @@ class Audit(ServiceBase, AuditBase):
619
598
  end: Optional[Union[datetime.datetime, str]] = None,
620
599
  limit: Optional[int] = None,
621
600
  max_results: Optional[int] = None,
622
- search_restriction: Optional[dict] = None,
601
+ search_restriction: Optional[Dict[str, Sequence[str]]] = None,
623
602
  verbose: Optional[bool] = None,
624
603
  verify_consistency: bool = False,
625
604
  verify_events: bool = True,
@@ -648,7 +627,7 @@ class Audit(ServiceBase, AuditBase):
648
627
  end (datetime, optional): An RFC-3339 formatted timestamp, or relative time adjustment from the current time.
649
628
  limit (int, optional): Optional[int] = None,
650
629
  max_results (int, optional): Maximum number of results to return.
651
- search_restriction (dict, optional): A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
630
+ search_restriction (Dict[str, Sequence[str]], optional): A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
652
631
  verbose (bool, optional): If true, response include root and membership and consistency proofs.
653
632
  verify_consistency (bool): True to verify logs consistency
654
633
  verify_events (bool): True to verify hash events and signatures
@@ -687,7 +666,11 @@ class Audit(ServiceBase, AuditBase):
687
666
  verbose=verbose,
688
667
  )
689
668
 
690
- response = self.request.post("v1/search", SearchOutput, data=input.dict(exclude_none=True))
669
+ response: PangeaResponse[SearchOutput] = self.request.post(
670
+ "v1/search", SearchOutput, data=input.dict(exclude_none=True)
671
+ )
672
+ if verify_consistency and response.result is not None:
673
+ self.update_published_roots(response.result)
691
674
  return self.handle_search_response(response, verify_consistency, verify_events)
692
675
 
693
676
  def results(
@@ -695,6 +678,7 @@ class Audit(ServiceBase, AuditBase):
695
678
  id: str,
696
679
  limit: Optional[int] = 20,
697
680
  offset: Optional[int] = 0,
681
+ assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None,
698
682
  verify_consistency: bool = False,
699
683
  verify_events: bool = True,
700
684
  ) -> PangeaResponse[SearchResultOutput]:
@@ -709,6 +693,7 @@ class Audit(ServiceBase, AuditBase):
709
693
  id (string): the id of a search action, found in `response.result.id`
710
694
  limit (integer, optional): the maximum number of results to return, default is 20
711
695
  offset (integer, optional): the position of the first result to return, default is 0
696
+ assert_search_restriction (Dict[str, Sequence[str]], optional): Assert the requested search results were queried with the exact same search restrictions, to ensure the results comply to the expected restrictions.
712
697
  verify_consistency (bool): True to verify logs consistency
713
698
  verify_events (bool): True to verify hash events and signatures
714
699
  Raises:
@@ -733,8 +718,13 @@ class Audit(ServiceBase, AuditBase):
733
718
  id=id,
734
719
  limit=limit,
735
720
  offset=offset,
721
+ assert_search_restriction=assert_search_restriction,
722
+ )
723
+ response: PangeaResponse[SearchResultOutput] = self.request.post(
724
+ "v1/results", SearchResultOutput, data=input.dict(exclude_none=True)
736
725
  )
737
- response = self.request.post("v1/results", SearchResultOutput, data=input.dict(exclude_none=True))
726
+ if verify_consistency and response.result is not None:
727
+ self.update_published_roots(response.result)
738
728
  return self.handle_results_response(response, verify_consistency, verify_events)
739
729
 
740
730
  def root(self, tree_size: Optional[int] = None) -> PangeaResponse[RootResult]:
@@ -760,3 +750,67 @@ class Audit(ServiceBase, AuditBase):
760
750
  """
761
751
  input = RootRequest(tree_size=tree_size)
762
752
  return self.request.post("v1/root", RootResult, data=input.dict(exclude_none=True))
753
+
754
+ def download_results(
755
+ self, result_id: str, format: Optional[DownloadFormat] = None
756
+ ) -> PangeaResponse[DownloadResult]:
757
+ """
758
+ Download search results
759
+
760
+ Get all search results as a compressed (gzip) CSV file.
761
+
762
+ OperationId: audit_post_v1_download_results
763
+
764
+ Args:
765
+ result_id: ID returned by the search API.
766
+ format: Format for the records.
767
+
768
+ Returns:
769
+ URL where search results can be downloaded.
770
+
771
+ Raises:
772
+ AuditException: If an Audit-based API exception occurs.
773
+ PangeaAPIException: If an API exception occurs.
774
+
775
+ Examples:
776
+ response = audit.download_results(
777
+ result_id="pas_[...]",
778
+ format=DownloadFormat.JSON,
779
+ )
780
+ """
781
+
782
+ input = DownloadRequest(result_id=result_id, format=format)
783
+ return self.request.post("v1/download_results", DownloadResult, data=input.dict(exclude_none=True))
784
+
785
+ def update_published_roots(self, result: SearchResultOutput):
786
+ """Fetches series of published root hashes from Arweave
787
+
788
+ This is used for subsequent calls to verify_consistency_proof(). Root hashes
789
+ are published on [Arweave](https://arweave.net).
790
+
791
+ Args:
792
+ result (SearchResultOutput): Result object from previous call to Audit.search() or Audit.results()
793
+
794
+ Raises:
795
+ AuditException: If an audit based api exception happens
796
+ PangeaAPIException: If an API Error happens
797
+ """
798
+
799
+ if not result.root:
800
+ return
801
+
802
+ tree_sizes, arweave_roots = self._get_tree_sizes_and_roots(result)
803
+
804
+ # fill the missing roots from the server (if allowed)
805
+ for tree_size in tree_sizes:
806
+ pub_root = None
807
+ if tree_size in arweave_roots:
808
+ pub_root = PublishedRoot(**arweave_roots[tree_size].dict(exclude_none=True))
809
+ pub_root.source = RootSource.ARWEAVE
810
+ elif self.allow_server_roots:
811
+ resp = self.root(tree_size=tree_size)
812
+ if resp.success and resp.result is not None:
813
+ pub_root = PublishedRoot(**resp.result.data.dict(exclude_none=True))
814
+ pub_root.source = RootSource.PANGEA
815
+ if pub_root is not None:
816
+ self.pub_roots[tree_size] = pub_root
@@ -1,6 +1,5 @@
1
1
  from pangea.exceptions import PangeaException
2
-
3
- from .models import EventEnvelope
2
+ from pangea.services.audit.models import EventEnvelope
4
3
 
5
4
 
6
5
  # Audit SDK Specific Exceptions
@@ -2,7 +2,7 @@
2
2
  # Author: Pangea Cyber Corporation
3
3
  import datetime
4
4
  import enum
5
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Dict, List, Optional, Sequence, Union
6
6
 
7
7
  from pangea.response import APIRequestModel, APIResponseModel, PangeaResponseResult
8
8
 
@@ -206,7 +206,7 @@ class SearchRestriction(APIResponseModel):
206
206
  Arguments:
207
207
  actor -- List of actors to include in search. If empty include all.
208
208
  action -- List of action to include in search. If empty include all.
209
- source -- List of sourcers to include in search. If empty include all.
209
+ source -- List of sources to include in search. If empty include all.
210
210
  status -- List of status to include in search. If empty include all.
211
211
  target -- List of targets to include in search. If empty include all.
212
212
  """
@@ -279,7 +279,7 @@ class SearchRequest(APIRequestModel):
279
279
  end: Optional[str] = None
280
280
  limit: Optional[int] = None
281
281
  max_results: Optional[int] = None
282
- search_restriction: Optional[dict] = None
282
+ search_restriction: Optional[Dict[str, Sequence[str]]] = None
283
283
  verbose: Optional[bool] = None
284
284
 
285
285
 
@@ -373,7 +373,24 @@ class SearchEvent(APIResponseModel):
373
373
  signature_verification: EventVerification = EventVerification.NONE
374
374
 
375
375
 
376
- class SearchOutput(PangeaResponseResult):
376
+ class SearchResultOutput(PangeaResponseResult):
377
+ """
378
+ Return class after a pagination search
379
+
380
+ Arguments:
381
+ count -- The total number of results that were returned by the search.
382
+ events -- A list of matching audit records.
383
+ root -- A root of a Merkle Tree.
384
+ unpublished_root -- Root of a unpublished Merkle Tree
385
+ """
386
+
387
+ count: int
388
+ events: List[SearchEvent]
389
+ root: Optional[Root] = None
390
+ unpublished_root: Optional[Root] = None
391
+
392
+
393
+ class SearchOutput(SearchResultOutput):
377
394
  """
378
395
  Result class after an audit search action
379
396
 
@@ -386,12 +403,8 @@ class SearchOutput(PangeaResponseResult):
386
403
  unpublished_root -- Root of a unpublished Merkle Tree
387
404
  """
388
405
 
389
- count: int
390
- events: List[SearchEvent]
391
406
  id: str
392
407
  expires_at: datetime.datetime
393
- root: Optional[Root] = None
394
- unpublished_root: Optional[Root] = None
395
408
 
396
409
 
397
410
  class SearchResultRequest(APIRequestModel):
@@ -402,25 +415,37 @@ class SearchResultRequest(APIRequestModel):
402
415
  id -- A search results identifier returned by the search call.
403
416
  limit -- Number of audit records to include from the first page of the results.
404
417
  offset -- Offset from the start of the result set to start returning results from.
418
+ assert_search_restriction -- Assert the requested search results were queried with the exact same search restrictions, to ensure the results comply to the expected restrictions.
405
419
  """
406
420
 
407
421
  id: str
408
422
  limit: Optional[int] = 20
409
423
  offset: Optional[int] = 0
424
+ assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None
410
425
 
411
426
 
412
- class SearchResultOutput(PangeaResponseResult):
413
- """
414
- Return class after a pagination search
427
+ class DownloadFormat(str, enum.Enum):
428
+ JSON = "json"
429
+ """JSON."""
415
430
 
416
- Arguments:
417
- count -- The total number of results that were returned by the search.
418
- events -- A list of matching audit records.
419
- root -- A root of a Merkle Tree.
420
- unpublished_root -- Root of a unpublished Merkle Tree
421
- """
431
+ CSV = "csv"
432
+ """CSV."""
422
433
 
423
- count: int
424
- events: List[SearchEvent]
425
- root: Optional[Root] = None
426
- unpublished_root: Optional[Root] = None
434
+ def __str__(self):
435
+ return str(self.value)
436
+
437
+ def __repr__(self):
438
+ return str(self.value)
439
+
440
+
441
+ class DownloadRequest(APIRequestModel):
442
+ result_id: str
443
+ """ID returned by the search API."""
444
+
445
+ format: Optional[str] = None
446
+ """Format for the records."""
447
+
448
+
449
+ class DownloadResult(PangeaResponseResult):
450
+ dest_url: str
451
+ """URL where search results can be downloaded."""
@@ -7,6 +7,7 @@ from cryptography import exceptions
7
7
  from cryptography.hazmat.primitives import serialization
8
8
  from cryptography.hazmat.primitives.asymmetric import ed25519
9
9
  from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, PublicKeyTypes
10
+
10
11
  from pangea.exceptions import PangeaException
11
12
  from pangea.services.audit.util import b64decode, b64decode_ascii, b64encode_ascii
12
13
  from pangea.services.vault.models.common import AsymmetricAlgorithm
@@ -10,6 +10,7 @@ from hashlib import sha256
10
10
  from typing import Dict, List, Optional
11
11
 
12
12
  import requests
13
+
13
14
  from pangea.services.audit.models import Event, EventEnvelope, PublishedRoot
14
15
  from pangea.utils import default_encoder, format_datetime
15
16
 
@@ -113,6 +113,10 @@ class AuthN(ServiceBase):
113
113
  Examples:
114
114
  response = authn.session.list()
115
115
  """
116
+
117
+ if isinstance(filter, dict):
118
+ filter = m.SessionListFilter(**filter)
119
+
116
120
  input = m.SessionListRequest(filter=filter, last=last, order=order, order_by=order_by, size=size)
117
121
  return self.request.post("v2/session/list", m.SessionListResults, data=input.dict(exclude_none=True))
118
122
 
@@ -267,6 +271,10 @@ class AuthN(ServiceBase):
267
271
  token="ptu_wuk7tvtpswyjtlsx52b7yyi2l7zotv4a",
268
272
  )
269
273
  """
274
+
275
+ if isinstance(filter, dict):
276
+ filter = m.SessionListFilter(**filter)
277
+
270
278
  input = m.ClientSessionListRequest(
271
279
  token=token, filter=filter, last=last, order=order, order_by=order_by, size=size
272
280
  )
@@ -589,6 +597,10 @@ class AuthN(ServiceBase):
589
597
  Examples:
590
598
  response = authn.user.list()
591
599
  """
600
+
601
+ if isinstance(filter, dict):
602
+ filter = m.UserListFilter(**filter)
603
+
592
604
  input = m.UserListRequest(
593
605
  filter=filter,
594
606
  last=last,
@@ -638,6 +650,9 @@ class AuthN(ServiceBase):
638
650
  Examples:
639
651
  response = authn.user.invites.list()
640
652
  """
653
+ if isinstance(filter, dict):
654
+ filter = m.UserInviteListFilter(**filter)
655
+
641
656
  input = m.UserInviteListRequest(filter=filter, last=last, order=order, order_by=order_by, size=size)
642
657
  return self.request.post(
643
658
  "v2/user/invite/list", m.UserInviteListResult, data=input.dict(exclude_none=True)
@@ -1061,6 +1076,9 @@ class AuthN(ServiceBase):
1061
1076
  response = authn.agreements.list()
1062
1077
  """
1063
1078
 
1079
+ if isinstance(filter, dict):
1080
+ filter = m.AgreementListFilter(**filter)
1081
+
1064
1082
  input = m.AgreementListRequest(filter=filter, last=last, order=order, order_by=order_by, size=size)
1065
1083
  return self.request.post("v2/agreements/list", m.AgreementListResult, data=input.dict(exclude_none=True))
1066
1084
 
@@ -51,7 +51,7 @@ class Intelligence(PangeaResponseResult):
51
51
  user_intel: bool
52
52
 
53
53
 
54
- class SessionToken(APIResponseModel):
54
+ class SessionToken(PangeaResponseResult):
55
55
  id: str
56
56
  type: str
57
57
  life: int
@@ -68,8 +68,8 @@ class LoginToken(SessionToken):
68
68
  token: str
69
69
 
70
70
 
71
- class ClientTokenCheckResult(LoginToken):
72
- pass
71
+ class ClientTokenCheckResult(SessionToken):
72
+ token: Optional[str]
73
73
 
74
74
 
75
75
  class IDProvider(str, enum.Enum):
@@ -234,7 +234,7 @@ class UserListFilter(APIRequestModel):
234
234
 
235
235
 
236
236
  class UserListRequest(APIRequestModel):
237
- filter: Optional[Union[Dict, UserListFilter]] = None
237
+ filter: Optional[UserListFilter] = None
238
238
  last: Optional[str] = None
239
239
  order: Optional[ItemOrder] = None
240
240
  order_by: Optional[UserListOrderBy] = None
@@ -289,7 +289,7 @@ class UserInviterOrderBy(enum.Enum):
289
289
 
290
290
 
291
291
  class UserInviteListFilter(APIRequestModel):
292
- callback: Optional[str] = None
292
+ callback: Optional[str]
293
293
  callback__contains: Optional[List[str]] = None
294
294
  callback__in: Optional[List[str]] = None
295
295
  created_at: Optional[str] = None
@@ -322,7 +322,7 @@ class UserInviteListFilter(APIRequestModel):
322
322
 
323
323
 
324
324
  class UserInviteListRequest(APIRequestModel):
325
- filter: Optional[Union[Dict, UserInviteListFilter]] = None
325
+ filter: Optional[UserInviteListFilter] = None
326
326
  last: Optional[str] = None
327
327
  order: Optional[ItemOrder] = None
328
328
  order_by: Optional[UserInviterOrderBy] = None
@@ -607,7 +607,7 @@ class SessionListFilter(APIRequestModel):
607
607
 
608
608
  class ClientSessionListRequest(APIRequestModel):
609
609
  token: str
610
- filter: Optional[Union[Dict, SessionListFilter]] = None
610
+ filter: Optional[SessionListFilter] = None
611
611
  last: Optional[str] = None
612
612
  order: Optional[ItemOrder] = None
613
613
  order_by: Optional[SessionListOrderBy] = None
@@ -663,7 +663,7 @@ class SessionInvalidateResult(PangeaResponseResult):
663
663
 
664
664
 
665
665
  class SessionListRequest(APIRequestModel):
666
- filter: Optional[Union[Dict, SessionListFilter]] = None
666
+ filter: Optional[SessionListFilter] = None
667
667
  last: Optional[str] = None
668
668
  order: Optional[ItemOrder] = None
669
669
  order_by: Optional[SessionListOrderBy] = None
@@ -760,7 +760,7 @@ class AgreementListFilter(APIRequestModel):
760
760
 
761
761
 
762
762
  class AgreementListRequest(APIRequestModel):
763
- filter: Optional[Union[Dict, AgreementListFilter]] = None
763
+ filter: Optional[AgreementListFilter] = None
764
764
  last: Optional[str] = None
765
765
  order: Optional[ItemOrder] = None
766
766
  order_by: Optional[AgreementListOrderBy] = None
pangea/services/base.py CHANGED
@@ -3,13 +3,13 @@
3
3
 
4
4
  import copy
5
5
  import logging
6
- from typing import Optional, Type, Union
6
+ from typing import Dict, Optional, Type, Union
7
7
 
8
8
  from pangea.asyncio.request import PangeaRequestAsync
9
9
  from pangea.config import PangeaConfig
10
10
  from pangea.exceptions import AcceptedRequestException
11
11
  from pangea.request import PangeaRequest
12
- from pangea.response import PangeaResponse, PangeaResponseResult
12
+ from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult
13
13
 
14
14
 
15
15
  class ServiceBase(object):
@@ -21,12 +21,12 @@ class ServiceBase(object):
21
21
  if not token:
22
22
  raise Exception("No token provided")
23
23
 
24
- self.config = config if copy.deepcopy(config) else PangeaConfig()
24
+ self.config = copy.deepcopy(config) if config is not None else PangeaConfig()
25
25
  self.logger = logging.getLogger(logger_name)
26
26
  self._token = token
27
- self.config_id: Optional[None] = config_id # type: ignore[assignment]
28
- self._request: Union[PangeaRequest, PangeaRequestAsync] = None # type: ignore[assignment]
29
- extra_headers = {} # type: ignore[var-annotated]
27
+ self.config_id: Optional[str] = config_id
28
+ self._request: Optional[Union[PangeaRequest, PangeaRequestAsync]] = None
29
+ extra_headers: Dict = {}
30
30
  self.request.set_extra_headers(extra_headers)
31
31
 
32
32
  @property
@@ -38,8 +38,8 @@ class ServiceBase(object):
38
38
  self._token = value
39
39
 
40
40
  @property
41
- def request(self):
42
- if not self._request:
41
+ def request(self) -> PangeaRequest:
42
+ if self._request is None or isinstance(self._request, PangeaRequestAsync):
43
43
  self._request = PangeaRequest(
44
44
  config=self.config,
45
45
  token=self.token,
@@ -55,7 +55,7 @@ class ServiceBase(object):
55
55
  exception: Optional[AcceptedRequestException] = None,
56
56
  response: Optional[PangeaResponse] = None,
57
57
  request_id: Optional[str] = None,
58
- result_class: Union[Type[PangeaResponseResult], dict] = dict, # type: ignore[assignment]
58
+ result_class: Union[Type[PangeaResponseResult], Type[Dict]] = dict,
59
59
  ) -> PangeaResponse:
60
60
  """
61
61
  Poll result
@@ -82,3 +82,6 @@ class ServiceBase(object):
82
82
  return self.request.poll_result_by_id(request_id=request_id, result_class=result_class, check_response=True)
83
83
  else:
84
84
  raise AttributeError("Need to set exception, response or request_id")
85
+
86
+ def download_file(self, url: str, filename: Optional[str] = None) -> AttachedFile:
87
+ return self.request.download_file(url=url, filename=filename)
@@ -3,8 +3,7 @@
3
3
  from typing import Any, Dict, List
4
4
 
5
5
  from pangea.response import APIRequestModel, APIResponseModel, PangeaResponse, PangeaResponseResult
6
-
7
- from .base import ServiceBase
6
+ from pangea.services.base import ServiceBase
8
7
 
9
8
 
10
9
  class IPCheckRequest(APIRequestModel):
@@ -2,14 +2,13 @@
2
2
  # Author: Pangea Cyber Corporation
3
3
  import io
4
4
  import logging
5
- from typing import Dict, List, Optional
5
+ from typing import Dict, List, Optional, Tuple
6
6
 
7
7
  from pangea.request import PangeaConfig, PangeaRequest
8
8
  from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult, TransferMethod
9
+ from pangea.services.base import ServiceBase
9
10
  from pangea.utils import FileUploadParams, get_file_upload_params
10
11
 
11
- from .base import ServiceBase
12
-
13
12
 
14
13
  class FileScanRequest(APIRequestModel):
15
14
  """
@@ -129,7 +128,7 @@ class FileScan(ServiceBase):
129
128
  size = params.size
130
129
  else:
131
130
  crc, sha, size = None, None, None
132
- files = [("upload", ("filename", file, "application/octet-stream"))]
131
+ files: List[Tuple] = [("upload", ("filename", file, "application/octet-stream"))]
133
132
  else:
134
133
  raise ValueError("Need to set file_path or file arguments")
135
134
 
pangea/services/intel.py CHANGED
@@ -6,10 +6,9 @@ from typing import Dict, List, Optional
6
6
 
7
7
  from pangea.exceptions import PangeaException
8
8
  from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult
9
+ from pangea.services.base import ServiceBase
9
10
  from pangea.utils import hash_256_filepath
10
11
 
11
- from .base import ServiceBase
12
-
13
12
 
14
13
  class IntelCommonRequest(APIRequestModel):
15
14
  """
pangea/services/redact.py CHANGED
@@ -5,8 +5,7 @@ import enum
5
5
  from typing import Dict, List, Optional, Union
6
6
 
7
7
  from pangea.response import APIRequestModel, APIResponseModel, PangeaResponse, PangeaResponseResult
8
-
9
- from .base import ServiceBase
8
+ from pangea.services.base import ServiceBase
10
9
 
11
10
 
12
11
  class RedactFormat(str, enum.Enum):