Skip to content

Commit 97abe6d

Browse files
author
Dan Hertz
authored
Merge pull request #25 from nightfallai/dan/plat-1347-add-support-for-429s-in-python
add 429 retry and more unit tests
2 parents eac688e + ebae63e commit 97abe6d

File tree

5 files changed

+251
-45
lines changed

5 files changed

+251
-45
lines changed

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ autopep8
1313
pytest
1414
pytest-cov
1515
requests
16+
responses
1617
freezegun

nightfall/api.py

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
~~~~~~~~~~~~~
55
This module provides a class which abstracts the Nightfall REST API.
66
"""
7+
import time
78
from datetime import datetime, timedelta
89
import hmac
910
import hashlib
1011
import json
1112
import logging
1213
import os
14+
from functools import wraps
1315
from typing import List, Tuple, Optional
1416

1517
import requests
18+
from requests.adapters import HTTPAdapter
19+
from urllib3 import Retry
1620

1721
from nightfall.detection_rules import DetectionRule
1822
from nightfall.exceptions import NightfallUserError, NightfallSystemError
@@ -43,13 +47,16 @@ def __init__(self, key: Optional[str] = None, signing_secret: Optional[str] = No
4347
raise NightfallUserError("need an API key either in constructor or in NIGHTFALL_API_KEY environment var",
4448
40001)
4549

46-
self._headers = {
50+
self.signing_secret = signing_secret
51+
self.logger = logging.getLogger(__name__)
52+
self.session = requests.Session()
53+
retries = Retry(total=5, allowed_methods=Retry.DEFAULT_ALLOWED_METHODS | {"PATCH", "POST"})
54+
self.session.mount('https://', HTTPAdapter(max_retries=retries))
55+
self.session.headers = {
4756
"Content-Type": "application/json",
4857
"User-Agent": "nightfall-python-sdk/1.1.1",
4958
'Authorization': f'Bearer {self.key}',
5059
}
51-
self.signing_secret = signing_secret
52-
self.logger = logging.getLogger(__name__)
5360

5461
def scan_text(self, texts: List[str], detection_rules: Optional[List[DetectionRule]] = None,
5562
detection_rule_uuids: Optional[List[str]] = None, context_bytes: Optional[int] = None) ->\
@@ -97,11 +104,7 @@ def scan_text(self, texts: List[str], detection_rules: Optional[List[DetectionRu
97104
return findings, parsed_response.get("redactedPayload")
98105

99106
def _scan_text_v3(self, data: dict):
100-
response = requests.post(
101-
url=self.TEXT_SCAN_ENDPOINT_V3,
102-
headers=self._headers,
103-
data=json.dumps(data)
104-
)
107+
response = self.session.post(url=self.TEXT_SCAN_ENDPOINT_V3, data=json.dumps(data))
105108

106109
self.logger.debug(f"HTTP Request URL: {response.request.url}")
107110
self.logger.debug(f"HTTP Request Body: {response.request.body}")
@@ -160,11 +163,7 @@ def _file_scan_initialize(self, location: str):
160163
data = {
161164
"fileSizeBytes": os.path.getsize(location)
162165
}
163-
response = requests.post(
164-
url=self.FILE_SCAN_INITIALIZE_ENDPOINT,
165-
headers=self._headers,
166-
data=json.dumps(data)
167-
)
166+
response = self.session.post(url=self.FILE_SCAN_INITIALIZE_ENDPOINT, data=json.dumps(data))
168167

169168
return response
170169

@@ -180,7 +179,7 @@ def read_chunks(fp, chunk_size):
180179
ix = ix + 1
181180

182181
def upload_chunk(id, data, headers):
183-
response = requests.patch(
182+
response = self.session.patch(
184183
url=self.FILE_SCAN_UPLOAD_ENDPOINT.format(id),
185184
data=data,
186185
headers=headers
@@ -189,18 +188,14 @@ def upload_chunk(id, data, headers):
189188

190189
with open(location, 'rb') as fp:
191190
for ix, piece in read_chunks(fp, chunk_size):
192-
headers = self._headers
193-
headers["X-UPLOAD-OFFSET"] = str(ix * chunk_size)
191+
headers = {"X-UPLOAD-OFFSET": str(ix * chunk_size)}
194192
response = upload_chunk(session_id, piece, headers)
195193
_validate_response(response, 204)
196194

197195
return True
198196

199197
def _file_scan_finalize(self, session_id: str):
200-
response = requests.post(
201-
url=self.FILE_SCAN_COMPLETE_ENDPOINT.format(session_id),
202-
headers=self._headers
203-
)
198+
response = self.session.post(url=self.FILE_SCAN_COMPLETE_ENDPOINT.format(session_id))
204199
return response
205200

206201
def _file_scan_scan(self, session_id: str, detection_rules: Optional[List[DetectionRule]] = None,
@@ -215,11 +210,7 @@ def _file_scan_scan(self, session_id: str, detection_rules: Optional[List[Detect
215210
if detection_rules:
216211
data["policy"]["detectionRules"] = [d.as_dict() for d in detection_rules]
217212

218-
response = requests.post(
219-
url=self.FILE_SCAN_SCAN_ENDPOINT.format(session_id),
220-
headers=self._headers,
221-
data=json.dumps(data)
222-
)
213+
response = self.session.post(url=self.FILE_SCAN_SCAN_ENDPOINT.format(session_id), data=json.dumps(data))
223214
return response
224215

225216
def validate_webhook(self, request_signature: str, request_timestamp: str, request_data: str) -> bool:

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[pytest]
22
markers =
33
filetest: marks tests as requiring a valid webhook to run
4+
integration: marks tests as calling out to the nightfall api to run

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def readme():
3232
packages=find_packages(exclude=['tests*']),
3333
install_requires=[
3434
'requests',
35+
'urllib3'
3536
],
3637
python_requires='>=3.7.*'
3738
)

0 commit comments

Comments
 (0)