Skip to content

Commit

Permalink
Merge pull request #9 from intezer/feature/wait-interval
Browse files Browse the repository at this point in the history
Feature/wait interval
  • Loading branch information
davidt99 authored May 31, 2020
2 parents ed134cc + ce25f59 commit a234b52
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 55 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.2
------
- Add option to specify wait interval
- Errors are more informative now

1.1.1
------
- Add support for python 3.8
Expand Down
12 changes: 4 additions & 8 deletions examples/analyze_by_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
from intezer_sdk.analysis import Analysis


def send_file_with_wait(file_path, dynamic_unpacking=None, static_unpacking=None): # type: (str, bool, bool) -> None
def send_file_with_wait(file_path):
api.set_global_api('<api_key>')
analysis = Analysis(file_path=file_path,
dynamic_unpacking=dynamic_unpacking,
static_unpacking=static_unpacking)
analysis = Analysis(file_path=file_path)
analysis.send(wait=True)
pprint(analysis.result())


def send_file_without_wait(file_path, dynamic_unpacking, static_unpacking): # type: (str, bool, bool) -> None
def send_file_without_wait(file_path):
api.set_global_api('<api_key>')
analysis = Analysis(file_path=file_path,
dynamic_unpacking=dynamic_unpacking,
static_unpacking=static_unpacking)
analysis = Analysis(file_path=file_path)
analysis.send()
analysis.wait_for_completion()
pprint(analysis.result())
Expand Down
2 changes: 1 addition & 1 deletion intezer_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.1'
__version__ = '1.2.0'
22 changes: 17 additions & 5 deletions intezer_sdk/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(self,
self._report = None
self._api = api or get_global_api()

def send(self, wait: bool = False) -> None:
def send(self, wait: typing.Union[bool, int] = False) -> None:
if self.analysis_id:
raise errors.AnalysisHasAlreadyBeenSent()

Expand All @@ -62,14 +62,26 @@ def send(self, wait: bool = False) -> None:
self.status = consts.AnalysisStatusCode.CREATED

if wait:
self.wait_for_completion()

def wait_for_completion(self):
if isinstance(wait, int):
self.wait_for_completion(wait, sleep_before_first_check=True)
else:
self.wait_for_completion(sleep_before_first_check=True)

def wait_for_completion(self, interval: int = None, sleep_before_first_check=False):
"""
Blocks until the analysis is completed
:param interval: The interval to wait between checks
:param sleep_before_first_check: Whether to sleep before the first status check
"""
if not interval:
interval = consts.CHECK_STATUS_INTERVAL
if self._is_analysis_running():
if sleep_before_first_check:
time.sleep(interval)
status_code = self.check_status()

while status_code != consts.AnalysisStatusCode.FINISH:
time.sleep(consts.CHECK_STATUS_INTERVAL)
time.sleep(interval)
status_code = self.check_status()

def check_status(self):
Expand Down
61 changes: 50 additions & 11 deletions intezer_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,45 @@
_global_api = None


def raise_for_status(response: requests.Response,
statuses_to_ignore: typing.List[HTTPStatus] = None,
allowed_statuses: typing.List[HTTPStatus] = None):
"""Raises stored :class:`HTTPError`, if one occurred."""

http_error_msg = ''
if isinstance(response.reason, bytes):
reason = response.reason.decode('utf-8', 'ignore')
else:
reason = response.reason

if statuses_to_ignore and response.status_code in statuses_to_ignore:
return
elif allowed_statuses and response.status_code not in allowed_statuses:
http_error_msg = '%s Custom Error: %s for url: %s' % (response.status_code, reason, response.url)
elif 400 <= response.status_code < 500:
if response.status_code != 400:
http_error_msg = '%s Client Error: %s for url: %s' % (response.status_code, reason, response.url)
else:
# noinspection PyBroadException
try:
error = response.json()
http_error_msg = '\n'.join(['{}:{}.'.format(key, value) for key, value in error['message'].items()])
except Exception:
http_error_msg = '%s Client Error: %s for url: %s' % (response.status_code, reason, response.url)
elif 500 <= response.status_code < 600:
http_error_msg = '%s Server Error: %s for url: %s' % (response.status_code, reason, response.url)

if http_error_msg:
# noinspection PyBroadException
try:
data = response.json()
http_error_msg = '%s, server returns %s, details: %s' % (http_error_msg, data['error'], data.get('details'))
except Exception:
pass

raise requests.HTTPError(http_error_msg, response=response)


class IntezerApi:
def __init__(self, api_version: str = None, api_key: str = None, base_url: str = None):
self.full_url = base_url + api_version
Expand Down Expand Up @@ -108,14 +147,14 @@ def get_latest_analysis(self, file_hash: str) -> Optional[dict]:
if response.status_code == HTTPStatus.NOT_FOUND:
return None

response.raise_for_status()
raise_for_status(response)

return response.json()['result']

def get_analysis_response(self, analyses_id) -> Response:
response = self._request_with_refresh_expired_access_token(path='/analyses/{}'.format(analyses_id),
method='GET')
response.raise_for_status()
raise_for_status(response)

return response

Expand Down Expand Up @@ -150,17 +189,17 @@ def index_by_file(self, file_path: str, index_as: IndexType, family_name: str =
def get_index_response(self, index_id: str) -> Response:
response = self._request_with_refresh_expired_access_token(path='/files/index/{}'.format(index_id),
method='GET')
response.raise_for_status()
raise_for_status(response)

return response

def _set_access_token(self, api_key: str):
response = requests.post(self.full_url + '/get-access-token', json={'api_key': api_key})

if response.status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.BAD_REQUEST):
raise errors.InvalidApiKey()
raise errors.InvalidApiKey(response)
if response.status_code != HTTPStatus.OK:
response.raise_for_status()
raise_for_status(response)

self._access_token = response.json()['result']

Expand Down Expand Up @@ -189,20 +228,20 @@ def _param_initialize(disable_dynamic_unpacking: bool = None,
@staticmethod
def _assert_analysis_response_status_code(response: Response):
if response.status_code == HTTPStatus.NOT_FOUND:
raise errors.HashDoesNotExistError()
raise errors.HashDoesNotExistError(response)
elif response.status_code == HTTPStatus.CONFLICT:
raise errors.AnalysisIsAlreadyRunning()
raise errors.AnalysisIsAlreadyRunning(response)
elif response.status_code == HTTPStatus.FORBIDDEN:
raise errors.InsufficientQuota()
raise errors.InsufficientQuota(response)
elif response.status_code != HTTPStatus.CREATED:
raise errors.IntezerError('Error in response status code:{}'.format(response.status_code))
raise errors.ServerError('Error in response status code:{}'.format(response.status_code), response)

@staticmethod
def _assert_index_response_status_code(response: Response):
if response.status_code == HTTPStatus.NOT_FOUND:
raise errors.HashDoesNotExistError()
raise errors.HashDoesNotExistError(response)
elif response.status_code != HTTPStatus.CREATED:
raise errors.IntezerError('Error in response status code:{}'.format(response.status_code))
raise errors.ServerError('Error in response status code:{}'.format(response.status_code), response)

@staticmethod
def _get_analysis_id_from_response(response: Response):
Expand Down
60 changes: 40 additions & 20 deletions intezer_sdk/errors.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,77 @@
import requests


def _parse_erroneous_response(response: requests.Response):
try:
data = response.json()
return data.get('error', '')
except ValueError:
return ''


class IntezerError(Exception):
pass


class ServerError(IntezerError):
def __init__(self, message: str, response: requests.Response):
self.response = response
detailed_error = _parse_erroneous_response(response)
if detailed_error:
message = '{}. Error:{}'.format(message, detailed_error)
super().__init__(message)


class AnalysisHasAlreadyBeenSent(IntezerError):
def __init__(self):
super(AnalysisHasAlreadyBeenSent, self).__init__('Analysis already been sent')


class IndexHasAlreadyBeenSent(IntezerError):
def __init__(self):
super(IndexHasAlreadyBeenSent, self).__init__('Index already been sent')
super().__init__('Index already been sent')


class AnalysisDoesNotExistError(IntezerError):
def __init__(self):
super(AnalysisDoesNotExistError, self).__init__('Analysis was not found')
super().__init__('Analysis was not found')


class HashDoesNotExistError(IntezerError):
def __init__(self):
super(HashDoesNotExistError, self).__init__('Hash was not found')
class HashDoesNotExistError(ServerError):
def __init__(self, response: requests.Response):
super().__init__('Hash was not found', response)


class ReportDoesNotExistError(IntezerError):
def __init__(self):
super(ReportDoesNotExistError, self).__init__('Report was not found')
super().__init__('Report was not found')


class AnalysisIsAlreadyRunning(IntezerError):
def __init__(self):
super(AnalysisIsAlreadyRunning, self).__init__('Analysis already running')
class AnalysisIsAlreadyRunning(ServerError):
def __init__(self, response: requests.Response):
super().__init__('Analysis already running', response)


class InsufficientQuota(IntezerError):
def __init__(self):
super(InsufficientQuota, self).__init__('Insufficient quota')
class InsufficientQuota(ServerError):
def __init__(self, response: requests.Response):
super().__init__('Insufficient quota', response)


class GlobalApiIsNotInitialized(IntezerError):
def __init__(self):
super(GlobalApiIsNotInitialized, self).__init__('Global API is not initialized')
super().__init__('Global API is not initialized')


class AnalysisIsStillRunning(IntezerError):
def __init__(self):
super(AnalysisIsStillRunning, self).__init__('Analysis is still running')
super().__init__('Analysis is still running')


class InvalidApiKey(IntezerError):
def __init__(self):
super(InvalidApiKey, self).__init__('Invalid api key')
class InvalidApiKey(ServerError):
def __init__(self, response: requests.Response):
super().__init__('Invalid api key', response)


class IndexFailed(IntezerError):
def __init__(self):
super(IndexFailed, self).__init__('Index operation failed')
class IndexFailed(ServerError):
def __init__(self, response: requests.Response):
super().__init__('Index operation failed', response)
25 changes: 19 additions & 6 deletions intezer_sdk/index.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
from http import HTTPStatus
from typing import Union

from intezer_sdk import consts
from intezer_sdk import errors
Expand Down Expand Up @@ -28,7 +29,7 @@ def __init__(self,
self._index_as = index_as
self._family_name = family_name

def send(self, wait: bool = False):
def send(self, wait: Union[bool, int] = False):
if self.index_id:
raise errors.IndexHasAlreadyBeenSent()

Expand All @@ -40,14 +41,26 @@ def send(self, wait: bool = False):
self.status = consts.IndexStatusCode.CREATED

if wait:
self.wait_for_completion()
if isinstance(wait, int):
self.wait_for_completion(wait, sleep_before_first_check=True)
else:
self.wait_for_completion(sleep_before_first_check=True)

def wait_for_completion(self):
def wait_for_completion(self, interval: int = None, sleep_before_first_check=False):
"""
Blocks until the index is completed
:param interval: The interval to wait between checks
:param sleep_before_first_check: Whether to sleep before the first status check
"""
if not interval:
interval = consts.CHECK_STATUS_INTERVAL
if self._is_index_operation_running():
if sleep_before_first_check:
time.sleep(interval)
status_code = self.check_status()

while status_code != consts.IndexStatusCode.FINISH:
time.sleep(consts.CHECK_STATUS_INTERVAL)
time.sleep(interval)
status_code = self.check_status()

def check_status(self):
Expand All @@ -57,13 +70,13 @@ def check_status(self):
response = self._api.get_index_response(self.index_id)
if response.status_code == HTTPStatus.OK:
if response.json()['status'] == 'failed':
raise errors.IndexFailed()
raise errors.IndexFailed(response)
else:
self.status = consts.IndexStatusCode.FINISH
elif response.status_code == HTTPStatus.ACCEPTED:
self.status = consts.IndexStatusCode.IN_PROGRESS
else:
raise errors.IntezerError('Error in response status code:{}'.format(response.status_code))
raise errors.ServerError('Error in response status code:{}'.format(response.status_code), response)

return self.status

Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
requests>=2.22.0,<3
responses==0.10.12
pytest==5.3.5
responses==0.10.14
pytest==5.4.2
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def rel(*xs):
install_requires=install_requires,
keywords='intezer',
test_requires=[
'responses == 0.10.12',
'pytest == 5.3.5'
'responses == 0.10.14',
'pytest == 5.4.2'
],
python_requires='!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
classifiers=[
Expand Down
Loading

0 comments on commit a234b52

Please sign in to comment.