-
Notifications
You must be signed in to change notification settings - Fork 0
Retry http errors #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7eb52b3
4759a1a
e14c5ef
bf5873c
5227c68
4fa079d
97b0717
433ee6f
e21c99e
08bb6b3
78390c4
6512040
1f2fbd2
d980c8d
dcabdcc
5b3524c
c4ff0cf
9d903a6
e0d8345
9d41dbb
8ed1a14
ec5f6cc
59be1cb
90ee0b8
2ed5fb2
0f91140
6ebfe51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| from deepomatic.api.version import __version__ | ||
|
|
||
| __all__ = ["__version__"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,28 +22,50 @@ | |
| THE SOFTWARE. | ||
| """ | ||
|
|
||
| import os | ||
| import functools | ||
| import json | ||
| import requests | ||
| import sys | ||
| import os | ||
| import platform | ||
| from requests.structures import CaseInsensitiveDict | ||
| from six import string_types | ||
| import sys | ||
|
|
||
| from deepomatic.api.exceptions import DeepomaticException, BadStatus | ||
| import requests | ||
| from deepomatic.api.exceptions import BadStatus, DeepomaticException | ||
| from deepomatic.api.http_retry import HTTPRetry | ||
| from deepomatic.api.version import __title__, __version__ | ||
| from requests.structures import CaseInsensitiveDict | ||
| from six import string_types | ||
|
|
||
| API_HOST = 'https://api.deepomatic.com' | ||
| API_VERSION = 0.7 | ||
|
|
||
| ############################################################################### | ||
|
|
||
|
|
||
| class RequestsTimeout(object): | ||
| FAST = (3.05, 10.) | ||
| MEDIUM = (3.05, 60.) | ||
| SLOW = (3.05, 600.) | ||
|
|
||
|
|
||
| class HTTPHelper(object): | ||
| def __init__(self, app_id=None, api_key=None, verify_ssl=None, host=None, version=API_VERSION, check_query_parameters=True, user_agent_prefix='', user_agent_suffix='', pool_maxsize=20): | ||
| def __init__(self, app_id=None, api_key=None, verify_ssl=None, | ||
| host=None, version=API_VERSION, check_query_parameters=True, | ||
| user_agent_prefix='', user_agent_suffix='', pool_maxsize=20, | ||
| requests_timeout=RequestsTimeout.FAST, **kwargs): | ||
maingoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Init the HTTP helper with API key and secret | ||
| Init the HTTP helper with API key and secret. | ||
| Check out the `client.Client` documentation for more details about the parameters. | ||
| """ | ||
|
|
||
| # `http_retry` is retrieved from `kwargs` because a default parameter `http_retry=HTTPRetry()` is dangerous | ||
| # If the rest of the code mutates `self.http_retry`, it would change the default parameter for all other `Client` instances | ||
| self.http_retry = kwargs.pop('http_retry', HTTPRetry()) | ||
|
|
||
| if len(kwargs) > 0: | ||
| raise TypeError("Too many parameters. HTTPRetry does not handle kwargs: {}".format(kwargs)) | ||
|
|
||
| self.requests_timeout = requests_timeout | ||
|
|
||
| if host is None: | ||
| host = os.getenv('DEEPOMATIC_API_URL', API_HOST) | ||
| if verify_ssl is None: | ||
|
|
@@ -157,7 +179,25 @@ def recursive_json_dump(prefix, obj, data_dict, omit_dot=False): | |
|
|
||
| return new_dict | ||
|
|
||
| def make_request(self, func, resource, params=None, data=None, content_type='application/json', files=None, stream=False, *args, **kwargs): | ||
| def send_request(self, requests_callable, *args, **kwargs): | ||
| # requests_callable must be a method from the requests module | ||
|
|
||
| # this is the timeout of requests module | ||
| requests_timeout = kwargs.pop('timeout', self.requests_timeout) | ||
| http_retry = kwargs.pop('http_retry', self.http_retry) | ||
|
|
||
| functor = functools.partial(requests_callable, *args, | ||
| verify=self.verify, | ||
| timeout=requests_timeout, **kwargs) | ||
|
|
||
| if http_retry is not None: | ||
| return http_retry.retry(functor) | ||
|
|
||
| return functor() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I would move that in an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is a matter of taste, I prefer when there is always a non indented return
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed; I was struggling with that and the early return: either both return in if/else, or only one return maybe? |
||
|
|
||
| def make_request(self, func, resource, params=None, data=None, | ||
| content_type='application/json', files=None, | ||
| stream=False, *args, **kwargs): | ||
|
|
||
| if content_type is not None: | ||
| if content_type.strip() == 'application/json': | ||
|
|
@@ -204,7 +244,11 @@ def make_request(self, func, resource, params=None, data=None, content_type='app | |
|
|
||
| if not resource.startswith('http'): | ||
| resource = self.resource_prefix + resource | ||
| response = func(resource, params=params, data=data, files=files, headers=headers, verify=self.verify, stream=stream, *args, **kwargs) | ||
|
|
||
| response = self.send_request(func, resource, *args, | ||
| params=params, data=data, | ||
| files=files, headers=headers, | ||
| stream=stream, **kwargs) | ||
|
|
||
| # Close opened files | ||
| for file in opened_files: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import functools | ||
|
|
||
| from deepomatic.api import utils | ||
| from deepomatic.api.exceptions import HTTPRetryError | ||
| from requests.exceptions import (ProxyError, RequestException, | ||
| TooManyRedirects, URLRequired) | ||
| from tenacity import (retry_if_exception, retry_if_result, stop_after_delay, | ||
| wait_chain, wait_fixed, wait_random_exponential) | ||
|
|
||
|
|
||
| class retry_if_exception_type(retry_if_exception): | ||
| # Taken from https://github.com/jd/tenacity/blob/2775f13b34b3ec67a774061a77fcd4e1e9b4157c/tenacity/retry.py#L72 | ||
| # Extented to support blacklist types | ||
| def __predicate(self, e): | ||
| return (isinstance(e, self.exception_types) and | ||
| not isinstance(e, self.exception_types_blacklist)) | ||
|
|
||
| def __init__(self, exception_types=Exception, | ||
| exception_types_blacklist=()): | ||
| self.exception_types = exception_types | ||
| self.exception_types_blacklist = exception_types_blacklist | ||
| super(retry_if_exception_type, self).__init__(self.__predicate) | ||
|
|
||
|
|
||
| class HTTPRetry(object): | ||
|
|
||
| """ | ||
| :param retry_if (optional): predicate to retry on requests errors. | ||
| More details directly in tenacity source code: | ||
|
|
||
| - https://github.com/jd/tenacity/blob/5.1.1/tenacity/__init__.py#L179 | ||
| - https://github.com/jd/tenacity/blob/5.1.1/tenacity/retry.py | ||
| If not provided, the default behavior is: | ||
| - Retry on status code from Default.RETRY_STATUS_CODES | ||
| - Retry on exceptions from Default.RETRY_EXCEPTION_TYPES excluding those from Default.RETRY_EXCEPTION_TYPES_BLACKLIST | ||
| :type retry_if: tenacity.retry_base | ||
| :param wait (optional): how to wait between retry | ||
| More details directly in tenacity source code https://github.com/jd/tenacity/blob/5.1.1/tenacity/wait.py | ||
|
|
||
| if not provided, the default behavior is: | ||
| ``` | ||
| random_wait = wait_random_exponential(multiplier=Default.RETRY_EXP_MULTIPLIER, | ||
| max=Default.RETRY_EXP_MAX) | ||
| wait_chain(wait_fixed(0.05), | ||
| wait_fixed(0.1), | ||
| wait_fixed(0.1) + random_wait) | ||
| ``` | ||
| :type wait: tenacity.wait_base | ||
| :param stop (optional). Tell when to stop retrying. By default it stops retrying after a delay of 60 seconds. A last retry can be done just before this delay is reached, thus the total amount of elapsed time might be a bit higher. More details in tenacity source code https://github.com/jd/tenacity/blob/5.1.1/tenacity/stop.py | ||
| Raises tenacity.RetryError when timeout is reached. | ||
| :type timeout: tenacity.stop_base | ||
| """ | ||
|
|
||
| class Default(object): | ||
| RETRY_EXP_MAX = 10. | ||
| RETRY_EXP_MULTIPLIER = 0.5 | ||
| RETRY_STATUS_CODES = [500, 502, 503, 504] | ||
| RETRY_EXCEPTION_TYPES = (RequestException, ) | ||
| RETRY_EXCEPTION_TYPES_BLACKLIST = (ValueError, ProxyError, TooManyRedirects, URLRequired) | ||
|
|
||
| def __init__(self, retry_if=None, wait=None, stop=None): | ||
| self.retry_status_code = {} | ||
| self.retry_if = retry_if | ||
| self.wait = wait | ||
| self.stop = stop | ||
|
|
||
| if self.stop is None: | ||
| self.stop = stop_after_delay(60) | ||
|
|
||
| if self.retry_if is None: | ||
| self.retry_status_code = set(HTTPRetry.Default.RETRY_STATUS_CODES) | ||
| self.retry_if = (retry_if_result(self.retry_if_status_code) | | ||
| retry_if_exception_type(HTTPRetry.Default.RETRY_EXCEPTION_TYPES, | ||
| HTTPRetry.Default.RETRY_EXCEPTION_TYPES_BLACKLIST)) | ||
|
|
||
| if self.wait is None: | ||
| random_wait = wait_random_exponential(multiplier=HTTPRetry.Default.RETRY_EXP_MULTIPLIER, | ||
| max=HTTPRetry.Default.RETRY_EXP_MAX) | ||
| self.wait = wait_chain(wait_fixed(0.05), | ||
| wait_fixed(0.1), | ||
| wait_fixed(0.1) + random_wait) | ||
|
|
||
| def retry(self, functor): | ||
| return utils.retry(functor, self.retry_if, self.wait, self.stop, retry_error_cls=HTTPRetryError) | ||
|
|
||
| def retry_if_status_code(self, response): | ||
| return response.status_code in self.retry_status_code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from deepomatic.api.exceptions import DeepomaticException | ||
| from deepomatic.api.resources.task import Task | ||
| from deepomatic.api.inputs import format_inputs | ||
|
|
||
|
|
||
| class InferenceResource(object): | ||
| def inference(self, return_task=False, wait_task=True, **kwargs): | ||
| assert(self._pk is not None) | ||
|
|
||
| inputs = kwargs.pop('inputs', None) | ||
| if inputs is None: | ||
| raise DeepomaticException("Missing keyword argument: inputs") | ||
| content_type, data, files = format_inputs(inputs, kwargs) | ||
| result = self._helper.post(self._uri(pk=self._pk, suffix='/inference'), content_type=content_type, data=data, files=files) | ||
| task_id = result['task_id'] | ||
| task = Task(self._helper, pk=task_id) | ||
| if wait_task: | ||
| task.wait() | ||
|
|
||
| if return_task: | ||
| return task | ||
| else: | ||
| return task['data'] |
Uh oh!
There was an error while loading. Please reload this page.