Skip to content
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

Refactor DSL code #169

Merged
merged 23 commits into from
Nov 21, 2020
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
246df96
DSL Refactor 1
leszekhanusz Nov 6, 2020
9d501a3
DSL Refactor 2
leszekhanusz Nov 6, 2020
921ea69
DSL Refactor 3
leszekhanusz Nov 7, 2020
8d34d31
DSL Refactor 4
leszekhanusz Nov 7, 2020
2b80179
Fix annotations for python 3.6
leszekhanusz Nov 7, 2020
79b082b
DSL Refactor 5
leszekhanusz Nov 7, 2020
c926a3d
DSL Refactor 6
leszekhanusz Nov 7, 2020
e0c7e34
DSL Refactor 7
leszekhanusz Nov 7, 2020
5e130cf
DOC document the dsl code
leszekhanusz Nov 8, 2020
0ffe5b9
DSL Refactor 8
leszekhanusz Nov 8, 2020
7406c7a
Merge branch 'master' into refactor_dsl
leszekhanusz Nov 8, 2020
dca2832
DOCS adding sphinx docs for the DSL module
leszekhanusz Nov 8, 2020
7501c17
Fix typo and modify doc following @KingDarBoja advice
leszekhanusz Nov 8, 2020
9974899
Add alias to selection as keyword argument at DSL
KingDarBoja Nov 8, 2020
db654f9
small cleaning
leszekhanusz Nov 11, 2020
fa8e0fe
Remove GraphQLTypeWithFields alias
leszekhanusz Nov 12, 2020
ec57e46
Rephrase DSLType comment
leszekhanusz Nov 12, 2020
2b8f985
fix _get_arg_serializer typing
leszekhanusz Nov 12, 2020
49eecd3
Really Fix nested input arguments this time
leszekhanusz Nov 12, 2020
dbfc221
The _get_arg_serializer method is not needed...
leszekhanusz Nov 13, 2020
3201c05
Allow aliases as keyword arguments for dsl_gql + new operation_name a…
leszekhanusz Nov 14, 2020
27e0eb7
Another refactor to allow multiple operations in documents
leszekhanusz Nov 14, 2020
a2d56d9
Merge branch 'master' into refactor_dsl
leszekhanusz Nov 21, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ The main features of GQL are:
* 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)
* Supports [File uploads](https://gql.readthedocs.io/en/latest/usage/file_upload.html)
* [gql-cli script](https://gql.readthedocs.io/en/latest/gql-cli/intro.html) to execute GraphQL queries from the command line
* [DSL module](https://gql.readthedocs.io/en/latest/advanced/dsl_module.html) to compose GraphQL queries dynamically

## Installation

224 changes: 208 additions & 16 deletions docs/advanced/dsl_module.rst
Original file line number Diff line number Diff line change
@@ -2,33 +2,225 @@ Compose queries dynamically
===========================

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

The following code:

.. code-block:: python

from gql.dsl import DSLSchema
ds = DSLSchema(StarWarsSchema)

query = dsl_gql(
DSLQuery(
ds.Query.hero.select(
ds.Character.id,
ds.Character.name,
ds.Character.friends.select(ds.Character.name),
)
)
)

will generate a query equivalent to:

.. code-block:: python

query = gql("""
query {
hero {
id
name
friends {
name
}
}
}
""")

How to use
----------

First generate the root using the :class:`DSLSchema <gql.dsl.DSLSchema>`::

ds = DSLSchema(client.schema)

Then use auto-generated attributes of the :code:`ds` instance
to get a root type (Query, Mutation or Subscription).
This will generate a :class:`DSLType <gql.dsl.DSLType>` instance::

ds.Query

From this root type, you use auto-generated attributes to get a field.
This will generate a :class:`DSLField <gql.dsl.DSLField>` instance::

ds.Query.hero

hero is a GraphQL object type and needs children fields. By default,
there is no children fields selected. To select the fields that you want
in your query, you use the :meth:`select <gql.dsl.DSLField.select>` method.

To generate the children fields, we use the same method as above to auto-generate the fields
from the :code:`ds` instance
(ie :code:`ds.Character.name` is the field `name` of the type `Character`)::

ds.Query.hero.select(ds.Character.name)

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

query_dsl = ds.Query.hero.select(
ds.Query.hero.select(ds.Character.name).select(ds.Character.id)

Or do it sequencially::

hero_query = ds.Query.hero

hero_query.select(ds.Character.name)
hero_query.select(ds.Character.id)

As you can select children fields of any object type, you can construct your complete query tree::

ds.Query.hero.select(
ds.Character.id,
ds.Character.name,
ds.Character.friends.select(ds.Character.name,),
ds.Character.friends.select(ds.Character.name),
)

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

.. code-block:: python
DSLQuery(
ds.Query.hero.select(
ds.Character.id,
ds.Character.name,
ds.Character.friends.select(ds.Character.name),
)
)


Once your operations are defined,
use the :func:`dsl_gql <gql.dsl.dsl_gql>` function to convert your operations into
a document which will be able to get executed in the client or a session::

query = dsl_gql(
DSLQuery(
ds.Query.hero.select(
ds.Character.id,
ds.Character.name,
ds.Character.friends.select(ds.Character.name),
)
)
)

result = client.execute(query)

Arguments
^^^^^^^^^

It is possible to add arguments to any field simply by calling it
with the required arguments::

ds.Query.human(id="1000").select(ds.Human.name)

It can also be done using the :meth:`args <gql.dsl.DSLField.args>` method::

ds.Query.human.args(id="1000").select(ds.Human.name)

hero {
id
name
friends {
name
}
Aliases
^^^^^^^

You can set an alias of a field using the :meth:`alias <gql.dsl.DSLField.alias>` method::

ds.Query.human.args(id=1000).alias("luke").select(ds.Character.name)

It is also possible to set the alias directly using keyword arguments of an operation::

DSLQuery(
luke=ds.Query.human.args(id=1000).select(ds.Character.name)
)

Or using keyword arguments in the :meth:`select <gql.dsl.DSLField.select>` method::

ds.Query.hero.select(
my_name=ds.Character.name
)

Mutations
^^^^^^^^^

For the mutations, you need to start from root fields starting from :code:`ds.Mutation`
then you need to create the GraphQL operation using the class
:class:`DSLMutation <gql.dsl.DSLMutation>`. Example::

query = dsl_gql(
DSLMutation(
ds.Mutation.createReview.args(
episode=6, review={"stars": 5, "commentary": "This is a great movie!"}
).select(ds.Review.stars, ds.Review.commentary)
)
)

Subscriptions
^^^^^^^^^^^^^

For the subscriptions, you need to start from root fields starting from :code:`ds.Subscription`
then you need to create the GraphQL operation using the class
:class:`DSLSubscription <gql.dsl.DSLSubscription>`. Example::

query = dsl_gql(
DSLSubscription(
ds.Subscription.reviewAdded(episode=6).select(ds.Review.stars, ds.Review.commentary)
)
)

Multiple fields in an operation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is possible to create an operation with multiple fields::

DSLQuery(
ds.Query.hero.select(ds.Character.name),
hero_of_episode_5=ds.Query.hero(episode=5).select(ds.Character.name),
)

Operation name
^^^^^^^^^^^^^^

You can set the operation name of an operation using a keyword argument
to :func:`dsl_gql <gql.dsl.dsl_gql>`::

query = dsl_gql(
GetHeroName=DSLQuery(ds.Query.hero.select(ds.Character.name))
)

will generate the request::

query GetHeroName {
hero {
name
}
}

.. warning::
Multiple operations in a document
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is possible to create an Document with multiple operations::

query = dsl_gql(
operation_name_1=DSLQuery( ... ),
operation_name_2=DSLQuery( ... ),
operation_name_3=DSLMutation( ... ),
)

Executable examples
-------------------

Async example
^^^^^^^^^^^^^

.. literalinclude:: ../code_examples/aiohttp_async_dsl.py

Sync example
^^^^^^^^^^^^^

.. literalinclude:: ../code_examples/requests_sync_dsl.py

Please note that the DSL module is still considered experimental in GQL 3 and is subject to changes
55 changes: 55 additions & 0 deletions docs/code_examples/aiohttp_async_dsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncio

from gql import Client
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
from gql.transport.aiohttp import AIOHTTPTransport


async def main():

transport = AIOHTTPTransport(url="https://countries.trevorblades.com/graphql")

client = Client(transport=transport, fetch_schema_from_transport=True)

# Using `async with` on the client will start a connection on the transport
# and provide a `session` variable to execute queries on this connection.
# Because we requested to fetch the schema from the transport,
# GQL will fetch the schema just after the establishment of the first session
async with client as session:

# Instanciate the root of the DSL Schema as ds
ds = DSLSchema(client.schema)

# Create the query using dynamically generated attributes from ds
query = dsl_gql(
DSLQuery(
ds.Query.continents(filter={"code": {"eq": "EU"}}).select(
ds.Continent.code, ds.Continent.name
)
)
)

result = await session.execute(query)
print(result)

# This can also be written as:

# I want to query the continents
query_continents = ds.Query.continents

# I want to get only the continents with code equal to "EU"
query_continents(filter={"code": {"eq": "EU"}})

# I want this query to return the code and name fields
query_continents.select(ds.Continent.code)
query_continents.select(ds.Continent.name)

# I generate a document from my query to be able to execute it
query = dsl_gql(DSLQuery(query_continents))

# Execute the query
result = await session.execute(query)
print(result)


asyncio.run(main())
4 changes: 2 additions & 2 deletions docs/code_examples/requests_sync.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport

sample_transport = RequestsHTTPTransport(
transport = RequestsHTTPTransport(
url="https://countries.trevorblades.com/", verify=True, retries=3,
)

client = Client(transport=sample_transport, fetch_schema_from_transport=True,)
client = Client(transport=transport, fetch_schema_from_transport=True)

query = gql(
"""
29 changes: 29 additions & 0 deletions docs/code_examples/requests_sync_dsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from gql import Client
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
from gql.transport.requests import RequestsHTTPTransport

transport = RequestsHTTPTransport(
url="https://countries.trevorblades.com/", verify=True, retries=3,
)

client = Client(transport=transport, fetch_schema_from_transport=True)

# Using `with` on the sync client will start a connection on the transport
# and provide a `session` variable to execute queries on this connection.
# Because we requested to fetch the schema from the transport,
# GQL will fetch the schema just after the establishment of the first session
with client as session:

# We should have received the schema now that the session is established
assert client.schema is not None

# Instanciate the root of the DSL Schema as ds
ds = DSLSchema(client.schema)

# Create the query using dynamically generated attributes from ds
query = dsl_gql(
DSLQuery(ds.Query.continents.select(ds.Continent.code, ds.Continent.name))
)

result = session.execute(query)
print(result)
4 changes: 2 additions & 2 deletions docs/modules/client.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Client
======
gql.client
==========

.. currentmodule:: gql.client

7 changes: 7 additions & 0 deletions docs/modules/dsl.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
gql.dsl
=======

.. currentmodule:: gql.dsl

.. automodule:: gql.dsl
:member-order: bysource
1 change: 1 addition & 0 deletions docs/modules/gql.rst
Original file line number Diff line number Diff line change
@@ -20,3 +20,4 @@ Sub-Packages

client
transport
dsl
4 changes: 2 additions & 2 deletions docs/modules/transport.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Transport
=========
gql.transport
=============

.. currentmodule:: gql.transport

Loading