Skip to content

Commit 1393024

Browse files
Add large file upload handling to tiscale.py
Bump version to 2.11.1
1 parent dd1ee50 commit 1393024

File tree

4 files changed

+134
-19
lines changed

4 files changed

+134
-19
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,14 @@ v2.5.1 (2024-04-02)
477477
- Fixed minor issues in the `advanced_search_using_network_indicators.ipynb` notebook.
478478

479479

480+
2.11.1 (2025-10-21)
481+
-------------------
482+
#### Improvements
483+
- **tiscale** module:
484+
- Added the `large_file` argument to upload methods. If this argument is set to `True`, the method will use the `MultipartEncoder` from `requests-toolbelt` to upload a large file to Spectra Detect in order not to run out of memory.
485+
486+
- `requests-toolbelt` added as an optional dependency under the `large_files` key.
487+
480488

481489
-------------------
482490
### Scheduled removals

ReversingLabs/SDK/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
A Python SDK for communicating with ReversingLabs services.
66
"""
77

8-
__version__ = "2.11.0"
8+
__version__ = "2.11.1"

ReversingLabs/SDK/tiscale.py

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
from ReversingLabs.SDK.helper import DEFAULT_USER_AGENT, RESPONSE_CODE_ERROR_MAP, \
1414
RequestTimeoutError, WrongInputError
1515

16+
try:
17+
from requests_toolbelt import MultipartEncoder
18+
_TOOLBELT_INSTALLED = True
19+
except ImportError:
20+
_TOOLBELT_INSTALLED = False
21+
1622

1723
class TitaniumScale(object):
1824

@@ -81,7 +87,7 @@ def test_connection(self):
8187

8288
return
8389

84-
def upload_sample_from_path(self, file_path, custom_token=None, user_data=None, custom_data=None):
90+
def upload_sample_from_path(self, file_path, custom_token=None, user_data=None, custom_data=None, large_file=False):
8591
"""Accepts a file path string for file upload and returns a response.
8692
:param file_path: path to file
8793
:type file_path: str
@@ -93,6 +99,8 @@ def upload_sample_from_path(self, file_path, custom_token=None, user_data=None,
9399
:param custom_data: user-defined data in the form of a JSON string; this data is
94100
included in file analysis reports
95101
:type custom_data: str
102+
:param large_file: Set to True if file size exceeds 1 GB
103+
:type large_file: bool
96104
:return: response
97105
:rtype: requests.Response
98106
"""
@@ -104,18 +112,28 @@ def upload_sample_from_path(self, file_path, custom_token=None, user_data=None,
104112
except IOError as error:
105113
raise WrongInputError("Error while opening file in 'rb' mode - {error}".format(error=str(error)))
106114

107-
response = self.__upload_files(
108-
file_handle=file_handle,
109-
custom_token=custom_token,
110-
user_data=user_data,
111-
custom_data=custom_data
112-
)
115+
if large_file:
116+
response = self.__upload_large_file(
117+
file_handle=file_handle,
118+
custom_token=custom_token,
119+
user_data=user_data,
120+
custom_data=custom_data
121+
)
122+
123+
else:
124+
response = self.__upload_files(
125+
file_handle=file_handle,
126+
custom_token=custom_token,
127+
user_data=user_data,
128+
custom_data=custom_data
129+
)
113130

114131
self.__raise_on_error(response)
115132

116133
return response
117134

118-
def upload_sample_from_file(self, file_source, custom_token=None, user_data=None, custom_data=None):
135+
def upload_sample_from_file(self, file_source, custom_token=None, user_data=None, custom_data=None,
136+
large_file=False):
119137
"""Accepts an open file in 'rb' mode for file upload and returns a response.
120138
:param file_source: open file
121139
:type file_source: file or BinaryIO
@@ -127,18 +145,29 @@ def upload_sample_from_file(self, file_source, custom_token=None, user_data=None
127145
:param custom_data: user-defined data in the form of a JSON string; this data is
128146
included in file analysis reports
129147
:type custom_data: str
148+
:param large_file: Set to True if file size exceeds 1 GB
149+
:type large_file: bool
130150
:return: response
131151
:rtype: requests.Response
132152
"""
133153
if not hasattr(file_source, "read"):
134154
raise WrongInputError("file_source parameter must be a file open in 'rb' mode.")
135155

136-
response = self.__upload_files(
137-
file_handle=file_source,
138-
custom_token=custom_token,
139-
user_data=user_data,
140-
custom_data=custom_data
141-
)
156+
if large_file:
157+
response = self.__upload_large_file(
158+
file_handle=file_source,
159+
custom_token=custom_token,
160+
user_data=user_data,
161+
custom_data=custom_data
162+
)
163+
164+
else:
165+
response = self.__upload_files(
166+
file_handle=file_source,
167+
custom_token=custom_token,
168+
user_data=user_data,
169+
custom_data=custom_data
170+
)
142171

143172
self.__raise_on_error(response)
144173

@@ -174,7 +203,7 @@ def get_results(self, task_url, full_report=False):
174203
return None
175204

176205
def upload_sample_and_get_results(self, file_path=None, file_source=None, full_report=False, custom_token=None,
177-
user_data=None, custom_data=None):
206+
user_data=None, custom_data=None, large_file=False):
178207
"""Accepts either a file path string or an open file in 'rb' mode for file upload
179208
and returns an analysis report response.
180209
This method combines uploading a sample and obtaining the analysis results.
@@ -194,6 +223,8 @@ def upload_sample_and_get_results(self, file_path=None, file_source=None, full_r
194223
:param custom_data: user-defined data in the form of a JSON string; this data is
195224
included in file analysis reports
196225
:type custom_data: str
226+
:param large_file: Set to True if file size exceeds 1 GB
227+
:type large_file: bool
197228
:return: response
198229
:rtype: requests.Response
199230
"""
@@ -206,14 +237,16 @@ def upload_sample_and_get_results(self, file_path=None, file_source=None, full_r
206237
file_path=file_path,
207238
custom_token=custom_token,
208239
user_data=user_data,
209-
custom_data=custom_data
240+
custom_data=custom_data,
241+
large_file=large_file
210242
)
211243
else:
212244
upload_response = self.upload_sample_from_file(
213245
file_source=file_source,
214246
custom_token=custom_token,
215247
user_data=user_data,
216-
custom_data=custom_data
248+
custom_data=custom_data,
249+
large_file=large_file
217250
)
218251

219252
task_url = upload_response.json().get("task_url")
@@ -252,6 +285,75 @@ def __get_results(self, task_url, full_report=False):
252285

253286
return response
254287

288+
def __upload_large_file(self, file_handle, custom_token, user_data, custom_data):
289+
"""A generic POST request method for all TitaniumScale methods,
290+
optimized for streaming large files using requests-toolbelt.
291+
:param file_handle: files to send
292+
:type file_handle: file or BinaryIO
293+
:param custom_token: set custom token string for filtering processing tasks (X-TiScale-Token)
294+
:type custom_token: str
295+
:param user_data: user-defined data in the form of a JSON string; this data is
296+
NOT included in file analysis reports
297+
:type user_data: str
298+
:param custom_data: user-defined data in the form of a JSON string; this data is
299+
included in file analysis reports
300+
:type custom_data: str
301+
:return: response
302+
:rtype: requests.Response
303+
"""
304+
if not _TOOLBELT_INSTALLED:
305+
raise ImportError("To use large file upload, you need to run "
306+
"'pip install reversinglabs-sdk-py3[large_files]' because additional dependencies "
307+
"are required.")
308+
309+
request_headers = self._headers.copy()
310+
311+
if custom_token is not None:
312+
if not isinstance(custom_token, str):
313+
raise WrongInputError("custom_token parameter must be string.")
314+
315+
request_headers["X-TiScale-Token"] = "Token {custom_token}".format(custom_token=custom_token)
316+
317+
fields = {}
318+
319+
if user_data is not None:
320+
try:
321+
json.loads(user_data)
322+
fields["user_data"] = user_data
323+
324+
except (TypeError, json.decoder.JSONDecodeError):
325+
raise WrongInputError("user_data parameter must be a valid JSON string.")
326+
327+
if custom_data is not None:
328+
try:
329+
json.loads(custom_data)
330+
fields["custom_data"] = custom_data
331+
332+
except (TypeError, json.decoder.JSONDecodeError):
333+
raise WrongInputError("custom_data parameter must be a valid JSON string.")
334+
335+
filename = "file"
336+
337+
fields["file"] = (filename, file_handle, "application/octet-stream")
338+
339+
multipart_encoder = MultipartEncoder(fields=fields)
340+
341+
url = self._url.format(endpoint=self.__UPLOAD_ENDPOINT)
342+
343+
request_headers["User-Agent"] = (f"{self._user_agent}; {self.__class__.__name__} "
344+
f"{inspect.currentframe().f_back.f_code.co_name}")
345+
request_headers["Content-Type"] = multipart_encoder.content_type
346+
347+
response = requests.post(
348+
url=url,
349+
data=multipart_encoder,
350+
verify=self._verify,
351+
proxies=self._proxies,
352+
headers=request_headers
353+
)
354+
355+
return response
356+
255357
def __upload_files(self, file_handle, custom_token, user_data, custom_data):
256358
"""A generic POST request method for all TitaniumScale methods.
257359
:param file_handle: files to send

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
requires = ["requests>=2.32.4"]
66

7+
extras_require = {
8+
"test": ["pytest"],
9+
"large_files": ["requests-toolbelt"]
10+
}
11+
712
packages = ["ReversingLabs",
813
"ReversingLabs.SDK"]
914

@@ -22,7 +27,7 @@
2227
packages=packages,
2328
python_requires=">=3.9",
2429
install_requires=requires,
25-
extras_require={"test": ["pytest"]},
30+
extras_require=extras_require,
2631
license="MIT",
2732
zip_safe=False,
2833
classifiers=[

0 commit comments

Comments
 (0)