diff --git a/MANIFEST.in b/MANIFEST.in index 72ce48f7..fbaa10b4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,7 +13,10 @@ include tox.ini include scripts/gql-cli recursive-include tests *.py *.graphql *.cnf *.yaml *.pem +recursive-include docs *.txt *.rst conf.py Makefile make.bat *.jpg *.png *.gif +recursive-include docs/code_examples *.py +prune docs/_build prune gql-checker global-exclude *.py[co] __pycache__ diff --git a/Makefile b/Makefile index a425508a..27913507 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean tests +.PHONY: clean tests docs dev-setup: python pip install -e ".[test]" @@ -16,6 +16,10 @@ check: mypy gql tests check-manifest +docs: + rm -rf ./docs/_build + cd docs; make html + clean: find . -name "*.pyc" -delete find . -name "__pycache__" | xargs -I {} rm -rf {} @@ -26,4 +30,5 @@ clean: rm -rf ./gql.egg-info rm -rf ./dist rm -rf ./build + rm -rf ./docs/_build rm -f ./.coverage diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/advanced/async_advanced_usage.rst b/docs/advanced/async_advanced_usage.rst new file mode 100644 index 00000000..4164cb37 --- /dev/null +++ b/docs/advanced/async_advanced_usage.rst @@ -0,0 +1,57 @@ +.. _async_advanced_usage: + +Async advanced usage +==================== + +It is possible to send multiple GraphQL queries (query, mutation or subscription) in parallel, +on the same websocket connection, using asyncio tasks. + +In order to retry in case of connection failure, we can use the great `backoff`_ module. + +.. code-block:: python + + # First define all your queries using a session argument: + + async def execute_query1(session): + result = await session.execute(query1) + print(result) + + async def execute_query2(session): + result = await session.execute(query2) + print(result) + + async def execute_subscription1(session): + async for result in session.subscribe(subscription1): + print(result) + + async def execute_subscription2(session): + async for result in session.subscribe(subscription2): + print(result) + + # Then create a couroutine which will connect to your API and run all your queries as tasks. + # We use a `backoff` decorator to reconnect using exponential backoff in case of connection failure. + + @backoff.on_exception(backoff.expo, Exception, max_time=300) + async def graphql_connection(): + + transport = WebsocketsTransport(url="wss://YOUR_URL") + + client = Client(transport=transport, fetch_schema_from_transport=True) + + async with client as session: + task1 = asyncio.create_task(execute_query1(session)) + task2 = asyncio.create_task(execute_query2(session)) + task3 = asyncio.create_task(execute_subscription1(session)) + task4 = asyncio.create_task(execute_subscription2(session)) + + await asyncio.gather(task1, task2, task3, task4) + + asyncio.run(graphql_connection()) + +Subscriptions tasks can be stopped at any time by running + +.. code-block:: python + + task.cancel() + +.. _backoff: https://github.com/litl/backoff diff --git a/docs/advanced/dsl_module.rst b/docs/advanced/dsl_module.rst new file mode 100644 index 00000000..aa6638df --- /dev/null +++ b/docs/advanced/dsl_module.rst @@ -0,0 +1,34 @@ +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. + +.. code-block:: python + + from gql.dsl import DSLSchema + + client = Client(schema=StarWarsSchema) + ds = DSLSchema(client) + + query_dsl = ds.Query.hero.select( + ds.Character.id, + ds.Character.name, + ds.Character.friends.select(ds.Character.name,), + ) + +will create a query equivalent to: + +.. code-block:: python + + hero { + id + name + friends { + name + } + } + +.. warning:: + + Please note that the DSL module is still considered experimental in GQL 3 and is subject to changes diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst new file mode 100644 index 00000000..3767112b --- /dev/null +++ b/docs/advanced/index.rst @@ -0,0 +1,9 @@ +Advanced +======== + +.. toctree:: + :maxdepth: 2 + + async_advanced_usage + local_schema + dsl_module diff --git a/docs/advanced/local_schema.rst b/docs/advanced/local_schema.rst new file mode 100644 index 00000000..1ceadb12 --- /dev/null +++ b/docs/advanced/local_schema.rst @@ -0,0 +1,25 @@ +Execution on a local schema +=========================== + +It is also possible to execute queries against a local schema (so without a transport), even +if it is not really useful except maybe for testing. + +.. code-block:: python + + from gql import gql, Client + + from .someSchema import SampleSchema + + client = Client(schema=SampleSchema) + + query = gql(''' + { + hello + } + ''') + + result = client.execute(query) + +See `tests/starwars/test_query.py`_ for an example + +.. _tests/starwars/test_query.py: https://github.com/graphql-python/gql/blob/master/tests/starwars/test_query.py diff --git a/docs/async/async_intro.rst b/docs/async/async_intro.rst new file mode 100644 index 00000000..6d4fea37 --- /dev/null +++ b/docs/async/async_intro.rst @@ -0,0 +1,18 @@ +On previous versions of GQL, the code was `sync` only , it means that when you ran +`execute` on the Client, you could do nothing else in the current Thread and had to wait for +an answer or a timeout from the backend to continue. The only http library was `requests`, allowing only sync usage. + +From the version 3 of GQL, we support `sync` and `async` :ref:`transports ` using `asyncio`_. + +With the :ref:`async transports `, there is now the possibility to execute GraphQL requests +asynchronously, :ref:`allowing to execute multiple requests in parallel if needed `. + +If you don't care or need async functionality, it is still possible, with :ref:`async transports `, +to run the `execute` or `subscribe` methods directly from the Client +(as described in the :ref:`Basic Usage ` example) and GQL will execute the request +in a synchronous manner by running an asyncio event loop itself. + +This won't work though if you already have an asyncio event loop running. In that case you should use +:ref:`Async Usage ` + +.. _asyncio: https://docs.python.org/3/library/asyncio.html diff --git a/docs/async/async_usage.rst b/docs/async/async_usage.rst new file mode 100644 index 00000000..f0183751 --- /dev/null +++ b/docs/async/async_usage.rst @@ -0,0 +1,17 @@ +.. _async_usage: + +Async Usage +=========== + +If you use an :ref:`async transport `, you can use GQL asynchronously using `asyncio`_. + +* put your code in an asyncio coroutine (method starting with :code:`async def`) +* use :code:`async with client as session:` to connect to the backend and provide a session instance +* use the :code:`await` keyword to execute requests: :code:`await session.execute(...)` +* then run your coroutine in an asyncio event loop by running :code:`asyncio.run` + +Example: + +.. literalinclude:: ../code_examples/aiohttp_async.py + +.. _asyncio: https://docs.python.org/3/library/asyncio.html diff --git a/docs/async/index.rst b/docs/async/index.rst new file mode 100644 index 00000000..3f3d2a8a --- /dev/null +++ b/docs/async/index.rst @@ -0,0 +1,10 @@ +Async vs Sync +============= + +.. include:: async_intro.rst + +.. toctree:: + :hidden: + :maxdepth: 1 + + async_usage diff --git a/docs/code_examples/aiohttp_async.py b/docs/code_examples/aiohttp_async.py new file mode 100644 index 00000000..dec11c69 --- /dev/null +++ b/docs/code_examples/aiohttp_async.py @@ -0,0 +1,28 @@ +from gql import gql, AIOHTTPTransport, Client +import asyncio + +async def main(): + + transport = AIOHTTPTransport(url='https://countries.trevorblades.com/graphql') + + # Using `async with` on the client will start a connection on the transport + # and provide a `session` variable to execute queries on this connection + async with Client( + transport=transport, + fetch_schema_from_transport=True, + ) as session: + + # Execute single query + query = gql(''' + query getContinents { + continents { + code + name + } + } + ''') + + result = await session.execute(query) + print(result) + +asyncio.run(main()) diff --git a/docs/code_examples/aiohttp_sync.py b/docs/code_examples/aiohttp_sync.py new file mode 100644 index 00000000..296d4533 --- /dev/null +++ b/docs/code_examples/aiohttp_sync.py @@ -0,0 +1,23 @@ +from gql import gql, Client, AIOHTTPTransport + +# Select your transport with a defined url endpoint +transport = AIOHTTPTransport(url="https://countries.trevorblades.com/") + +# Create a GraphQL client using the defined transport +client = Client(transport=transport, fetch_schema_from_transport=True) + +# Provide a GraphQL query +query = gql( + """ + query getContinents { + continents { + code + name + } + } +""" +) + +# Execute the query on the transport +result = client.execute(query) +print(result) diff --git a/docs/code_examples/requests_sync.py b/docs/code_examples/requests_sync.py new file mode 100644 index 00000000..eb821add --- /dev/null +++ b/docs/code_examples/requests_sync.py @@ -0,0 +1,25 @@ +from gql import gql, Client +from gql.transport.requests import RequestsHTTPTransport + +sample_transport=RequestsHTTPTransport( + url='https://countries.trevorblades.com/', + verify=True, + retries=3, +) + +client = Client( + transport=sample_transport, + fetch_schema_from_transport=True, +) + +query = gql(''' + query getContinents { + continents { + code + name + } + } +''') + +result = client.execute(query) +print(result) diff --git a/docs/code_examples/websockets_async.py b/docs/code_examples/websockets_async.py new file mode 100644 index 00000000..b91b442f --- /dev/null +++ b/docs/code_examples/websockets_async.py @@ -0,0 +1,41 @@ +import logging +logging.basicConfig(level=logging.INFO) + +from gql import gql, Client, WebsocketsTransport +import asyncio + +async def main(): + + transport = WebsocketsTransport(url='wss://countries.trevorblades.com/graphql') + + # Using `async with` on the client will start a connection on the transport + # and provide a `session` variable to execute queries on this connection + async with Client( + transport=sample_transport, + fetch_schema_from_transport=True, + ) as session: + + # Execute single query + query = gql(''' + query getContinents { + continents { + code + name + } + } + ''') + result = await session.execute(query) + print(result) + + # Request subscription + subscription = gql(''' + subscription { + somethingChanged { + id + } + } + ''') + async for result in session.subscribe(subscription): + print(result) + +asyncio.run(main()) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..f1c281c1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,77 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('./..')) + + +# -- Project information ----------------------------------------------------- + +project = 'gql 3' +copyright = '2020, graphql-python.org' +author = 'graphql-python.org' + +# The full version, including alpha/beta/rc tags +release = '3.0.0a1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'gql-3-doc' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# -- AutoDoc configuration ------------------------------------------------- +# autoclass_content = "both" +autodoc_default_options = { + 'members': True, + 'inherited-members': True, + 'special-members': '__init__', + 'undoc-members': True, + 'show-inheritance': True +} +autosummary_generate = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..ead330e8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,28 @@ +Welcome to GQL 3 documentation! +================================= + +.. warning:: + + Please note that the following documentation describes the current version which is currently only available + as a pre-release and needs to be installed with "`--pre`" + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + intro + usage/index + async/index + transports/index + advanced/index + modules/gql + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 00000000..9ef1f2db --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,39 @@ +Introduction +============ + +`GQL 3`_ is a `GraphQL`_ Client for Python 3.6+ which plays nicely with other graphql implementations compatible with the spec. + +Under the hood, it uses `GraphQL-core`_ which is a Python port of `GraphQL.js`_, +the JavaScript reference implementation for GraphQL. + +Installation +------------ + +You can install GQL 3 using pip_:: + + pip install --pre gql + +.. warning:: + + Please note that the following documentation describes the current version which is currently only available + as a pre-release and needs to be installed with "`--pre`" + +After installation, you can start using GQL by importing from the top-level +:mod:`gql` package. + +Reporting Issues and Contributing +--------------------------------- + +Please visit the `GitHub repository for gql`_ if you're interested in the current development or +want to report issues or send pull requests. + +We welcome all kinds of contributions if the coding guidelines are respected. +Please check the `Contributing`_ file to learn how to make a good pull request. + +.. _GraphQL: https://graphql.org/ +.. _GraphQL-core: https://github.com/graphql-python/graphql-core +.. _GraphQL.js: https://github.com/graphql/graphql-js +.. _GQL 3: https://github.com/graphql-python/gql +.. _pip: https://pip.pypa.io/ +.. _GitHub repository for gql: https://github.com/graphql-python/gql +.. _Contributing: https://github.com/graphql-python/gql/blob/master/CONTRIBUTING.md diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..9534b018 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules/client.rst b/docs/modules/client.rst new file mode 100644 index 00000000..954b4e61 --- /dev/null +++ b/docs/modules/client.rst @@ -0,0 +1,6 @@ +Client +====== + +.. currentmodule:: gql.client + +.. automodule:: gql.client diff --git a/docs/modules/gql.rst b/docs/modules/gql.rst new file mode 100644 index 00000000..94121ea3 --- /dev/null +++ b/docs/modules/gql.rst @@ -0,0 +1,22 @@ +Reference +========= + +.. currentmodule:: gql + +.. _top-level-functions: + +Top-Level Functions +------------------- + +.. automodule:: gql + +.. _sub-packages: + +Sub-Packages +------------ + +.. toctree:: + :maxdepth: 1 + + client + transport diff --git a/docs/modules/transport.rst b/docs/modules/transport.rst new file mode 100644 index 00000000..dd4627e0 --- /dev/null +++ b/docs/modules/transport.rst @@ -0,0 +1,16 @@ +Transport +========= + +.. currentmodule:: gql.transport + +.. autoclass:: gql.transport.transport.Transport + +.. autoclass:: gql.transport.local_schema.LocalSchemaTransport + +.. autoclass:: gql.transport.requests.RequestsHTTPTransport + +.. autoclass:: gql.transport.async_transport.AsyncTransport + +.. autoclass:: gql.transport.aiohttp.AIOHTTPTransport + +.. autoclass:: gql.transport.websockets.WebsocketsTransport diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..64431755 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx>=3.0.0,<4 +sphinx_rtd_theme>=0.4,<1 \ No newline at end of file diff --git a/docs/transports/aiohttp.rst b/docs/transports/aiohttp.rst new file mode 100644 index 00000000..cdca6f45 --- /dev/null +++ b/docs/transports/aiohttp.rst @@ -0,0 +1,13 @@ +AIOHTTPTransport +================ + +This transport uses the `aiohttp`_ library and allows you to send GraphQL queries using the HTTP protocol. + +.. note:: + + GraphQL subscriptions are not supported on the HTTP transport. + For subscriptions you should use the :ref:`websockets transport `. + +.. literalinclude:: ../code_examples/aiohttp_async.py + +.. _aiohttp: https://docs.aiohttp.org diff --git a/docs/transports/async_transports.rst b/docs/transports/async_transports.rst new file mode 100644 index 00000000..9fb1b017 --- /dev/null +++ b/docs/transports/async_transports.rst @@ -0,0 +1,14 @@ +.. _async_transports: + +Async Transports +================ + +Async transports are transports which are using an underlying async library. They allow us to +:ref:`run GraphQL queries asynchronously ` + +.. toctree:: + :maxdepth: 1 + + aiohttp + websockets + phoenix diff --git a/docs/transports/index.rst b/docs/transports/index.rst new file mode 100644 index 00000000..f291c1d0 --- /dev/null +++ b/docs/transports/index.rst @@ -0,0 +1,13 @@ +.. _transports: + +Transports +========== + +GQL Transports are used to define how the connection is made with the backend. +We have different transports for different underlying protocols (http, websockets, ...) + +.. toctree:: + :maxdepth: 2 + + async_transports + sync_transports diff --git a/docs/transports/phoenix.rst b/docs/transports/phoenix.rst new file mode 100644 index 00000000..20a86c3d --- /dev/null +++ b/docs/transports/phoenix.rst @@ -0,0 +1,10 @@ +PhoenixChannelWebsocketsTransport +================================= + +The PhoenixChannelWebsocketsTransport is an **EXPERIMENTAL** async transport which allows you +to execute queries and subscriptions against an `Absinthe`_ backend using the `Phoenix`_ +framework `channels`_. + +.. _Absinthe: http://absinthe-graphql.org +.. _Phoenix: https://www.phoenixframework.org +.. _channels: https://hexdocs.pm/phoenix/Phoenix.Channel.html#content diff --git a/docs/transports/requests.rst b/docs/transports/requests.rst new file mode 100644 index 00000000..d1a5417c --- /dev/null +++ b/docs/transports/requests.rst @@ -0,0 +1,9 @@ +RequestsHTTPTransport +===================== + +The RequestsHTTPTransport is a sync transport using the `requests`_ library +and allows you to send GraphQL queries using the HTTP protocol. + +.. literalinclude:: ../code_examples/requests_sync.py + +.. _requests: https://requests.readthedocs.io diff --git a/docs/transports/sync_transports.rst b/docs/transports/sync_transports.rst new file mode 100644 index 00000000..3ed566d3 --- /dev/null +++ b/docs/transports/sync_transports.rst @@ -0,0 +1,12 @@ +.. _sync_transports: + +Sync Transports +================ + +Sync transports are transports which are using an underlying sync library. +They cannot be used asynchronously. + +.. toctree:: + :maxdepth: 1 + + requests diff --git a/docs/transports/websockets.rst b/docs/transports/websockets.rst new file mode 100644 index 00000000..a082d887 --- /dev/null +++ b/docs/transports/websockets.rst @@ -0,0 +1,71 @@ +.. _websockets_transport: + +WebsocketsTransport +=================== + +The websockets transport implements the `Apollo websockets transport protocol`_. + +This transport allows to do multiple queries, mutations and subscriptions on the same websocket connection. + +.. literalinclude:: ../code_examples/websockets_async.py + +Websockets SSL +-------------- + +If you need to connect to an ssl encrypted endpoint: + +* use _wss_ instead of _ws_ in the url of the transport + +.. code-block:: python + + sample_transport = WebsocketsTransport( + url='wss://SERVER_URL:SERVER_PORT/graphql', + headers={'Authorization': 'token'} + ) + +If you have a self-signed ssl certificate, you need to provide an ssl_context with the server public certificate: + +.. code-block:: python + + import pathlib + import ssl + + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + localhost_pem = pathlib.Path(__file__).with_name("YOUR_SERVER_PUBLIC_CERTIFICATE.pem") + ssl_context.load_verify_locations(localhost_pem) + + sample_transport = WebsocketsTransport( + url='wss://SERVER_URL:SERVER_PORT/graphql', + ssl=ssl_context + ) + +If you have also need to have a client ssl certificate, add: + +.. code-block:: python + + ssl_context.load_cert_chain(certfile='YOUR_CLIENT_CERTIFICATE.pem', keyfile='YOUR_CLIENT_CERTIFICATE_KEY.key') + +Websockets authentication +------------------------- + +There are two ways to send authentication tokens with websockets depending on the server configuration. + +1. Using HTTP Headers + +.. code-block:: python + + sample_transport = WebsocketsTransport( + url='wss://SERVER_URL:SERVER_PORT/graphql', + headers={'Authorization': 'token'} + ) + +2. With a payload in the connection_init websocket message + +.. code-block:: python + + sample_transport = WebsocketsTransport( + url='wss://SERVER_URL:SERVER_PORT/graphql', + init_payload={'Authorization': 'token'} + ) + +.. _Apollo websockets transport protocol: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md diff --git a/docs/usage/basic_usage.rst b/docs/usage/basic_usage.rst new file mode 100644 index 00000000..d53c18d5 --- /dev/null +++ b/docs/usage/basic_usage.rst @@ -0,0 +1,21 @@ +.. _basic_usage: + +Basic usage +----------- + +In order to execute a GraphQL request against a GraphQL API: + +* create your gql :ref:`transport ` in order to choose the destination url + and the protocol used to communicate with it +* create a gql :class:`Client ` with the selected transport +* parse a query using :func:`gql ` +* execute the query on the client to get the result + +.. literalinclude:: ../code_examples/aiohttp_sync.py + +.. warning:: + + Please note that this basic example won't work if you have an asyncio event loop running. In some + python environments (as with Jupyter which uses IPython) an asyncio event loop is created for you. + In that case you should use instead the :ref:`Async Usage example`. + diff --git a/docs/usage/headers.rst b/docs/usage/headers.rst new file mode 100644 index 00000000..23af64a7 --- /dev/null +++ b/docs/usage/headers.rst @@ -0,0 +1,8 @@ +HTTP Headers +============ + +If you want to add additional http headers for your connection, you can specify these in your transport: + +.. code-block:: python + + transport = AIOHTTPTransport(url='YOUR_URL', headers={'Authorization': 'token'}) diff --git a/docs/usage/index.rst b/docs/usage/index.rst new file mode 100644 index 00000000..2d5d5fd3 --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,11 @@ +Usage +===== + +.. toctree:: + :maxdepth: 2 + + basic_usage + validation + subscriptions + variables + headers diff --git a/docs/usage/subscriptions.rst b/docs/usage/subscriptions.rst new file mode 100644 index 00000000..15645b06 --- /dev/null +++ b/docs/usage/subscriptions.rst @@ -0,0 +1,28 @@ +Subscriptions +============= + +Using the :ref:`websockets transport `, it is possible to execute GraphQL subscriptions: + +.. code-block:: python + + from gql import gql, Client, WebsocketsTransport + + transport = WebsocketsTransport(url='wss://your_server/graphql') + + client = Client( + transport=transport, + fetch_schema_from_transport=True, + ) + + query = gql(''' + subscription yourSubscription { + ... + } + ''') + + for result in client.subscribe(query): + print (result) + +.. note:: + + The websockets transport can also execute queries or mutations, it is not restricted to subscriptions diff --git a/docs/usage/validation.rst b/docs/usage/validation.rst new file mode 100644 index 00000000..0e840cd2 --- /dev/null +++ b/docs/usage/validation.rst @@ -0,0 +1,43 @@ +.. _schema_validation: + +Schema validation +================= + +It a GraphQL schema is provided, gql will validate the queries locally before sending them to the backend. +If no schema is provided, gql will send the query to the backend without local validation. + +You can either provide a schema yourself, or you can request gql to get the schema +from the backend using `introspection`_. + +Using a provided schema +----------------------- + +The schema can be provided as a String (which is usually stored in a .graphql file): + +.. code-block:: python + + with open('path/to/schema.graphql') as f: + schema_str = f.read() + + client = Client(schema=schema_str) + +OR can be created using python classes: + +.. code-block:: python + + from .someSchema import SampleSchema + # SampleSchema is an instance of GraphQLSchema + + client = Client(schema=SampleSchema) + +See `tests/starwars/schema.py`_ for an example of such a schema. + +Using introspection +------------------- + +In order to get the schema directly from the GraphQL Server API using the transport, you need +to set the `fetch_schema_from_transport` argument of Client to True, and the client will +fetch the schema before the execution of the first query. + +.. _introspection: https://graphql.org/learn/introspection +.. _tests/starwars/schema.py: https://github.com/graphql-python/gql/blob/master/tests/starwars/schema.py diff --git a/docs/usage/variables.rst b/docs/usage/variables.rst new file mode 100644 index 00000000..81924c6e --- /dev/null +++ b/docs/usage/variables.rst @@ -0,0 +1,32 @@ +Using variables +=============== + +It is possible to provide variable values with your query by providing a Dict to +the variable_values argument of the `execute` or the `subscribe` methods. + +The variable values will be sent alongside the query in the transport message +(there is no local substitution). + +.. code-block:: python + + query = gql( + """ + query getContinentName ($code: ID!) { + continent (code: $code) { + name + } + } + """ + ) + + params = {"code": "EU"} + + # Get name of continent with code "EU" + result = client.execute(query, variable_values=params) + print(result) + + params = {"code": "AF"} + + # Get name of continent with code "AF" + result = client.execute(query, variable_values=params) + print(result) diff --git a/gql/__init__.py b/gql/__init__.py index bad425d4..fefd91ad 100644 --- a/gql/__init__.py +++ b/gql/__init__.py @@ -1,3 +1,12 @@ +"""The primary :mod:`gql` package includes everything you need to +execute GraphQL requests: + + - the :func:`gql ` method to parse a GraphQL query + - the :class:`Client ` class as the entrypoint to execute requests + and create sessions + - all the transports classes implementing different communication protocols +""" + from .client import Client from .gql import gql from .transport.aiohttp import AIOHTTPTransport diff --git a/gql/client.py b/gql/client.py index 13f67327..af5ae28b 100644 --- a/gql/client.py +++ b/gql/client.py @@ -20,6 +20,25 @@ class Client: + """The Client class is the main entrypoint to execute GraphQL requests + on a GQL transport. + + It can take sync or async transports as argument and can either execute + and subscribe to requests itself with the + :func:`execute ` and + :func:`subscribe ` methods + OR can be used to get a sync or async session depending on the + transport type. + + To connect to an :ref:`async transport ` and get an + :class:`async session `, + use :code:`async with client as session:` + + To connect to a :ref:`sync transport ` and get a + :class:`sync session `, + use :code:`with client as session:` + """ + def __init__( self, schema: Optional[Union[str, GraphQLSchema]] = None, @@ -29,6 +48,16 @@ def __init__( fetch_schema_from_transport: bool = False, execute_timeout: Optional[int] = 10, ): + """Initialize the client with the given parameters. + + :param schema: an optional GraphQL Schema for local validation + See :ref:`schema_validation` + :param transport: The provided :ref:`transport `. + :param fetch_schema_from_transport: Boolean to indicate that if we want to fetch + the schema from the transport using an introspection query + :param execute_timeout: The maximum time in seconds for the execution of a + request before a TimeoutError is raised + """ assert not ( type_def and introspection ), "Cannot provide introspection and type definition at the same time." @@ -81,7 +110,8 @@ def __init__( with self as session: session.fetch_schema() - def validate(self, document): + def validate(self, document: DocumentNode): + """:meta private:""" assert ( self.schema ), "Cannot validate the document locally, you need to pass a schema." @@ -91,21 +121,36 @@ def validate(self, document): raise validation_errors[0] def execute_sync(self, document: DocumentNode, *args, **kwargs) -> Dict: + """:meta private:""" with self as session: return session.execute(document, *args, **kwargs) async def execute_async(self, document: DocumentNode, *args, **kwargs) -> Dict: + """:meta private:""" async with self as session: return await session.execute(document, *args, **kwargs) def execute(self, document: DocumentNode, *args, **kwargs) -> Dict: - """Execute the provided document AST against the configured remote server. + """Execute the provided document AST against the remote server using + the transport provided during init. - This function WILL BLOCK until the result is received from the server. + This function **WILL BLOCK** until the result is received from the server. Either the transport is sync and we execute the query synchronously directly OR the transport is async and we execute the query in the asyncio loop (blocking here until answer). + + This method will: + + - connect using the transport to get a session + - execute the GraphQL request on the transport session + - close the session and close the connection to the server + + If you have multiple requests to send, it is better to get your own session + and execute the requests in your session. + + The extra arguments passed in the method will be passed to the transport + execute method. """ if isinstance(self.transport, AsyncTransport): @@ -135,6 +180,7 @@ def execute(self, document: DocumentNode, *args, **kwargs) -> Dict: async def subscribe_async( self, document: DocumentNode, *args, **kwargs ) -> AsyncGenerator[Dict, None]: + """:meta private:""" async with self as session: generator: AsyncGenerator[Dict, None] = session.subscribe( @@ -228,13 +274,14 @@ def __exit__(self, *args): class SyncClientSession: - """An instance of this class is created when using 'with' on the client. + """An instance of this class is created when using :code:`with` on the client. It contains the sync method execute to send queries - with the sync transports. + on a sync transport using the same session. """ def __init__(self, client: Client): + """:param client: the :class:`client ` used""" self.client = client def _execute(self, document: DocumentNode, *args, **kwargs) -> ExecutionResult: @@ -263,6 +310,10 @@ def execute(self, document: DocumentNode, *args, **kwargs) -> Dict: return result.data def fetch_schema(self) -> None: + """Fetch the GraphQL schema explicitely using introspection. + + Don't use this function and instead set the fetch_schema_from_transport + attribute to True""" execution_result = self.transport.execute(parse(get_introspection_query())) self.client.introspection = execution_result.data self.client.schema = build_client_schema(self.client.introspection) @@ -273,13 +324,15 @@ def transport(self): class AsyncClientSession: - """An instance of this class is created when using 'async with' on the client. + """An instance of this class is created when using :code:`async with` on a + :class:`client `. It contains the async methods (execute, subscribe) to send queries - with the async transports. + on an async transport using the same session. """ def __init__(self, client: Client): + """:param client: the :class:`client ` used""" self.client = client async def fetch_and_validate(self, document: DocumentNode): @@ -323,6 +376,10 @@ async def _subscribe( async def subscribe( self, document: DocumentNode, *args, **kwargs ) -> AsyncGenerator[Dict, None]: + """Coroutine to subscribe asynchronously to the provided document AST + asynchronously using the async transport. + + The extra arguments are passed to the transport subscribe method.""" # Validate and subscribe on the transport async for result in self._subscribe(document, *args, **kwargs): @@ -339,7 +396,6 @@ async def subscribe( async def _execute( self, document: DocumentNode, *args, **kwargs ) -> ExecutionResult: - # Fetch schema from transport if needed and validate document if possible await self.fetch_and_validate(document) @@ -350,6 +406,10 @@ async def _execute( ) async def execute(self, document: DocumentNode, *args, **kwargs) -> Dict: + """Coroutine to execute the provided document AST asynchronously using + the async transport. + + The extra arguments are passed to the transport execute method.""" # Validate and execute on the transport result = await self._execute(document, *args, **kwargs) @@ -367,6 +427,10 @@ async def execute(self, document: DocumentNode, *args, **kwargs) -> Dict: return result.data async def fetch_schema(self) -> None: + """Fetch the GraphQL schema explicitely using introspection. + + Don't use this function and instead set the fetch_schema_from_transport + attribute to True""" execution_result = await self.transport.execute( parse(get_introspection_query()) ) diff --git a/gql/gql.py b/gql/gql.py index 221710ed..903c9609 100644 --- a/gql/gql.py +++ b/gql/gql.py @@ -2,5 +2,16 @@ def gql(request_string: str) -> DocumentNode: + """Given a String containing a GraphQL request, parse it into a Document. + + :param request_string: the GraphQL request as a String + :type request_string: str + :return: a Document which can be later executed or subscribed by a + :class:`Client `, by an + :class:`async session ` or by a + :class:`sync session ` + + :raises GraphQLError: if a syntax error is encountered. + """ source = Source(request_string, "GraphQL request") return parse(source) diff --git a/gql/transport/aiohttp.py b/gql/transport/aiohttp.py index bdd53852..2ae83999 100644 --- a/gql/transport/aiohttp.py +++ b/gql/transport/aiohttp.py @@ -18,11 +18,10 @@ class AIOHTTPTransport(AsyncTransport): - """Transport to execute GraphQL queries on remote servers with an http connection. + """:ref:`Async Transport ` to execute GraphQL queries + on remote servers with an HTTP connection. - This transport use the aiohttp library with asyncio - - See README.md for Usage + This transport use the aiohttp library with asyncio. """ def __init__( @@ -42,7 +41,11 @@ def __init__( :param cookies: Dict of HTTP cookies. :param auth: BasicAuth object to enable Basic HTTP auth if needed :param ssl: ssl_context of the connection. Use ssl=False to disable encryption - :param client_session_args: Dict of extra args passed to aiohttp.ClientSession + :param client_session_args: Dict of extra args passed to + `aiohttp.ClientSession`_ + + .. _aiohttp.ClientSession: + https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.ClientSession """ self.url: str = url self.headers: Optional[LooseHeaders] = headers @@ -55,11 +58,13 @@ def __init__( self.session: Optional[aiohttp.ClientSession] = None async def connect(self) -> None: - """Coroutine which will: + """Coroutine which will create an aiohttp ClientSession() as self.session. - - create an aiohttp ClientSession() as self.session + Don't call this coroutine directly on the transport, instead use + :code:`async with` on the client and this coroutine will be executed + to create the session. - Should be cleaned with a call to the close coroutine + Should be cleaned with a call to the close coroutine. """ if self.session is None: @@ -84,6 +89,12 @@ async def connect(self) -> None: raise TransportAlreadyConnected("Transport is already connected") async def close(self) -> None: + """Coroutine which will close the aiohttp session. + + Don't call this coroutine directly on the transport, instead use + :code:`async with` on the client and this coroutine will be executed + when you exit the async context manager. + """ if self.session is not None: await self.session.close() self.session = None @@ -95,11 +106,19 @@ async def execute( operation_name: Optional[str] = None, extra_args: Dict[str, Any] = {}, ) -> ExecutionResult: - """Execute the provided document AST against the configured remote server. + """Execute the provided document AST against the configured remote server + using the current session. This uses the aiohttp library to perform a HTTP POST request asynchronously to the remote server. - The result is sent as an ExecutionResult object. + Don't call this coroutine directly on the transport, instead use + :code:`execute` on a client or a session. + + :param document: the parsed GraphQL request + :param variables_values: An optional Dict of variable values + :param operation_name: An optional Operation name for the request + :param extra_args: additional arguments to send to the aiohttp post method + :returns: an ExecutionResult object. """ query_str = print_ast(document) @@ -149,4 +168,8 @@ def subscribe( variable_values: Optional[Dict[str, str]] = None, operation_name: Optional[str] = None, ) -> AsyncGenerator[ExecutionResult, None]: + """Subscribe is not supported on HTTP. + + :meta private: + """ raise NotImplementedError(" The HTTP transport does not support subscriptions") diff --git a/gql/transport/phoenix_channel_websockets.py b/gql/transport/phoenix_channel_websockets.py index 6e96b72e..aaa6686a 100644 --- a/gql/transport/phoenix_channel_websockets.py +++ b/gql/transport/phoenix_channel_websockets.py @@ -14,19 +14,28 @@ class PhoenixChannelWebsocketsTransport(WebsocketsTransport): + """The PhoenixChannelWebsocketsTransport is an **EXPERIMENTAL** async transport + which allows you to execute queries and subscriptions against an `Absinthe`_ + backend using the `Phoenix`_ framework `channels`_. + + .. _Absinthe: http://absinthe-graphql.org + .. _Phoenix: https://www.phoenixframework.org + .. _channels: https://hexdocs.pm/phoenix/Phoenix.Channel.html#content + """ + def __init__( self, channel_name: str, heartbeat_interval: float = 30, *args, **kwargs ) -> None: + """Initialize the transport with the given parameters. + + :param channel_name: Channel on the server this transport will join + :param heartbeat_interval: Interval in second between each heartbeat messages + sent by the client + """ self.channel_name = channel_name self.heartbeat_interval = heartbeat_interval self.subscription_ids_to_query_ids: Dict[str, int] = {} super(PhoenixChannelWebsocketsTransport, self).__init__(*args, **kwargs) - """Initialize the transport with the given request parameters. - - :param channel_name Channel on the server this transport will join - :param heartbeat_interval Interval in second between each heartbeat messages - sent by the client - """ async def _send_init_message_and_wait_ack(self) -> None: """Join the specified channel and wait for the connection ACK. diff --git a/gql/transport/requests.py b/gql/transport/requests.py index 8eb4b2f8..823d0bc6 100644 --- a/gql/transport/requests.py +++ b/gql/transport/requests.py @@ -17,7 +17,8 @@ class RequestsHTTPTransport(Transport): - """Transport to execute GraphQL queries on remote servers. + """:ref:`Sync Transport ` used to execute GraphQL queries + on remote servers. The transport uses the requests library to send HTTP POST requests. """ @@ -53,7 +54,7 @@ def __init__( :param retries: Pre-setup of the requests' Session for performing retries :param method: HTTP method used for requests. (Default: POST). :param kwargs: Optional arguments that ``request`` takes. - These can be seen at the :requests_: source code or the official :docs_: + These can be seen at the `requests`_ source code or the official `docs`_ .. _requests: https://github.com/psf/requests/blob/master/requests/api.py .. _docs: https://requests.readthedocs.io/en/master/ diff --git a/gql/transport/websockets.py b/gql/transport/websockets.py index b4552b8c..63af4703 100644 --- a/gql/transport/websockets.py +++ b/gql/transport/websockets.py @@ -78,12 +78,11 @@ async def set_exception(self, exception: Exception) -> None: class WebsocketsTransport(AsyncTransport): - """Transport to execute GraphQL queries on remote servers with websocket connection. + """:ref:`Async Transport ` used to execute GraphQL queries on + remote servers with websocket connection. This transport uses asyncio and the websockets library in order to send requests on a websocket connection. - - See README.md for usage. """ def __init__( @@ -97,7 +96,7 @@ def __init__( ack_timeout: int = 10, connect_args: Dict[str, Any] = {}, ) -> None: - """Initialize the transport with the given request parameters. + """Initialize the transport with the given parameters. :param url: The GraphQL server URL. Example: 'wss://server.com:PORT/graphql'. :param headers: Dict of HTTP Headers. @@ -454,7 +453,8 @@ async def execute( variable_values: Optional[Dict[str, str]] = None, operation_name: Optional[str] = None, ) -> ExecutionResult: - """Execute a GrqphQLQuery. + """Execute the provided document AST against the configured remote server + using the current session. Send a query but close the async generator as soon as we have the first answer. diff --git a/setup.py b/setup.py index 2a240290..27b9ac8e 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,8 @@ "flake8==3.8.1", "isort==4.3.21", "mypy==0.770", + "sphinx>=3.0.0,<4", + "sphinx_rtd_theme>=0.4,<1" ] + tests_require setup( diff --git a/tox.ini b/tox.ini index 36d97dd4..7f4eb158 100644 --- a/tox.ini +++ b/tox.ini @@ -54,6 +54,12 @@ deps = -e.[dev] commands = mypy gql tests +[testenv:docs] +basepython = python3.8 +deps = -e.[dev] +commands = + sphinx-build -b html -nEW docs docs/_build/html + [testenv:manifest] basepython = python3.8 deps = -e.[dev]