Skip to content

Commit e293073

Browse files
authored
Refactor RequestsHttpTransport Class (#63)
Also improve test coverage and doc strings.
1 parent 523bb72 commit e293073

File tree

10 files changed

+264
-52
lines changed

10 files changed

+264
-52
lines changed

README.md

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ The example below shows how you can execute queries against a local schema.
3434
```python
3535
from gql import gql, Client
3636

37-
client = Client(schema=schema)
37+
from .someSchema import SampleSchema
38+
39+
40+
client = Client(schema=SampleSchema)
3841
query = gql('''
39-
{
40-
hello
41-
}
42+
{
43+
hello
44+
}
4245
''')
4346

4447
client.execute(query)
@@ -50,12 +53,69 @@ If you want to add additional headers when executing the query, you can specify
5053
from gql import Client
5154
from gql.transport.requests import RequestsHTTPTransport
5255

56+
from .someSchema import SampleSchema
57+
5358
client = Client(transport=RequestsHTTPTransport(
54-
url='/graphql', headers={'Authorization': 'token'}), schema=schema)
59+
url='/graphql', headers={'Authorization': 'token'}), schema=SampleSchema)
60+
```
61+
62+
To execute against a graphQL API. (We get the schema by using introspection).
63+
64+
```python
65+
from gql import gql, Client
66+
from gql.transport.requests import RequestsHTTPTransport
67+
68+
sample_transport=RequestsHTTPTransport(
69+
url='https://countries.trevorblades.com/',
70+
use_json=True,
71+
headers={
72+
"Content-type": "application/json",
73+
},
74+
verify=False
75+
)
76+
77+
client = Client(
78+
retries=3,
79+
transport=sample_transport,
80+
fetch_schema_from_transport=True,
81+
)
82+
83+
query = gql('''
84+
query getContinents {
85+
continents {
86+
code
87+
name
88+
}
89+
}
90+
''')
91+
92+
client.execute(query)
5593
```
5694

95+
If you have a local schema stored as a `schema.graphql` file, you can do:
96+
97+
```python
98+
from graphql import build_ast_schema, parse
99+
from gql import gql, Client
100+
101+
with open('path/to/schema.graphql') as source:
102+
document = parse(source.read())
103+
104+
schema = build_ast_schema(document)
105+
106+
client = Client(schema=schema)
107+
query = gql('''
108+
{
109+
hello
110+
}
111+
''')
112+
113+
client.execute(query)
114+
```
115+
116+
57117
## Contributing
58-
See [CONTRIBUTING.md](contributing.md)
118+
See [CONTRIBUTING.md](CONTRIBUTING.md)
59119

60120
## License
61121

gql/transport/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import abc
2+
from typing import Union
3+
4+
import six
5+
from graphql.execution import ExecutionResult
6+
from graphql.language.ast import Node, Document
7+
from promise import Promise
8+
9+
10+
@six.add_metaclass(abc.ABCMeta)
11+
class Transport:
12+
@abc.abstractmethod
13+
def execute(self, document):
14+
# type: (Union[Node, Document]) -> Union[ExecutionResult, Promise[ExecutionResult]]
15+
"""Execute the provided document AST for either a remote or local GraphQL Schema.
16+
17+
:param document: GraphQL query as AST Node or Document object.
18+
:return: Either ExecutionResult or a Promise that resolves to ExecutionResult object.
19+
"""
20+
raise NotImplementedError("Any Transport subclass must implement execute method")

gql/transport/http.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

gql/transport/local_schema.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
1-
from graphql.execution import execute
1+
from typing import Union, Any
22

3+
from graphql import GraphQLSchema
4+
from graphql.execution import execute, ExecutionResult
5+
from graphql.language.ast import Document
6+
from promise import Promise
37

4-
class LocalSchemaTransport(object):
8+
from gql.transport import Transport
59

6-
def __init__(self, schema):
10+
11+
class LocalSchemaTransport(Transport):
12+
"""A transport for executing GraphQL queries against a local schema."""
13+
def __init__(
14+
self, # type: LocalSchemaTransport
15+
schema # type: GraphQLSchema
16+
):
17+
"""Initialize the transport with the given local schema.
18+
19+
:param schema: Local schema as GraphQLSchema object
20+
"""
721
self.schema = schema
822

923
def execute(self, document, *args, **kwargs):
24+
# type: (Document, Any, Any) -> Union[ExecutionResult, Promise[ExecutionResult]]
25+
"""Execute the given document against the configured local schema.
26+
27+
:param document: GraphQL query as AST Node object.
28+
:param args: Positional options for execute method from graphql-core library.
29+
:param kwargs: Keyword options passed to execute method from graphql-core library.
30+
:return: Either ExecutionResult or a Promise that resolves to ExecutionResult object.
31+
"""
1032
return execute(
1133
self.schema,
1234
document,

gql/transport/requests.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,70 @@
11
from __future__ import absolute_import
22

3+
from typing import Any, Dict, Union
4+
35
import requests
46
from graphql.execution import ExecutionResult
7+
from graphql.language.ast import Node
58
from graphql.language.printer import print_ast
9+
from requests.auth import AuthBase
10+
from requests.cookies import RequestsCookieJar
611

7-
from .http import HTTPTransport
12+
from gql.transport import Transport
813

914

10-
class RequestsHTTPTransport(HTTPTransport):
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)
15+
class RequestsHTTPTransport(Transport):
16+
"""Transport to execute GraphQL queries on remote servers.
17+
18+
The transport uses the requests library to send HTTP POST requests.
19+
"""
20+
def __init__(
21+
self, # type: RequestsHTTPTransport
22+
url, # type: str
23+
headers=None, # type: Dict[str, Any]
24+
cookies=None, # type: Union[Dict[str, Any], RequestsCookieJar]
25+
auth=None, # type: AuthBase
26+
use_json=False, # type: bool
27+
timeout=None, # type: int
28+
verify=True, # type: bool
29+
**kwargs # type: Any
30+
):
31+
"""Initialize the transport with the given request parameters.
32+
33+
:param url: The GraphQL server URL.
34+
:param headers: Dictionary of HTTP Headers to send with the :class:`Request` (Default: None).
35+
:param cookies: Dict or CookieJar object to send with the :class:`Request` (Default: None).
36+
:param auth: Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth (Default: None).
37+
:param use_json: Send request body as JSON instead of form-urlencoded (Default: False).
38+
:param timeout: Specifies a default timeout for requests (Default: None).
39+
:param verify: Either a boolean, in which case it controls whether we verify
40+
the server's TLS certificate, or a string, in which case it must be a path
41+
to a CA bundle to use. (Default: True).
42+
:param kwargs: Optional arguments that ``request`` takes. These can be seen at the :requests_: source code
43+
or the official :docs_:
44+
45+
.. _requests: https://github.com/psf/requests/blob/master/requests/api.py
46+
.. _docs: https://requests.readthedocs.io/en/master/
1747
"""
18-
super(RequestsHTTPTransport, self).__init__(url, **kwargs)
48+
self.url = url
49+
self.headers = headers
50+
self.cookies = cookies
1951
self.auth = auth
20-
self.default_timeout = timeout
2152
self.use_json = use_json
53+
self.default_timeout = timeout
54+
self.verify = verify
55+
self.kwargs = kwargs
2256

2357
def execute(self, document, variable_values=None, timeout=None):
58+
# type: (Node, dict, int) -> ExecutionResult
59+
"""Execute the provided document AST against the configured remote server.
60+
This uses the requests library to perform a HTTP POST request to the remote server.
61+
62+
:param document: GraphQL query as AST Node object.
63+
:param variable_values: Dictionary of input parameters (Default: None).
64+
:param timeout: Specifies a default timeout for requests (Default: None).
65+
:return: The result of execution. `data` is the result of executing the query, `errors` is null if no errors
66+
occurred, and is a non-empty array if an error occurred.
67+
"""
2468
query_str = print_ast(document)
2569
payload = {
2670
'query': query_str,
@@ -33,9 +77,13 @@ def execute(self, document, variable_values=None, timeout=None):
3377
'auth': self.auth,
3478
'cookies': self.cookies,
3579
'timeout': timeout or self.default_timeout,
80+
'verify': self.verify,
3681
data_key: payload
3782
}
3883

84+
# Pass kwargs to requests post method
85+
post_args.update(self.kwargs)
86+
3987
response = requests.post(self.url, **post_args)
4088
try:
4189
result = response.json()

tests/fixtures/graphql/sample.graphql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type User {
2+
id: ID!
3+
username: String
4+
firstName: String
5+
lastName: String
6+
fullName: String
7+
}
8+
9+
type Query {
10+
user(id: ID!): User
11+
}
12+
13+
schema {
14+
query: Query
15+
}

tests/starwars/test_dsl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ def ds():
1414

1515

1616
def test_invalid_field_on_type_query(ds):
17-
with pytest.raises(KeyError) as excInfo:
17+
with pytest.raises(KeyError) as exc_info:
1818
ds.Query.extras.select(
1919
ds.Character.name
2020
)
21-
assert "Field extras does not exist in type Query." in str(excInfo.value)
21+
assert "Field extras does not exist in type Query." in str(exc_info.value)
2222

2323

2424
def test_incompatible_query_field(ds):
25-
with pytest.raises(Exception) as excInfo:
25+
with pytest.raises(Exception) as exc_info:
2626
ds.query('hero')
27-
assert "Received incompatible query field" in str(excInfo.value)
27+
assert "Received incompatible query field" in str(exc_info.value)
2828

2929

3030
def test_hero_name_query(ds):

tests/starwars/test_query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,12 @@ def test_check_type_of_luke(client):
343343

344344
def test_parse_error(client):
345345
result = None
346-
with pytest.raises(Exception) as excinfo:
346+
with pytest.raises(Exception) as exc_info:
347347
query = gql('''
348348
qeury
349349
''')
350350
result = client.execute(query)
351-
error = excinfo.value
351+
error = exc_info.value
352352
formatted_error = format_error(error)
353353
assert formatted_error['locations'] == [{'column': 13, 'line': 2}]
354354
assert 'Syntax Error GraphQL request (2:13) Unexpected Name "qeury"' in formatted_error['message']

tests/starwars/test_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def validation_errors(client, query):
7777

7878

7979
def test_incompatible_request_gql(client):
80-
with pytest.raises(Exception) as excInfo:
80+
with pytest.raises(Exception) as exc_info:
8181
gql(123)
82-
assert "Received incompatible request" in str(excInfo.value)
82+
assert "Received incompatible request" in str(exc_info.value)
8383

8484

8585
def test_nested_query_with_fragment(client):

0 commit comments

Comments
 (0)