Skip to content

Commit 162b73d

Browse files
authored
Merge pull request #5 from ekampf/feature/transport-error-retry
Client support for retrying requests
2 parents b30c393 + 77af22a commit 162b73d

File tree

6 files changed

+98
-16
lines changed

6 files changed

+98
-16
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ python:
77
- 3.5
88
cache: pip
99
install:
10-
- pip install pytest pytest-cov coveralls flake8
10+
- pip install -r ./dev_requirements.txt
1111
- pip install -e .
1212
script:
1313
- flake8 gql

dev_requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pytest
2+
pytest-cov
3+
coveralls
4+
flake8
5+
mock

gql/client.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
import logging
2+
13
from graphql import parse, introspection_query, build_ast_schema, build_client_schema
24
from graphql.validation import validate
35

46
from .transport.local_schema import LocalSchemaTransport
57

68

7-
class Client(object):
9+
class RetryError(Exception):
10+
"""Custom exception thrown when retry logic fails"""
11+
def __init__(self, retries_count, last_exception):
12+
message = "Failed %s retries: %s" % (retries_count, last_exception)
13+
super(RetryError, self).__init__(message)
14+
self.last_exception = last_exception
15+
816

17+
class Client(object):
918
def __init__(self, schema=None, introspection=None, type_def=None, transport=None,
10-
fetch_schema_from_transport=False):
19+
fetch_schema_from_transport=False, retries=0):
1120
assert not(type_def and introspection), 'Cant provide introspection type definition at the same time'
1221
if transport and fetch_schema_from_transport:
1322
assert not schema, 'Cant fetch the schema from transport if is already provided'
@@ -25,6 +34,7 @@ def __init__(self, schema=None, introspection=None, type_def=None, transport=Non
2534
self.schema = schema
2635
self.introspection = introspection
2736
self.transport = transport
37+
self.retries = retries
2838

2939
def validate(self, document):
3040
if not self.schema:
@@ -36,7 +46,28 @@ def validate(self, document):
3646
def execute(self, document, *args, **kwargs):
3747
if self.schema:
3848
self.validate(document)
39-
result = self.transport.execute(document, *args, **kwargs)
49+
50+
result = self._get_result(document, *args, **kwargs)
4051
if result.errors:
4152
raise result.errors[0]
53+
4254
return result.data
55+
56+
def _get_result(self, document, *args, **kwargs):
57+
if not self.retries:
58+
return self.transport.execute(document, *args, **kwargs)
59+
60+
last_exception = None
61+
retries_count = 0
62+
while retries_count < self.retries:
63+
try:
64+
result = self.transport.execute(document, *args, **kwargs)
65+
return result
66+
except Exception as e:
67+
last_exception = e
68+
logging.warn("Request failed with exception %s. Retrying for the %s time...",
69+
e, retries_count + 1, exc_info=True)
70+
finally:
71+
retries_count += 1
72+
73+
raise RetryError(retries_count, last_exception)

gql/transport/requests.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,33 @@
88

99

1010
class RequestsHTTPTransport(HTTPTransport):
11-
def __init__(self, url, auth=None, **kwargs):
11+
def __init__(self, url, auth=None, use_json=False, timeout=None, **kwargs):
12+
"""
13+
:param url: The GraphQL URL
14+
:param auth: Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth
15+
:param use_json: Send request body as JSON instead of form-urlencoded
16+
:param timeout: Specifies a default timeout for requests (Default: None)
17+
"""
1218
super(RequestsHTTPTransport, self).__init__(url, **kwargs)
1319
self.auth = auth
20+
self.default_timeout = timeout
21+
self.use_json = use_json
1422

15-
def execute(self, document, variable_values=None):
23+
def execute(self, document, variable_values=None, timeout=None):
1624
query_str = print_ast(document)
17-
request = requests.post(
18-
self.url,
19-
data={
20-
'query': query_str,
21-
'variables': variable_values
22-
},
23-
headers=self.headers,
24-
auth=self.auth
25-
)
25+
payload = {
26+
'query': query_str,
27+
'variables': variable_values or {}
28+
}
29+
30+
data_key = 'json' if self.use_json else 'data'
31+
post_args = {
32+
'headers': self.headers,
33+
'auth': self.auth,
34+
'timeout': timeout or self.default_timeout,
35+
data_key: payload
36+
}
37+
request = requests.post(self.url, **post_args)
2638
request.raise_for_status()
2739

2840
result = request.json()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@
2929
'graphql-core>=0.5.0',
3030
'promise>=0.4.0'
3131
],
32-
tests_require=['pytest>=2.7.2'],
32+
tests_require=['pytest>=2.7.2', 'mock'],
3333
)

tests/test_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
import mock
3+
4+
from gql import Client, gql
5+
from gql.transport.requests import RequestsHTTPTransport
6+
7+
8+
@mock.patch('gql.transport.requests.RequestsHTTPTransport.execute')
9+
def test_retries(execute_mock):
10+
expected_retries = 3
11+
execute_mock.side_effect =Exception("fail")
12+
13+
client = Client(
14+
retries=expected_retries,
15+
transport=RequestsHTTPTransport(url='http://swapi.graphene-python.org/graphql')
16+
)
17+
18+
query = gql('''
19+
{
20+
myFavoriteFilm: film(id:"RmlsbToz") {
21+
id
22+
title
23+
episodeId
24+
}
25+
}
26+
''')
27+
28+
with pytest.raises(Exception):
29+
client.execute(query)
30+
31+
assert execute_mock.call_count == expected_retries
32+
33+
34+

0 commit comments

Comments
 (0)