Skip to content

Commit 44803d4

Browse files
authored
Refactor DSL code (#169)
Refactoring of the DSL code. The DSLSchema only needs a schema, a Client is no more needed. New dsl_gql function to convert the DSL operations into a Document This allows DSL queries to be executed in a session instead of a client (Solves #138) Added typing annotations Added code documentation and sphinx docs (Solves #82) Supports Subscriptions Fix nested input arguments Allow to set the alias directly in the select method Allow multiple operations in a document Allow to set the operation name
1 parent 5874f54 commit 44803d4

File tree

14 files changed

+908
-221
lines changed

14 files changed

+908
-221
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ The main features of GQL are:
4040
* Supports [sync or async usage](https://gql.readthedocs.io/en/latest/async/index.html), [allowing concurrent requests](https://gql.readthedocs.io/en/latest/advanced/async_advanced_usage.html#async-advanced-usage)
4141
* Supports [File uploads](https://gql.readthedocs.io/en/latest/usage/file_upload.html)
4242
* [gql-cli script](https://gql.readthedocs.io/en/latest/gql-cli/intro.html) to execute GraphQL queries from the command line
43+
* [DSL module](https://gql.readthedocs.io/en/latest/advanced/dsl_module.html) to compose GraphQL queries dynamically
4344

4445
## Installation
4546

docs/advanced/dsl_module.rst

Lines changed: 208 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,225 @@ Compose queries dynamically
22
===========================
33

44
Instead of providing the GraphQL queries as a Python String, it is also possible to create GraphQL queries dynamically.
5-
Using the DSL module, we can create a query using a Domain Specific Language which is created from the schema.
5+
Using the :mod:`DSL module <gql.dsl>`, we can create a query using a Domain Specific Language which is created from the schema.
6+
7+
The following code:
68

79
.. code-block:: python
810
9-
from gql.dsl import DSLSchema
11+
ds = DSLSchema(StarWarsSchema)
12+
13+
query = dsl_gql(
14+
DSLQuery(
15+
ds.Query.hero.select(
16+
ds.Character.id,
17+
ds.Character.name,
18+
ds.Character.friends.select(ds.Character.name),
19+
)
20+
)
21+
)
22+
23+
will generate a query equivalent to:
24+
25+
.. code-block:: python
26+
27+
query = gql("""
28+
query {
29+
hero {
30+
id
31+
name
32+
friends {
33+
name
34+
}
35+
}
36+
}
37+
""")
38+
39+
How to use
40+
----------
41+
42+
First generate the root using the :class:`DSLSchema <gql.dsl.DSLSchema>`::
43+
44+
ds = DSLSchema(client.schema)
45+
46+
Then use auto-generated attributes of the :code:`ds` instance
47+
to get a root type (Query, Mutation or Subscription).
48+
This will generate a :class:`DSLType <gql.dsl.DSLType>` instance::
49+
50+
ds.Query
51+
52+
From this root type, you use auto-generated attributes to get a field.
53+
This will generate a :class:`DSLField <gql.dsl.DSLField>` instance::
54+
55+
ds.Query.hero
56+
57+
hero is a GraphQL object type and needs children fields. By default,
58+
there is no children fields selected. To select the fields that you want
59+
in your query, you use the :meth:`select <gql.dsl.DSLField.select>` method.
60+
61+
To generate the children fields, we use the same method as above to auto-generate the fields
62+
from the :code:`ds` instance
63+
(ie :code:`ds.Character.name` is the field `name` of the type `Character`)::
64+
65+
ds.Query.hero.select(ds.Character.name)
1066

11-
client = Client(schema=StarWarsSchema)
12-
ds = DSLSchema(client)
67+
The select method return the same instance, so it is possible to chain the calls::
1368

14-
query_dsl = ds.Query.hero.select(
69+
ds.Query.hero.select(ds.Character.name).select(ds.Character.id)
70+
71+
Or do it sequencially::
72+
73+
hero_query = ds.Query.hero
74+
75+
hero_query.select(ds.Character.name)
76+
hero_query.select(ds.Character.id)
77+
78+
As you can select children fields of any object type, you can construct your complete query tree::
79+
80+
ds.Query.hero.select(
1581
ds.Character.id,
1682
ds.Character.name,
17-
ds.Character.friends.select(ds.Character.name,),
83+
ds.Character.friends.select(ds.Character.name),
1884
)
1985

20-
will create a query equivalent to:
86+
Once your root query fields are defined, you can put them in an operation using
87+
:class:`DSLQuery <gql.dsl.DSLQuery>`,
88+
:class:`DSLMutation <gql.dsl.DSLMutation>` or
89+
:class:`DSLSubscription <gql.dsl.DSLSubscription>`::
2190

22-
.. code-block:: python
91+
DSLQuery(
92+
ds.Query.hero.select(
93+
ds.Character.id,
94+
ds.Character.name,
95+
ds.Character.friends.select(ds.Character.name),
96+
)
97+
)
98+
99+
100+
Once your operations are defined,
101+
use the :func:`dsl_gql <gql.dsl.dsl_gql>` function to convert your operations into
102+
a document which will be able to get executed in the client or a session::
103+
104+
query = dsl_gql(
105+
DSLQuery(
106+
ds.Query.hero.select(
107+
ds.Character.id,
108+
ds.Character.name,
109+
ds.Character.friends.select(ds.Character.name),
110+
)
111+
)
112+
)
113+
114+
result = client.execute(query)
115+
116+
Arguments
117+
^^^^^^^^^
118+
119+
It is possible to add arguments to any field simply by calling it
120+
with the required arguments::
121+
122+
ds.Query.human(id="1000").select(ds.Human.name)
123+
124+
It can also be done using the :meth:`args <gql.dsl.DSLField.args>` method::
125+
126+
ds.Query.human.args(id="1000").select(ds.Human.name)
23127

24-
hero {
25-
id
26-
name
27-
friends {
28-
name
29-
}
128+
Aliases
129+
^^^^^^^
130+
131+
You can set an alias of a field using the :meth:`alias <gql.dsl.DSLField.alias>` method::
132+
133+
ds.Query.human.args(id=1000).alias("luke").select(ds.Character.name)
134+
135+
It is also possible to set the alias directly using keyword arguments of an operation::
136+
137+
DSLQuery(
138+
luke=ds.Query.human.args(id=1000).select(ds.Character.name)
139+
)
140+
141+
Or using keyword arguments in the :meth:`select <gql.dsl.DSLField.select>` method::
142+
143+
ds.Query.hero.select(
144+
my_name=ds.Character.name
145+
)
146+
147+
Mutations
148+
^^^^^^^^^
149+
150+
For the mutations, you need to start from root fields starting from :code:`ds.Mutation`
151+
then you need to create the GraphQL operation using the class
152+
:class:`DSLMutation <gql.dsl.DSLMutation>`. Example::
153+
154+
query = dsl_gql(
155+
DSLMutation(
156+
ds.Mutation.createReview.args(
157+
episode=6, review={"stars": 5, "commentary": "This is a great movie!"}
158+
).select(ds.Review.stars, ds.Review.commentary)
159+
)
160+
)
161+
162+
Subscriptions
163+
^^^^^^^^^^^^^
164+
165+
For the subscriptions, you need to start from root fields starting from :code:`ds.Subscription`
166+
then you need to create the GraphQL operation using the class
167+
:class:`DSLSubscription <gql.dsl.DSLSubscription>`. Example::
168+
169+
query = dsl_gql(
170+
DSLSubscription(
171+
ds.Subscription.reviewAdded(episode=6).select(ds.Review.stars, ds.Review.commentary)
172+
)
173+
)
174+
175+
Multiple fields in an operation
176+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
177+
178+
It is possible to create an operation with multiple fields::
179+
180+
DSLQuery(
181+
ds.Query.hero.select(ds.Character.name),
182+
hero_of_episode_5=ds.Query.hero(episode=5).select(ds.Character.name),
183+
)
184+
185+
Operation name
186+
^^^^^^^^^^^^^^
187+
188+
You can set the operation name of an operation using a keyword argument
189+
to :func:`dsl_gql <gql.dsl.dsl_gql>`::
190+
191+
query = dsl_gql(
192+
GetHeroName=DSLQuery(ds.Query.hero.select(ds.Character.name))
193+
)
194+
195+
will generate the request::
196+
197+
query GetHeroName {
198+
hero {
199+
name
200+
}
30201
}
31202

32-
.. warning::
203+
Multiple operations in a document
204+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
205+
206+
It is possible to create an Document with multiple operations::
207+
208+
query = dsl_gql(
209+
operation_name_1=DSLQuery( ... ),
210+
operation_name_2=DSLQuery( ... ),
211+
operation_name_3=DSLMutation( ... ),
212+
)
213+
214+
Executable examples
215+
-------------------
216+
217+
Async example
218+
^^^^^^^^^^^^^
219+
220+
.. literalinclude:: ../code_examples/aiohttp_async_dsl.py
221+
222+
Sync example
223+
^^^^^^^^^^^^^
224+
225+
.. literalinclude:: ../code_examples/requests_sync_dsl.py
33226

34-
Please note that the DSL module is still considered experimental in GQL 3 and is subject to changes
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import asyncio
2+
3+
from gql import Client
4+
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
5+
from gql.transport.aiohttp import AIOHTTPTransport
6+
7+
8+
async def main():
9+
10+
transport = AIOHTTPTransport(url="https://countries.trevorblades.com/graphql")
11+
12+
client = Client(transport=transport, fetch_schema_from_transport=True)
13+
14+
# Using `async with` on the client will start a connection on the transport
15+
# and provide a `session` variable to execute queries on this connection.
16+
# Because we requested to fetch the schema from the transport,
17+
# GQL will fetch the schema just after the establishment of the first session
18+
async with client as session:
19+
20+
# Instanciate the root of the DSL Schema as ds
21+
ds = DSLSchema(client.schema)
22+
23+
# Create the query using dynamically generated attributes from ds
24+
query = dsl_gql(
25+
DSLQuery(
26+
ds.Query.continents(filter={"code": {"eq": "EU"}}).select(
27+
ds.Continent.code, ds.Continent.name
28+
)
29+
)
30+
)
31+
32+
result = await session.execute(query)
33+
print(result)
34+
35+
# This can also be written as:
36+
37+
# I want to query the continents
38+
query_continents = ds.Query.continents
39+
40+
# I want to get only the continents with code equal to "EU"
41+
query_continents(filter={"code": {"eq": "EU"}})
42+
43+
# I want this query to return the code and name fields
44+
query_continents.select(ds.Continent.code)
45+
query_continents.select(ds.Continent.name)
46+
47+
# I generate a document from my query to be able to execute it
48+
query = dsl_gql(DSLQuery(query_continents))
49+
50+
# Execute the query
51+
result = await session.execute(query)
52+
print(result)
53+
54+
55+
asyncio.run(main())

docs/code_examples/requests_sync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from gql import Client, gql
22
from gql.transport.requests import RequestsHTTPTransport
33

4-
sample_transport = RequestsHTTPTransport(
4+
transport = RequestsHTTPTransport(
55
url="https://countries.trevorblades.com/", verify=True, retries=3,
66
)
77

8-
client = Client(transport=sample_transport, fetch_schema_from_transport=True,)
8+
client = Client(transport=transport, fetch_schema_from_transport=True)
99

1010
query = gql(
1111
"""
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from gql import Client
2+
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
3+
from gql.transport.requests import RequestsHTTPTransport
4+
5+
transport = RequestsHTTPTransport(
6+
url="https://countries.trevorblades.com/", verify=True, retries=3,
7+
)
8+
9+
client = Client(transport=transport, fetch_schema_from_transport=True)
10+
11+
# Using `with` on the sync client will start a connection on the transport
12+
# and provide a `session` variable to execute queries on this connection.
13+
# Because we requested to fetch the schema from the transport,
14+
# GQL will fetch the schema just after the establishment of the first session
15+
with client as session:
16+
17+
# We should have received the schema now that the session is established
18+
assert client.schema is not None
19+
20+
# Instanciate the root of the DSL Schema as ds
21+
ds = DSLSchema(client.schema)
22+
23+
# Create the query using dynamically generated attributes from ds
24+
query = dsl_gql(
25+
DSLQuery(ds.Query.continents.select(ds.Continent.code, ds.Continent.name))
26+
)
27+
28+
result = session.execute(query)
29+
print(result)

docs/modules/client.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Client
2-
======
1+
gql.client
2+
==========
33

44
.. currentmodule:: gql.client
55

docs/modules/dsl.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
gql.dsl
2+
=======
3+
4+
.. currentmodule:: gql.dsl
5+
6+
.. automodule:: gql.dsl
7+
:member-order: bysource

docs/modules/gql.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ Sub-Packages
2020

2121
client
2222
transport
23+
dsl

docs/modules/transport.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Transport
2-
=========
1+
gql.transport
2+
=============
33

44
.. currentmodule:: gql.transport
55

0 commit comments

Comments
 (0)