{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-29T17:01:56.709375Z", "start_time": "2018-01-29T17:01:54.722626Z" }, "hide_input": false, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "# package imports\n", "\n", "# why all the try excepts?\n", "# because weird stuff happens\n", "# when this code is executed from a jupyter\n", "# notebook vs as a module vs as __main__\n", "\n", "from functools import partial\n", "from datetime import date as Date\n", "import asyncio\n", "import typing as T\n", "import sqlite3\n", "\n", "import pkg_resources\n", "\n", "try:\n", " from graphql_example.logging_utilities import *\n", "except ModuleNotFoundError:\n", " from logging_utilities import *\n", "\n", "try:\n", " from graphql_example.on_startup import (\n", " configure_logging,\n", " configure_database,\n", " create_tables,\n", " seed_db\n", ")\n", "except ModuleNotFoundError:\n", " from on_startup import (\n", " configure_logging,\n", " configure_database,\n", " create_tables,\n", " seed_db\n", " )\n", "\n", "\n", "try:\n", " from graphql_example.on_cleanup import drop_tables, close_db\n", "except:\n", " from on_cleanup import drop_tables, close_db\n", " \n", "try:\n", " from graphql_example.db_queries import fetch_authors, fetch_books\n", "except ModuleNotFoundError:\n", " from db_queries import fetch_authors, fetch_books\n", "\n", "from graphql.execution.executors.asyncio import AsyncioExecutor\n", "from aiohttp_graphql import GraphQLView\n", "from aiohttp import web\n", "\n", "import markdown" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# graphql + aiohttp" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "[**slideshow**](http://nbviewer.jupyter.org/format/slides/github/knowsuchagency/graphql-example/blob/master/graphql_example/graphql_example.ipynb?flush_cache=true#/)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "## Installation - requires Python 3.6\n", "\n", "preferably within a virtualenv:\n", "\n", "`pip3 install graphql-example`\n", "\n", "### to run the web-app\n", "\n", "(after pip install)\n", "\n", "`graphql_example runserver`\n", "\n", "### to run tests\n", "\n", "```\n", "git clone https://github.com/knowsuchagency/graphql-example\n", "pip3 install .[dev]\n", "fab test\n", "```" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### In the beginning... there was REST." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Really Genesis, shortly thereafter followed by SOAP" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### The Model" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let's say we had a simple data model such as the following:\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:57.972147Z", "start_time": "2018-01-28T22:27:57.960990Z" }, "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class Author:\n", " first_name: str\n", " last_name: str\n", " age: int\n", " books: T.Optional[T.List['Book']]\n", "\n", "\n", "class Book:\n", " title: str\n", " author: Author\n", " published: Date" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "In a RESTful server, the endpoints used to retrieve that data might look something like this:\n", "\n", "---\n", "\n", "/rest/author/{id}\n", "\n", "/rest/authors?limit=5?otherParam=value\n", "\n", "/rest/book/{id}\n", "\n", "/rest/books?author=\"Jim\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### What's wrong with this?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "Well, let's say on the client-side, we wanted to retrieve a set of authors using a query like\n", "\n", "`/rest/authors?age=34`\n", "\n", "We may get a structure like the following\n", "\n", "```\n", "[\n", " {\n", " \"first_name\": \"Amy\",\n", " \"last_name: \"Jones\",\n", " \"age\": 34,\n", " \"books\": [{...}]\n", " } ...\n", "]\n", "```" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "However, maybe we're unconcerned with the `books` field and all that extra information seems to be slowing down our page load.\n", "\n", "To mitigate this problem, maybe we add another filter to the authors endpoint i.e. `/rest/authors?age=34&no_books=true`\n", "\n", "Great, so now we get something like the following\n", "\n", "```\n", "[\n", " {\n", " \"first_name\": \"Amy\",\n", " \"last_name: \"Jones\",\n", " \"age\": 34,\n", " } ...\n", "]\n", "```" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "But...\n", "\n", "* What happens if we type `..?no_books=True`, `..?no_books=TRUE`, or `..?no_books=t`? \n", "\n", "In general, how do we define and interpret the arguments passed as query params on the server side?\n", "\n", "Furthermore, how to we enforce that contract with our clients and give them helpful feedback when mistakes are made?\n", "\n", "Also, what happens if we have a similar query to the one above, where we ARE interested in the `book` field for a given author, but only a subset of that data, such as `book.title` excluding all other data in the `book` field? \n", "\n", "Seems hardly worth creating another filter. We'll probably just suck it up and retrieve all the data in the book field and ignore what isn't needed on the client side." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### General considerations with REST\n", "\n", "* How do we handle different data types and serialization between the client server for complex data types?\n", "* How do we ensure that our rest api documenation is up-to-date? How do we ensure it's documented at all?\n", "* How do we communicate to the client what data can be retrieved from the server?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Solutions have been proposed\n", "\n", "Standards\n", "\n", "* [JSON-API](http://jsonapi.org/)\n", "* [OData](http://www.odata.org/)\n", "* [Falcor](https://github.com/Netflix/falcor)\n", "* [GraphQL](https://github.com/facebook/graphql)\n", "\n", "Frameworks\n", "\n", "* [Eve](http://python-eve.org/)\n", "* [apistar](https://github.com/encode/apistar)\n", "* [hug](https://github.com/timothycrosley/hug)\n", "* [graphene](http://graphene-python.org/)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### What makes GraphQL different?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "**Buzz**\n", "\n", "![buzz](https://i.imgur.com/zE4WD0C.jpg)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "But really, GraphQL is first-and-foremost a declarative language specification for client-side data fetching. \n", "\n", "And, despite the snark, the buzz is important. GraphQL is created and backed by Facebook, and there is a rapidly growing community and ecosystem of libraries that make GraphQL compelling over other standards like JSON-API, Falcor, or OData.\n", "\n", "A GraphQL-compliant server will be able to tell what information can be exchanged with the client and how, in a way that is more expressive and provides more guarantees of correctness than REST.\n", "\n", "Still, it's up to the backend engineer to correctly implement the spec, so it's not magic." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "On the server-side, having the client describe the specific shape of data it wants can allow the server to make smarter decisions as to how to serve that data i.e. re-shaping SQL expressions or batching database queries.\n", "\n", "On the client-side, it's great just to be able to introspect what's available and have a well-defined way of communicating with the server. Since the server will communicate what type of data can be sent/received, the client doesn't need to worry that the api documentation isn't up-to-date or doesn't exist." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Show me the code!\n", "\n", "\n", "We're going to implement a backend server that has a couple RESTful endpoints and one written in GraphQL to demonstrate our earlier points.\n", "\n", "The framework we'll use to build our server will be [aiohttp](https://aiohttp.readthedocs.io/en/stable/), which works on top of asyncio. This will allow us to write our `views` or `controllers` (depending on how one interprets MVC) using co-routines. This means requests can be processed asynchronously without resorting to muti-tenant architecture or multi-threading. We can write asynchronous code and leverage the full power of asyncio's event loop. Cool." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## aiohttp views\n", "\n", "aiohttp also allows one to add views to routes\n", "using decorators or explicitly i.e.\n", "\n", "```python3\n", "from aiohttp import web\n", "\n", "app = web.Application()\n", "routes = web.RouteTableDef()\n", "\n", "@routes.get('/get')\n", "async def index(request):\n", " ...\n", " \n", "#or\n", "\n", "app.router.add_route('GET', '/', index)\n", "\n", "# or\n", "\n", "app.router.add_get('/', index)\n", "\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-29T17:02:27.793821Z", "start_time": "2018-01-29T17:02:27.751818Z" }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "template = \"\"\"\n", "# Welcome to the example page\n", "\n", "## Rest routes\n", "\n", "### Authors\n", "\n", "For an individual author:\n", "\n", "[/rest/author/{{id}}](/rest/author/1)\n", "\n", "To query authors:\n", "\n", "[/rest/authors?age={{number}}&limit={{another_number}}](/rest/authors?limit=3)\n", "\n", "The following can be passed as query parameters to authors:\n", "\n", " limit: The amount of results you wish to be limited to\n", " age: The age of the author, as an integer\n", " first_name: The first name of the author as a string\n", " last_name: The last name of the author as a string\n", " no_books: Removes the {{books}} field from the resulting authors\n", "\n", "### Books\n", "\n", "For an individual book:\n", "\n", "[/rest/book/{{id}}](/rest/book/1)\n", "\n", "To query books:\n", "\n", "[/rest/books?author_id={{number}}&limit={{another_number}}](/rest/books?limit=3)\n", "\n", "The following can be passed as query parameters to books:\n", "\n", " limit: The amount of results you wish to be limiited to\n", " title: The title of the book\n", " published: The date published in the following format %m/%d/%Y\n", " author_id: The uuid of the author in the database\n", " \n", " \n", "## [GraphQL](/graphql)\n", "\n", "\"\"\"\n", "\n", "html = markdown.markdown(template)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.004550Z", "start_time": "2018-01-28T22:27:57.974739Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "#@routes.get('/')\n", "async def index(request):\n", " # this logging sexiness is a talk for another time\n", " # but it's a thin wrapper around eliot.start_action\n", " with log_request(request):\n", "\n", " response = web.Response(\n", " text=html,\n", " content_type='Content-Type: text/html'\n", " )\n", " \n", " with log_response(response):\n", " return response" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Rest views\n", "\n", "These define the logic for the restful routes we'll create on our application i.e.\n", "\n", "For a single resource:\n", "\n", "`/rest/author/{id}`\n", "\n", "`/rest/book/{id}`\n", "\n", "Or based on url query parameters:\n", "\n", "`/rest/authors?age=42&no_books=true`\n", "\n", "`/rest/books?author_id=3&limit=5`" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.100355Z", "start_time": "2018-01-28T22:27:58.007737Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "async def author(request):\n", " \"\"\"Return a single author for a given id.\"\"\"\n", "\n", " connection = request.app['connection']\n", "\n", " with log_request(request):\n", " try:\n", " \n", " # when using co-routines, it's important that each co-routine be non-blocking\n", " # meaning no individual action will take a long amount of time, preventing\n", " # the event loop from letting other co-routines execute\n", " \n", " # since our database query may not immediately return, we run it\n", " # in an \"executor\" (a thread pool) and await for it to finish\n", " \n", " # functools.partial allows us to create a callable that\n", " # bundles necessary positional and keyword arguments with it\n", " # in a way that is pickle-able https://docs.python.org/3/library/pickle.html\n", " \n", " # i.e. `print('hello', end=' ') == partial(print, 'hello', end= ' ')()`\n", " \n", " db_query = partial(\n", " fetch_authors, connection, id=int(request.match_info['id']))\n", " \n", " # * unpacks an arbitrary number of tuples (see pep-3132)\n", "\n", " author, *_ = await request.loop.run_in_executor(None, db_query)\n", "\n", " except ValueError:\n", " author = None\n", "\n", " if not author:\n", " log_message('Author not found', id=request.match_info['id'])\n", " raise web.HTTPNotFound\n", "\n", " response = web.json_response(author)\n", "\n", " with log_response(response):\n", " return response" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.100355Z", "start_time": "2018-01-28T22:27:58.007737Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "async def book(request):\n", " \"\"\"Return a single book for a given id.\"\"\"\n", "\n", " connection = request.app['connection']\n", "\n", " with log_request(request):\n", " try:\n", "\n", " db_query = partial(\n", " fetch_books, connection, id=int(request.match_info['id']))\n", "\n", " book, *_ = await request.loop.run_in_executor(None, db_query)\n", "\n", " except ValueError:\n", " book = None\n", "\n", " if not book:\n", " log_message('Book not found', id=request.match_info['id'])\n", " raise web.HTTPNotFound\n", "\n", " response = web.json_response(book)\n", "\n", " with log_response(response):\n", " return response" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.217413Z", "start_time": "2018-01-28T22:27:58.102774Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "async def books(request):\n", " \"\"\"Return json response of books based on query params.\"\"\"\n", " connection = request.app['connection']\n", "\n", " with log_request(request):\n", "\n", " # parse values from query params\n", " \n", " title = request.query.get('title')\n", " published = request.query.get('published')\n", " author_id = request.query.get('author_id')\n", " limit = int(request.query.get('limit', 0))\n", "\n", " query_db = partial(fetch_books,\n", " request.app['connection'],\n", " title=title,\n", " published=published,\n", " author_id=author_id,\n", " limit=limit)\n", " \n", " query_db_task = request.loop.run_in_executor(None, query_db)\n", " \n", " books: T.List[dict] = await query_db_task\n", "\n", " response = web.json_response(books)\n", "\n", " with log_response(response):\n", "\n", " return response" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.217413Z", "start_time": "2018-01-28T22:27:58.102774Z" }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "async def authors(request):\n", " \"\"\"Return json response of authors based on query params.\"\"\"\n", " connection = request.app['connection']\n", "\n", " with log_request(request):\n", "\n", " # parse values from query params\n", "\n", " first_name = request.query.get('first_name')\n", " last_name = request.query.get('last_name')\n", " age = None or int(request.query.get('age', 0))\n", " limit = int(request.query.get('limit', 0))\n", " \n", " # client may not want/need book information\n", " no_books = str(request.query.get('no_books','')).lower().startswith('t')\n", "\n", " query_db = partial(\n", " fetch_authors,\n", " request.app['connection'],\n", " first_name=first_name,\n", " last_name=last_name,\n", " age=age,\n", " limit=limit,\n", " no_books=no_books\n", " )\n", " \n", " query_db_task = request.loop.run_in_executor(None, query_db)\n", " \n", " authors: T.List[dict] = await query_db_task\n", "\n", " response = web.json_response(authors)\n", "\n", " with log_response(response):\n", "\n", " return response" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# GraphQL\n", "\n", "So before, we saw an example of how you might implement a REST api where each resource i.e. \n", "\n", " /rest/author/{id}\n", " /rest/authors? ...\n", " \n", " \n", "GraphQL is conceptually very different. The idea is that instead mapping your URL's to your data\n", "and implenting queries and mutations through a combination of URL query params and HTTP verbs, your\n", "entire API is exposed via a single URL.\n", "\n", "Requests to this url will have a GraphQL query either in the body of the HTTP request, or in the `query`\n", "url parameter. This GraphQL query will tell the server what data to fetch or change.\n", "\n", "The GraphQL server, in turn, exposes the data as a vertex-edge or node-link graph where the root node\n", "has a special name, `query` which is left out of graphql queries as its' implicitly there, in the\n", "same way `https://google.com` implicitly has the root DNS node contained `https://google.com.`\n", "\n", "Each resource we create, such as an `author` (objects in GraphQL parlance) will have an associated resolver function that exposes that object on the graph.\n", "\n", "So concretely, what does this look like in-practice? \n", "\n", "Well, on the client side hitting `http://localhost:8080/rest/authors?limit=3&no_books=true` may return something like\n", "\n", " [\n", " {\n", " \"id\": 1,\n", " \"first_name\": \"Willene\",\n", " \"last_name\": \"Whitaker\",\n", " \"age\": 26\n", " },\n", " {\n", " \"id\": 2,\n", " \"first_name\": \"Cedric\",\n", " \"last_name\": \"Williams\",\n", " \"age\": 52\n", " },\n", " {\n", " \"id\": 3,\n", " \"first_name\": \"Kaila\",\n", " \"last_name\": \"Snider\",\n", " \"age\": 33\n", " }\n", " ]\n", " \n", "\n", "If we wanted to get the same information using graphql, we'd start by writing our query.\n", "\n", " {\n", " authors(limit: 3) {\n", " id\n", " first_name\n", " last_name\n", " age\n", " }\n", " }\n", " \n", "In a scenario where we're using the excellent [httpie](https://httpie.org/) client and we have the above query bound to the `query` variable, we'd retreive the information as such:\n", "\n", " http :8080/graphql \"query=$query\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Writing GraphQL server code\n", "\n", "Since we're using [graphene](http://graphene-python.org/), we'll first need to use the library to create a schema describing our data.\n", "\n", "This schema will then tell the library how to implement the GraphQL-compliant endpoint view." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "## The Schema" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.590041Z", "start_time": "2018-01-28T22:27:58.220003Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "import graphene as g\n", "from graphql.execution.executors.asyncio import AsyncioExecutor\n", "\n", "\n", "\n", "class Author(g.ObjectType):\n", " \"\"\"This is a human being.\"\"\"\n", " id = g.Int(description='The primary key in the database')\n", " first_name = g.String()\n", " last_name = g.String()\n", " age = g.Int()\n", " # we can't use g.List(Book)\n", " # directly since it's not\n", " # yet defined\n", " books = g.List(lambda: Book)\n", "\n", "\n", "class Book(g.ObjectType):\n", " \"\"\"A book, written by an author.\"\"\"\n", " id = g.Int(description='The primary key in the database')\n", " title = g.String(description='The title of the book')\n", " published = g.String(description='The date it was published')\n", " author = g.Field(Author)\n", " " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Describing the Graph\n", "\n", "Once we've defined our schema, we need to expose that information by adding our **objects** as **fields** on the graph\n", "and writing **resolver functions** for those fields that describe out to fetch the actual data we want.\n", "\n", "Remember that the root node of our graph is an object with the name `query`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:27:58.590041Z", "start_time": "2018-01-28T22:27:58.220003Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "async def configure_graphql(app):\n", " \"\"\"\n", " Since our resolvers depend on the app's db connection, this\n", " co-routine must execute after that part of the application\n", " is configured\n", " \"\"\"\n", " connection = app['connection']\n", "\n", "\n", " class Query(g.ObjectType):\n", "\n", " author = g.Field(Author)\n", " book = g.Field(Book)\n", "\n", " authors = g.List(\n", " Author,\n", "\n", " # the following will be passed as named\n", " # arguments to the resolver function.\n", " # Don't ask why; it took me forever to\n", " # figure it out. Despite its functionality,\n", " # graphene's documentation leaves a lot to be desired\n", "\n", " id=g.Int(),\n", " first_name=g.String(),\n", " last_name=g.String(),\n", " age=g.Int(),\n", " limit=g.Int(\n", " description='The amount of results you wish to be limited to'))\n", "\n", " books = g.List(\n", " Book,\n", " id=g.Int(),\n", " title=g.String(),\n", " published=g.String(),\n", " author_id=g.Int(\n", " description='The unique ID of the author in the database'),\n", " limit=g.Int(description='The amount of results you with to be limited to'))\n", "\n", " async def resolve_books(self,\n", " info,\n", " id=None,\n", " title=None,\n", " published=None,\n", " author_id=None,\n", " limit=None):\n", "\n", " query_db = partial(\n", " fetch_books,\n", " \n", " connection,\n", " id=id,\n", " title=title,\n", " published=published,\n", " author_id=author_id,\n", " limit=limit)\n", " \n", " fetched = await app.loop.run_in_executor(None, query_db)\n", "\n", " books = []\n", "\n", " for book_dict in fetched:\n", "\n", " author = Author(\n", " id=book_dict['author']['id'],\n", " first_name=book_dict['author']['first_name'],\n", " last_name=book_dict['author']['last_name'],\n", " age=book_dict['author']['age'])\n", "\n", " book = Book(\n", " id=book_dict['id'],\n", " title=book_dict['title'],\n", " published=book_dict['published'],\n", " author=author)\n", "\n", " books.append(book)\n", "\n", " return books\n", "\n", " async def resolve_authors(self,\n", " info,\n", " id=None,\n", " first_name=None,\n", " last_name=None,\n", " age=None,\n", " limit=None):\n", "\n", " query_db = partial(\n", " fetch_authors,\n", " \n", " connection,\n", " id=id,\n", " first_name=first_name,\n", " last_name=last_name,\n", " age=age,\n", " limit=limit)\n", " \n", " fetched = await app.loop.run_in_executor(None, query_db)\n", "\n", " authors = []\n", "\n", " for author_dict in fetched:\n", "\n", " books = [\n", " Book(id=b['id'], title=b['title'], published=b['published'])\n", " for b in author_dict['books']\n", " ]\n", "\n", " author = Author(\n", " id=author_dict['id'],\n", " first_name=author_dict['first_name'],\n", " last_name=author_dict['last_name'],\n", " age=author_dict['age'],\n", " books=books)\n", "\n", " authors.append(author)\n", "\n", " return authors\n", "\n", " \n", " schema = g.Schema(query=Query, auto_camelcase=False)\n", " \n", " # create the view\n", " \n", " executor = AsyncioExecutor(loop=app.loop)\n", " \n", " gql_view = GraphQLView(schema=schema,\n", " executor=executor,\n", " graphiql=True,\n", " enable_async=True\n", " )\n", " \n", " # attach the view to the app router\n", " \n", " app.router.add_route(\n", " '*',\n", " '/graphql',\n", " gql_view,\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2018-01-28T22:28:13.926737Z", "start_time": "2018-01-28T22:27:58.592665Z" }, "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "def app_factory(*args, db=':memory:', logfile='log.json', **config_params):\n", "\n", " # initialize app\n", " app = web.Application()\n", "\n", " # set top-level configuration\n", " app['config'] = {k: v \n", " for k, v in config_params.items()\n", " if v is not None}\n", " if db:\n", " app['config']['db'] = db\n", " if logfile:\n", " app['config']['logfile'] = logfile\n", "\n", " # startup\n", " app.on_startup.append(configure_logging)\n", " app.on_startup.append(configure_database)\n", " app.on_startup.append(create_tables)\n", " app.on_startup.append(seed_db)\n", " app.on_startup.append(configure_graphql)\n", "\n", " # example routes\n", " app.router.add_get('/', index)\n", " \n", " # rest routes\n", " app.router.add_get('/rest/author/{id}', author)\n", " app.router.add_get('/rest/authors', authors)\n", " app.router.add_get('/rest/book/{id}', book)\n", " app.router.add_get('/rest/books', books)\n", "\n", "\n", " # cleanup\n", " app.on_cleanup.append(drop_tables)\n", " app.on_cleanup.append(close_db)\n", "\n", " return app\n", "\n", "\n", "if __name__ == '__main__':\n", "\n", " app = app_factory()\n", "\n", " web.run_app(app, host='127.0.0.1', port=8080)\n" ] } ], "metadata": { "_draft": { "nbviewer_url": "https://gist.github.com/d2fccf0d3aaacce6eab10707181130f7" }, "celltoolbar": "Slideshow", "gist": { "data": { "description": "graphql_example/graphql_example.ipynb", "public": true }, "id": "d2fccf0d3aaacce6eab10707181130f7" }, "hide_input": false, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4" } }, "nbformat": 4, "nbformat_minor": 2 }