{ "info": { "author": "Michael Williamson", "author_email": "mike@zwobble.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6" ], "description": "High-performance library for implementing GraphQL APIs\n======================================================\n\nGraphLayer is a Python library for implementing high-performance GraphQL APIs.\nBy running resolve functions for each node in the request rather than each node in the response,\nthe overhead of asynchronous function calls is reduced.\nQueries can also be written directly to fetch batches directly to avoid the N+1 problem\nwithout intermediate layers such as DataLoader.\n\nWhy GraphLayer?\n---------------\n\nWhat causes GraphQL APIs to be slow?\nIn most implementations of GraphQL,\nresolve functions are run for each node in the response.\nThis can lead to poor performance for two main reasons.\nThe first is the N+1 problem.\nThe second is the overhead of calling a potentially asynchronous resolve function for every node in the response.\nAlthough the overhead per call is small,\nfor large datasets, the sum of this overhead can be the vast majority of the time to respond.\n\nGraphLayer suggests instead that resolve functions could be called according to the shape of the request rather than the response.\nThis avoids the N+1 problem without introducing additional complexity,\nsuch as batching requests in the manner of DataLoader and similar libraries,\nand allowing resolve functions to be written in a style that more naturally maps to data stores such as SQL databases.\nSecondly, although there's still the overhead of calling resolve functions,\nthis overhead is multipled by the number of the nodes in the request rather than the response:\nfor large datasets, this is a considerable saving.\n\nAs a concrete example, consider the query:\n\n::\n\n query {\n books {\n title\n author {\n name\n }\n }\n }\n\nA naive GraphQL implementation would issue one SQL query to get the list of all books,\nthen N queries to get the author of each book.\nUsing DataLoader, the N queries to get the author of each book would be combined into a single query.\nUsing GraphLayer, there would be a single query to get the authors without any batching.\n\nInstallation\n------------\n\n::\n\n pip install git+https://github.com/mwilliamson/python-graphlayer.git#egg=graphlayer[graphql]\n\nTutorial\n--------\n\nThis tutorial builds up a simple application using SQLAlchemy and GraphLayer.\nThe goal is to execute the following query:\n\n::\n\n query {\n books(genre: \"comedy\") {\n title\n author {\n name\n }\n }\n }\n\nThat is, get the list of all books in the comedy genre,\nwith the title and name of the author for each book.\n\nIn this tutorial, we'll build up the necessary code from scratch,\nusing only the core of GraphLayer, to give an understanding of how GraphLayer works.\nIn practice, there are a number of helper functions that make implementation much simpler.\nWe'll see how to write our example using those helpers at the end.\n\nEnvironment\n~~~~~~~~~~~\n\nYou'll need a Python environment with at least Python 3.5 installed,\nwith graphlayer, graphql, and SQLAlchemy installed.\nFor instance, on the command line:\n\n::\n\n python3 -m venv .venv\n . .venv/bin/activate\n pip install --upgrade pip setuptools wheel\n pip install git+https://github.com/mwilliamson/python-graphlayer.git#egg=graphlayer[graphql]\n pip install SQLAlchemy\n\nGetting started\n~~~~~~~~~~~~~~~\n\nLet's start with a simple query, getting the count of books:\n\n::\n\n query {\n bookCount\n }\n\nAll queries share the same root object,\nbut can ask for whatever fields they want.\nAs a first step, we'll define the schema of the root object.\nFor now, we'll define a single integer field called ``book_count``\n(note that casing is automatically converted between camel case and snake case):\n\n.. code-block:: python\n\n import graphlayer as g\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"book_count\", type=g.Int),\n ))\n\nWe'll also need to define how to resolve the book count by defining a resolver function.\nEach resolver function takes the graph and a query of a particular type,\nand returns the result of that query.\nThe decorator ``g.resolver()`` is used to mark which type of query a resolver is for.\nIn this case, we'll need create a resolver for the root type.\nFor now, we'll define a resolver that returns a fixed object,\nand prints out the query so we can a take a look at it.\n\n.. code-block:: python\n\n import graphlayer as g\n from graphlayer.graphql import execute\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"book_count\", type=g.Int),\n ))\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n print(\"query:\", query)\n return query.create_object({\n \"bookCount\": 3,\n })\n\n resolvers = (resolve_root, )\n graph_definition = g.define_graph(resolvers=resolvers)\n graph = graph_definition.create_graph({})\n\n execute(\n \"\"\"\n query {\n bookCount\n }\n \"\"\",\n graph=graph,\n query_type=Root,\n )\n\nRunning this will print out:\n\n::\n\n query: ObjectQuery(\n type=Root,\n field_queries=(\n FieldQuery(\n key=\"bookCount\",\n field=Root.fields.book_count,\n type_query=ScalarQuery(type=Int),\n args=(),\n ),\n ),\n )\n\nNote that the ``FieldQuery`` has a ``key`` attribute.\nSince the user can rename fields in the query,\nwe should use the key as passed in the field query.\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n field_query = query.field_queries[0]\n\n return query.create_object({\n field_query.key: 3,\n })\n\nAt the moment,\nsince only one field is defined on Root,\nwe can always assume that field is being requested.\nHowever, that often won't be the case.\nFor instance, we could add an author count to the root:\n\n\n.. code-block:: python\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"author_count\", type=g.Int),\n g.field(\"book_count\", type=g.Int),\n ))\n\nNow we'll need to check what field is being requested.\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n def resolve_field(field):\n if field == Root.fields.author_count:\n return 2\n elif field == Root.fields.book_count:\n return 3\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n field_query = query.field_queries[0]\n\n return query.create_object({\n field_query.key: resolve_field(field_query.field),\n })\n\nWhat's more, the user might request more than one field,\nso we should iterate through ``query.field_queries`` when generating the result.\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n def resolve_field(field):\n if field == Root.fields.author_count:\n return 2\n elif field == Root.fields.book_count:\n return 3\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return query.create_object(dict(\n (field_query.key, resolve_field(field_query.field))\n for field_query in query.field_queries\n ))\n\nIf we print the data from the execution result:\n\n\n.. code-block:: python\n\n print(\"result:\", execute(\n \"\"\"\n query {\n bookCount\n }\n \"\"\",\n graph=graph,\n query_type=Root,\n ).data)\n\nThen we should get the output:\n\n::\n\n result: {'bookCount': 3}\n\nAdding SQLAlchemy\n~~~~~~~~~~~~~~~~~\n\nSo far, we've returned hard-coded values.\nLet's add in a database using SQLAlchemy and an in-memory SQLite database.\nAt the start of our script we'll add some code to set up the database schema and add data:\n\n\n.. code-block:: python\n\n import sqlalchemy.ext.declarative\n import sqlalchemy.orm\n\n Base = sqlalchemy.ext.declarative.declarative_base()\n\n class AuthorRecord(Base):\n __tablename__ = \"author\"\n\n id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)\n name = sqlalchemy.Column(sqlalchemy.Unicode, nullable=False)\n\n class BookRecord(Base):\n __tablename__ = \"book\"\n\n id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)\n title = sqlalchemy.Column(sqlalchemy.Unicode, nullable=False)\n genre = sqlalchemy.Column(sqlalchemy.Unicode, nullable=False)\n author_id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey(AuthorRecord.id), nullable=False)\n\n engine = sqlalchemy.create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n\n session = sqlalchemy.orm.Session(engine)\n author_wodehouse = AuthorRecord(name=\"PG Wodehouse\")\n author_berni\u00e8res = AuthorRecord(name=\"Louis de Berni\u00e8res\")\n session.add_all((author_wodehouse, author_berni\u00e8res))\n session.flush()\n session.add(BookRecord(title=\"Leave It to Psmith\", genre=\"comedy\", author_id=author_wodehouse.id))\n session.add(BookRecord(title=\"Right Ho, Jeeves\", genre=\"comedy\", author_id=author_wodehouse.id))\n session.add(BookRecord(title=\"Captain Corelli's Mandolin\", genre=\"historical_fiction\", author_id=author_berni\u00e8res.id))\n session.flush()\n\nNext, we'll update our resolvers to use the database:\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n def resolve_field(field):\n if field == Root.fields.author_count:\n return session.query(AuthorRecord).count()\n elif field == Root.fields.book_count:\n return session.query(BookRecord).count()\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return query.create_object(dict(\n (field_query.key, resolve_field(field_query.field))\n for field_query in query.field_queries\n ))\n\nAdding books to the root\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nSo far, we've added two scalar fields to the root.\nLet's add in a ``books`` field, which should be a little more interesting.\nOur aim is to be able to run the query:\n\n::\n\n query {\n books {\n title\n }\n }\n\nWe start by creating a ``Book`` object type,\nand using it to define the ``books`` field on ``Root``:\n\n\n.. code-block:: python\n\n Book = g.ObjectType(\"Book\", fields=(\n g.field(\"title\", type=g.String),\n g.field(\"genre\", type=g.String),\n ))\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"author_count\", type=g.Int),\n g.field(\"book_count\", type=g.Int),\n g.field(\"books\", type=g.ListType(Book)),\n ))\n\nWe'll need to update the root resolver to handle the new field.\nAlthough we could handle the field directly in the root resolver,\nwe'll instead ask the graph to resolve the query for us.\nThis allows us to have a common way to resolve books,\nregardless of where they appear in the query.\n\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n def resolve_field(field_query):\n if field_query.field == Root.fields.author_count:\n return session.query(AuthorRecord).count()\n elif field_query.field == Root.fields.book_count:\n return session.query(BookRecord).count()\n elif field_query.field == Root.fields.books:\n return graph.resolve(field_query.type_query)\n else:\n raise Exception(\"unknown field: {}\".format(field_query.field))\n\n return query.create_object(dict(\n (field_query.key, resolve_field(field_query))\n for field_query in query.field_queries\n ))\n\nThis means we need to define a resolver for a list of books.\nFor now, let's just print the query and return an empty list so we can see what the query looks like.\n\n\n\n.. code-block:: python\n\n @g.resolver(g.ListType(Book))\n def resolve_books(graph, query):\n print(\"books query:\", query)\n return []\n\n resolvers = (resolve_root, resolve_books)\n\nIf update the query we pass to ``execute``:\n\n\n.. code-block:: python\n\n print(\"result:\", execute(\n \"\"\"\n query {\n books {\n title\n }\n }\n \"\"\",\n graph=graph,\n query_type=Root,\n ))\n\nThen our script should now produce the output:\n\n::\n\n books query: ListQuery(\n type=List(Book),\n element_query=ObjectQuery(\n type=Book,\n field_queries=(\n FieldQuery(\n key=\"title\",\n field=Book.fields.title,\n type_query=ScalarQuery(type=String),\n args=(),\n ),\n ),\n ),\n )\n result: {'books': []}\n\nSimilarly to the ``ObjectQuery`` we had when resolving the root object,\nwe have an ``ObjectQuery`` for ``Book``.\nSince a list is being requested, this is then wrapped in a ``ListQuery``,\nwith the object query being accessible through the ``element_query`` attribute.\n\nWe can write a resolver for a list of books by first fetching all of the books,\nand then mapping each fetched book to an object according to the fields requested in the query.\n\n\n.. code-block:: python\n\n @g.resolver(g.ListType(Book))\n def resolve_books(graph, query):\n books = session.query(BookRecord.title, BookRecord.genre).all()\n\n def resolve_field(book, field):\n if field == Book.fields.title:\n return book.title\n elif field == Book.fields.genre:\n return book.genre\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return [\n query.element_query.create_object(dict(\n (field_query.key, resolve_field(book, field_query.field))\n for field_query in query.element_query.field_queries\n ))\n for book in books\n ]\n\nRunning this code should give the output:\n\n::\n\n result: {'books': [{'title': 'Leave It to Psmith'}, {'title': 'Right Ho, Jeeves'}, {'title': \"Captain Corelli's Mandolin\"}]}\n\nWe can make the resolver more efficient by only fetching those columns required by the query.\nAlthough this makes comparatively little difference with the data we have at the moment,\nthis can help improve performance when there are many more fields the user can request,\nand with larger data sets.\n\n\n.. code-block:: python\n\n @g.resolver(g.ListType(Book))\n def resolve_books(graph, query):\n field_to_expression = {\n Book.fields.title: BookRecord.title,\n Book.fields.genre: BookRecord.genre,\n }\n\n expressions = frozenset(\n field_to_expression[field_query.field]\n for field_query in query.element_query.field_queries\n )\n\n books = session.query(*expressions).all()\n\n def resolve_field(book, field):\n if field == Book.fields.title:\n return book.title\n elif field == Book.fields.genre:\n return book.genre\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return [\n query.element_query.create_object(dict(\n (field_query.key, resolve_field(book, field_query.field))\n for field_query in query.element_query.field_queries\n ))\n for book in books\n ]\n\nAdding a genre parameter to the books field\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSo far, the ``books`` field returns all of the books in the database.\nLet's add an optional ``genre`` parameter, so we can run the following query:\n\n::\n\n query {\n books(genre: \"comedy\") {\n title\n }\n }\n\nBefore we start actually adding the parameter,\nwe need to make a change to how books are resolved.\nAt the moment, the code resolves queries for lists of books,\nwhich doesn't provide a convenient way for us to tell the resolver to only fetch a subset of books.\nTo solve this, we'll wrap the object query in our own custom query class.\n\n\n.. code-block:: python\n\n class BookQuery(object):\n def __init__(self, object_query):\n self.type = (BookQuery, object_query.type)\n self.object_query = object_query\n\nWe can then create a ``BookQuery`` in the root resolver:\n\n\n\n.. code-block:: python\n\n elif field_query.field == Root.fields.books:\n return graph.resolve(BookQuery(field_query.type_query.element_query))\n\nAnd we'll have to update ``resolve_books`` accordingly.\nSpecifically, we need to replace ``g.resolver(g.ListType(Book))`` with ``g.resolver((BookQuery, Book))``,\nand replace ``query.element_query`` with ``query.object_query``.\n\n\n.. code-block:: python\n\n @g.resolver((BookQuery, Book))\n def resolve_books(graph, query):\n field_to_expression = {\n Book.fields.title: BookRecord.title,\n Book.fields.genre: BookRecord.genre,\n }\n\n expressions = frozenset(\n field_to_expression[field_query.field]\n for field_query in query.object_query.field_queries\n )\n\n books = session.query(*expressions).all()\n\n def resolve_field(book, field):\n if field == Book.fields.title:\n return book.title\n elif field == Book.fields.genre:\n return book.genre\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return [\n query.object_query.create_object(dict(\n (field_query.key, resolve_field(book, field_query.field))\n for field_query in query.object_query.field_queries\n ))\n for book in books\n ]\n\nNow we can get on with actually adding the parameter.\nWe'll first need to update the definition of the ``books`` field on ``Root``:\n\n\n.. code-block:: python\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"author_count\", type=g.Int),\n g.field(\"book_count\", type=g.Int),\n g.field(\"books\", type=g.ListType(Book), params=(\n g.param(\"genre\", type=g.String, default=None),\n )),\n ))\n\nNext, we'll update ``BookQuery`` to support filtering by adding a ``where`` method:\n\n\n.. code-block:: python\n\n class BookQuery(object):\n def __init__(self, object_query, genre=None):\n self.type = (BookQuery, object_query.type)\n self.object_query = object_query\n self.genre = genre\n\n def where(self, *, genre):\n return BookQuery(self.object_query, genre=genre)\n\nWe can use this ``where`` method when resolving the ``books`` field in the root resolver.\n\n\n\n.. code-block:: python\n\n elif field_query.field == Root.fields.books:\n book_query = BookQuery(field_query.type_query.element_query)\n\n if field_query.args.genre is not None:\n book_query = book_query.where(genre=field_query.args.genre)\n\n return graph.resolve(book_query)\n\nFinally, we need to filter the books we fetch from the database.\nWe'll replace:\n\n.. code-block:: python\n\n books = session.query(*expressions).all()\n\nwith:\n\n\n.. code-block:: python\n\n sqlalchemy_query = session.query(*expressions)\n\n if query.genre is not None:\n sqlalchemy_query = sqlalchemy_query.filter(BookRecord.genre == query.genre)\n\n books = sqlalchemy_query.all()\n\nIf we update our script with the new query:\n\n\n.. code-block:: python\n\n print(\"result:\", execute(\n \"\"\"\n query {\n books(genre: \"comedy\") {\n title\n }\n }\n \"\"\",\n graph=graph,\n query_type=Root,\n ))\n\nWe should see only books in the comedy genre in the output:\n\n::\n\n result: {'books': [{'title': 'Leave It to Psmith'}, {'title': 'Right Ho, Jeeves'}]}\n\nAdding authors to the root\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSimilarly to the ``books`` field on the root,\nwe can add an ``authors`` field to the root.\nWe start by defining the ``Author`` object type,\nand adding the ``authors`` field to ``Root``.\n\n\n.. code-block:: python\n\n Author = g.ObjectType(\"Author\", fields=(\n g.field(\"name\", type=g.String),\n ))\n\n Root = g.ObjectType(\"Root\", fields=(\n g.field(\"author_count\", type=g.Int),\n g.field(\"authors\", type=g.ListType(Author)),\n\n g.field(\"book_count\", type=g.Int),\n g.field(\"books\", type=g.ListType(Book), params=(\n g.param(\"genre\", type=g.String, default=None),\n )),\n ))\n\nWe define an ``AuthorQuery``,\nwhich can be resolved by a new resolver.\n\n\n\n.. code-block:: python\n\n class AuthorQuery(object):\n def __init__(self, object_query):\n self.type = (AuthorQuery, object_query.type)\n self.object_query = object_query\n\n @g.resolver((AuthorQuery, Author))\n def resolve_authors(graph, query):\n authors = session.query(AuthorRecord.name).all()\n\n def resolve_field(author, field):\n if field == Author.fields.name:\n return author.name\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n return [\n query.object_query.create_object(dict(\n (field_query.key, resolve_field(author, field_query.field))\n for field_query in query.object_query.field_queries\n ))\n for author in authors\n ]\n\n resolvers = (resolve_root, resolve_authors, resolve_books)\n\nFinally, we update the root resolver to resolve the ``authors`` field.\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n def resolve_root(graph, query):\n def resolve_field(field_query):\n if field_query.field == Root.fields.author_count:\n return session.query(AuthorRecord).count()\n elif field_query.field == Root.fields.authors:\n return graph.resolve(AuthorQuery(field_query.type_query.element_query))\n elif field_query.field == Root.fields.book_count:\n return session.query(BookRecord).count()\n\nAdding an author field to books\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs the last change to the schema,\nlet's add an ``author`` field to ``Book``.\nWe start by updating the type:\n\n\n.. code-block:: python\n\n Book = g.ObjectType(\"Book\", fields=(\n g.field(\"title\", type=g.String),\n g.field(\"genre\", type=g.String),\n g.field(\"author\", type=Author),\n ))\n\nWe then need to update the resolver for books.\nIf the ``author`` field is requested,\nthen we'll need to fetch the ``author_id`` from the database,\nso we update ``field_to_expression``:\n\n\n.. code-block:: python\n\n field_to_expression = {\n Book.fields.title: BookRecord.title,\n Book.fields.genre: BookRecord.genre,\n Book.fields.author: BookRecord.author_id,\n }\n\nAs well as fetching books,\nwe'll need to fetch the authors too.\nWe can do this by delegating to the graph.\nWhen fetching authors for the root, having them returned as a list was the most convenient format.\nHowever, when fetching authors for books,\nit'd be more convenient to return them in a dictionary keyed by ID so they can easily matched to books by ``author_id``.\nWe can change the ``AuthorQuery`` to optionally allow this alternative format:\n\n\n.. code-block:: python\n\n class AuthorQuery(object):\n def __init__(self, object_query, is_keyed_by_id=False):\n self.type = (AuthorQuery, object_query.type)\n self.object_query = object_query\n self.is_keyed_by_id = is_keyed_by_id\n\n def key_by_id(self):\n return AuthorQuery(self.object_query, is_keyed_by_id=True)\n\nWe then need to update the resolver to handle this:\n\n\n.. code-block:: python\n\n @g.resolver((AuthorQuery, Author))\n def resolve_authors(graph, query):\n sqlalchemy_query = session.query(AuthorRecord.name)\n\n if query.is_keyed_by_id:\n sqlalchemy_query = sqlalchemy_query.add_columns(AuthorRecord.id)\n\n authors = sqlalchemy_query.all()\n\n def resolve_field(author, field):\n if field == Author.fields.name:\n return author.name\n else:\n raise Exception(\"unknown field: {}\".format(field))\n\n def to_object(author):\n return query.object_query.create_object(dict(\n (field_query.key, resolve_field(author, field_query.field))\n for field_query in query.object_query.field_queries\n ))\n\n if query.is_keyed_by_id:\n return dict(\n (author.id, to_object(author))\n for author in authors\n )\n else:\n return [\n to_object(author)\n for author in authors\n ]\n\nNow we can update the books resolver to fetch the authors using the graph:\n\n\n.. code-block:: python\n\n books = sqlalchemy_query.all()\n\n authors = dict(\n (field_query.key, graph.resolve(AuthorQuery(field_query.type_query).key_by_id()))\n for field_query in query.object_query.field_queries\n if field_query.field == Book.fields.author\n )\n\nThis creates a dictionary mapping from each field query to the authors fetched for that field query.\nWe can this use this dictionary when resolving each field:\n\n\n.. code-block:: python\n\n def resolve_field(book, field_query):\n if field_query.field == Book.fields.title:\n return book.title\n elif field_query.field == Book.fields.genre:\n return book.genre\n elif field_query.field == Book.fields.author:\n return authors[field_query.key][book.author_id]\n else:\n raise Exception(\"unknown field: {}\".format(field_query.field))\n\n return [\n query.object_query.create_object(dict(\n (field_query.key, resolve_field(book, field_query))\n for field_query in query.object_query.field_queries\n ))\n for book in books\n ]\n\nNow if we update our executed query:\n\n\n.. code-block:: python\n\n print(\"result:\", execute(\n \"\"\"\n query {\n books(genre: \"comedy\") {\n title\n author {\n name\n }\n }\n }\n \"\"\",\n graph=graph,\n query_type=Root,\n ))\n\nWe should see:\n\n::\n\n result: {'books': [{'title': 'Leave It to Psmith', 'author': {'name': 'PG Wodehouse'}}, {'title': 'Right Ho, Jeeves', 'author': {'name': 'PG Wodehouse'}}]}\n\nOne inefficiency in the current implementation is that we fetch all authors,\nregardless of whether they're the author of a book that we've fetched.\nWe can fix this by filtering the author query by IDs,\nsimilarly to how we filtered the book query by genre.\nWe update ``AuthorQuery`` to add in an ``ids`` attribute:\n\n\n.. code-block:: python\n\n class AuthorQuery(object):\n def __init__(self, object_query, ids=None, is_keyed_by_id=False):\n self.type = (AuthorQuery, object_query.type)\n self.object_query = object_query\n self.ids = ids\n self.is_keyed_by_id = is_keyed_by_id\n\n def key_by_id(self):\n return AuthorQuery(self.object_query, ids=self.ids, is_keyed_by_id=True)\n\n def where(self, *, ids):\n return AuthorQuery(self.object_query, ids=ids, is_keyed_by_id=self.is_keyed_by_id)\n\nWe use that ``ids`` attribute in the author resolver:\n\n\n.. code-block:: python\n\n sqlalchemy_query = session.query(AuthorRecord.name)\n\n if query.ids is not None:\n sqlalchemy_query = sqlalchemy_query.filter(AuthorRecord.id.in_(query.ids))\n\n if query.is_keyed_by_id:\n sqlalchemy_query = sqlalchemy_query.add_columns(AuthorRecord.id)\n\n authors = sqlalchemy_query.all()\n\nAnd we set the IDs in the book resolver:\n\n\n.. code-block:: python\n\n books = sqlalchemy_query.all()\n\n def get_author_ids():\n return frozenset(\n book.author_id\n for book in books\n )\n\n def get_authors_for_field_query(field_query):\n author_query = AuthorQuery(field_query.type_query) \\\n .where(ids=get_author_ids()) \\\n .key_by_id()\n return graph.resolve(author_query)\n\n authors = dict(\n (field_query.key, get_authors_for_field_query(field_query))\n for field_query in query.object_query.field_queries\n if field_query.field == Book.fields.author\n )\n\nDependency injection\n~~~~~~~~~~~~~~~~~~~~\n\nIn our example so far,\nwe've treated the SQLAlchemy session as a global variable.\nIn practice, it's sometimes useful to pass the session (and other dependencies) around explicitly.\nDependencies for resolvers are marked using the decorator ``g.dependencies``,\nwhich allow dependencies to be passed as keyword arguments to resolvers.\nFor instance, to add a dependency on a SQLAlchemy session to ``resolve_root``:\n\n\n.. code-block:: python\n\n @g.resolver(Root)\n @g.dependencies(session=sqlalchemy.orm.Session)\n def resolve_root(graph, query, *, session):\n\nA dependency can be identified by any value.\nIn this case, we identify the session dependency by its class, ``sqlalchemy.orm.Session``.\nWhen creating the graph,\nwe need to pass in dependencies:\n\n\n.. code-block:: python\n\n graph = graph_definition.create_graph({\n sqlalchemy.orm.Session: session,\n })\n\n\nExtracting duplication\n~~~~~~~~~~~~~~~~~~~~~~\n\nWhen implementing resolvers, there are common patterns that tend to occur.\nBy extracting these common patterns into functions that build resolvers,\nwe can reduce duplication and simplify the definition of resolvers.\nFor instance, our root resolver can be rewritten as:\n\n\n.. code-block:: python\n\n resolve_root = g.root_object_resolver(Root)\n\n @resolve_root.field(Root.fields.author_count)\n @g.dependencies(session=sqlalchemy.orm.Session)\n def root_resolve_author_count(graph, query, args, *, session):\n return session.query(AuthorRecord).count()\n\n @resolve_root.field(Root.fields.authors)\n def root_resolve_authors(graph, query, args):\n return graph.resolve(AuthorQuery(query.element_query))\n\n @resolve_root.field(Root.fields.book_count)\n @g.dependencies(session=sqlalchemy.orm.Session)\n def root_resolve_book_count(graph, query, args, *, session):\n return session.query(BookRecord).count()\n\n @resolve_root.field(Root.fields.books)\n def root_resolve_books(graph, query, args):\n book_query = BookQuery(query.element_query)\n\n if args.genre is not None:\n book_query = book_query.where(genre=args.genre)\n\n return graph.resolve(book_query)\n\nSimilarly, we can use the ``graphlayer.sqlalchemy`` module to define the resolvers for authors and books:\n\n\n.. code-block:: python\n\n import graphlayer.sqlalchemy as gsql\n\n @resolve_root.field(Root.fields.authors)\n def root_resolve_authors(graph, query, args):\n return graph.resolve(gsql.select(query))\n\n @resolve_root.field(Root.fields.books)\n def root_resolve_books(graph, query, args):\n book_query = gsql.select(query)\n\n if args.genre is not None:\n book_query = book_query.where(BookRecord.genre == args.genre)\n\n return graph.resolve(book_query)\n\n resolve_authors = gsql.sql_table_resolver(\n Author,\n AuthorRecord,\n fields={\n Author.fields.name: gsql.expression(AuthorRecord.name),\n },\n )\n\n resolve_books = gsql.sql_table_resolver(\n Book,\n BookRecord,\n fields={\n Book.fields.title: gsql.expression(BookRecord.title),\n Book.fields.genre: gsql.expression(BookRecord.genre),\n Book.fields.author: lambda graph, field_query: gsql.join(\n key=BookRecord.author_id,\n resolve=lambda author_ids: graph.resolve(\n gsql.select(field_query.type_query).by(AuthorRecord.id, author_ids),\n ),\n ),\n },\n )\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/mwilliamson/python-graphlayer", "keywords": "graphql graph join", "license": "", "maintainer": "", "maintainer_email": "", "name": "graphlayer", "package_url": "https://pypi.org/project/graphlayer/", "platform": "", "project_url": "https://pypi.org/project/graphlayer/", "project_urls": { "Homepage": "http://github.com/mwilliamson/python-graphlayer" }, "release_url": "https://pypi.org/project/graphlayer/0.2.8/", "requires_dist": null, "requires_python": "", "summary": "High-performance library for implementing GraphQL APIs", "version": "0.2.8" }, "last_serial": 5995021, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "d32cdb66fecf552076e0ab1177a90260", "sha256": "f66ce36de26bdd0c9b03fa0d7a6930e1757d1d1531577a2e68d7332d0d5a9782" }, "downloads": -1, "filename": "graphlayer-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "d32cdb66fecf552076e0ab1177a90260", "packagetype": "bdist_wheel", "python_version": "3.5", "requires_python": null, "size": 21575, "upload_time": "2019-01-31T17:09:21", "url": "https://files.pythonhosted.org/packages/a4/07/6625127358ba9587d3d5f1910ff995739a63f761dc7b589dd635c4722dee/graphlayer-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3545e3f8c2d953de88ec33ef0f16ada1", "sha256": "3364a54c941e167fec373092ed712f95136bf0bfaa2b6e2565387bb55b99a26b" }, "downloads": -1, "filename": "graphlayer-0.1.0.tar.gz", "has_sig": false, "md5_digest": "3545e3f8c2d953de88ec33ef0f16ada1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30433, "upload_time": "2019-01-31T17:09:18", "url": "https://files.pythonhosted.org/packages/bd/14/c3b3cec13cf79d9a5f78a410e4072c55575723d14fe435e3458c02aef276/graphlayer-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "40cdb8a62d255e31573e9d63c30bcc05", "sha256": "df078421a6d31f19f1d3095b5d6aff9a21fdf11c81ad26dced4fecf759cd0025" }, "downloads": -1, "filename": "graphlayer-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "40cdb8a62d255e31573e9d63c30bcc05", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 21635, "upload_time": "2019-02-05T20:30:51", "url": "https://files.pythonhosted.org/packages/72/43/9a903abad7fdfd7bd1272fd09b5446d660fe0ce0029e804b29228bcf6b74/graphlayer-0.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c72ddad1c165da54735577a3339198e5", "sha256": "2b7125bad0098c247499a5cda4fc6fe02808622bf7113176d4aba507daba31bc" }, "downloads": -1, "filename": "graphlayer-0.1.1.tar.gz", "has_sig": false, "md5_digest": "c72ddad1c165da54735577a3339198e5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31365, "upload_time": "2019-02-05T20:30:49", "url": "https://files.pythonhosted.org/packages/5f/ff/973dbfb2e9602a340db350a257ee1a0579b57e28bb236e0228d16dbf8671/graphlayer-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "b5d686ade3481349364c9937d6e3111f", "sha256": "ca37b0fdc8bd43cccaa6281f06b555e3aaf2ddc47ba29a982c9995bbb88fb41c" }, "downloads": -1, "filename": "graphlayer-0.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "b5d686ade3481349364c9937d6e3111f", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 21679, "upload_time": "2019-02-15T09:30:18", "url": "https://files.pythonhosted.org/packages/f3/10/b395aa7ea805f572013939bd61f0b4930e41c9d987ec28db735bfc8e3628/graphlayer-0.1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "49cf62266f0bbec992aa404bed0eecb1", "sha256": "d78544c0a7a6a2c7405f679e8d4cc08926b94f46e4a95597d0dd699b4c136643" }, "downloads": -1, "filename": "graphlayer-0.1.2.tar.gz", "has_sig": false, "md5_digest": "49cf62266f0bbec992aa404bed0eecb1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31414, "upload_time": "2019-02-15T09:30:15", "url": "https://files.pythonhosted.org/packages/e5/bc/c3e0b80c41c7d95794e3c42bc827dd9e4f92372b813d0d28ddbd5a7cb3e9/graphlayer-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "bf1c048ff3be9165c3b87fdbf7c87531", "sha256": "328f44afd91ff1a90fbafa79e739e8b99a4395c1ea031e0577c51d3e63831bc5" }, "downloads": -1, "filename": "graphlayer-0.1.3-py3-none-any.whl", "has_sig": false, "md5_digest": "bf1c048ff3be9165c3b87fdbf7c87531", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 21732, "upload_time": "2019-02-18T18:10:39", "url": "https://files.pythonhosted.org/packages/19/4c/d4025d392d261578aff3e8d117cb63817878a0551862b77f1e30f8a1d9e7/graphlayer-0.1.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "84bf8065b5a244e15f3a1496bb53895f", "sha256": "e1329680ca16e397d90a6e64292cf2285c4455fb6eea4a13f73f5b0382f12add" }, "downloads": -1, "filename": "graphlayer-0.1.3.tar.gz", "has_sig": false, "md5_digest": "84bf8065b5a244e15f3a1496bb53895f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31456, "upload_time": "2019-02-18T18:10:36", "url": "https://files.pythonhosted.org/packages/91/1c/683a9a51bac0db643e43506fb6b443fb56fea13af57596c41212c31cb970/graphlayer-0.1.3.tar.gz" } ], "0.1.4": [ { "comment_text": "", "digests": { "md5": "778c60c7680f32e6e3ffea5b301e32da", "sha256": "b04f287cef1317c4dc10cd90404e0776856dbf6fce26c7f05981b32a3fc5edae" }, "downloads": -1, "filename": "graphlayer-0.1.4-py3-none-any.whl", "has_sig": false, "md5_digest": "778c60c7680f32e6e3ffea5b301e32da", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 21894, "upload_time": "2019-02-19T18:57:44", "url": "https://files.pythonhosted.org/packages/31/97/a0fd77e267d656dd4ff3f38166b46d30228cc30fc4b6bf3d24a667eab373/graphlayer-0.1.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "23039a2ec77955bb0c31a2056df76b4b", "sha256": "87e1c3841d6ec480e2c0b81c46a350b78f2616108160a50cce1c190dcec5d453" }, "downloads": -1, "filename": "graphlayer-0.1.4.tar.gz", "has_sig": false, "md5_digest": "23039a2ec77955bb0c31a2056df76b4b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31623, "upload_time": "2019-02-19T18:57:42", "url": "https://files.pythonhosted.org/packages/e8/de/7e9d74c32f7e4d991c56aa278e47d8ecd23eb3bd97cffbd6b17f11b27b3b/graphlayer-0.1.4.tar.gz" } ], "0.1.5": [ { "comment_text": "", "digests": { "md5": "281f928bbeb57c0f362fb5eb42f5e1fb", "sha256": "5aae1118966eda99ec8e4ce6a2fd3b829c3636485ce2fc39bbe7a153e7d6c827" }, "downloads": -1, "filename": "graphlayer-0.1.5-py3-none-any.whl", "has_sig": false, "md5_digest": "281f928bbeb57c0f362fb5eb42f5e1fb", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 21954, "upload_time": "2019-03-10T14:46:38", "url": "https://files.pythonhosted.org/packages/6b/c5/41385afab2f70c81cb33d583cbd2358a8eab8ef97b7485898e6ee6bcfb68/graphlayer-0.1.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3179f41d0122fc6a306dc578284de49b", "sha256": "2969293f6f15b39a9cb21b3eb3e0ed17e71652863f52f61f0b39241265c209f7" }, "downloads": -1, "filename": "graphlayer-0.1.5.tar.gz", "has_sig": false, "md5_digest": "3179f41d0122fc6a306dc578284de49b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31570, "upload_time": "2019-03-10T14:46:36", "url": "https://files.pythonhosted.org/packages/61/0b/ac6ab49ff1471bfe8fe2082c5240cf98d76e92c374764886f9478169b697/graphlayer-0.1.5.tar.gz" } ], "0.1.6": [ { "comment_text": "", "digests": { "md5": "0c8d66ed5f1eaa224ccf7637d6e8833e", "sha256": "550f87be0fe1641d1d952480844db2cf53fe1198a19afe1b23599f3bd494de59" }, "downloads": -1, "filename": "graphlayer-0.1.6-py3-none-any.whl", "has_sig": false, "md5_digest": "0c8d66ed5f1eaa224ccf7637d6e8833e", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 23034, "upload_time": "2019-04-17T18:19:33", "url": "https://files.pythonhosted.org/packages/68/2e/95ba53276311cfab41a2f3b532ecf5284e8df07429784c51b82a55356abb/graphlayer-0.1.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "7dd597cff77e796cec06c24e230f97b4", "sha256": "92ce6aec8b867f8c76616cf7edace25ca3117b4a05971072a39cf8ef420d0e72" }, "downloads": -1, "filename": "graphlayer-0.1.6.tar.gz", "has_sig": false, "md5_digest": "7dd597cff77e796cec06c24e230f97b4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31584, "upload_time": "2019-04-17T18:19:31", "url": "https://files.pythonhosted.org/packages/cb/7d/494e8a2d8695a584dc3728cf96fc082f380d902f645da166af552ad0b6b9/graphlayer-0.1.6.tar.gz" } ], "0.1.7": [ { "comment_text": "", "digests": { "md5": "c415c2577aac49eb5ac3c2bae76897db", "sha256": "cbb0e318dc045113d0d50313f575f6333c1a7744ed819f6dd4da28e62d2e2f62" }, "downloads": -1, "filename": "graphlayer-0.1.7-py3-none-any.whl", "has_sig": false, "md5_digest": "c415c2577aac49eb5ac3c2bae76897db", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 23537, "upload_time": "2019-05-31T10:06:21", "url": "https://files.pythonhosted.org/packages/d3/56/745932862bc8953f630036b565fa24d10a582a48ddccd82e278765899eb4/graphlayer-0.1.7-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3eaa80926248765dfac63e20877b69f7", "sha256": "b3b6211ebeed4dba1605a77d7730103552917d070ac5196aa1a11d32d9cca416" }, "downloads": -1, "filename": "graphlayer-0.1.7.tar.gz", "has_sig": false, "md5_digest": "3eaa80926248765dfac63e20877b69f7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31879, "upload_time": "2019-05-31T10:06:18", "url": "https://files.pythonhosted.org/packages/23/58/ae4f204141204877ea4a2f80e8e980caab772eedbf2d3404b88a80222378/graphlayer-0.1.7.tar.gz" } ], "0.1.8": [ { "comment_text": "", "digests": { "md5": "f67521ed5a54004324d1fc719d15ba5f", "sha256": "d38d817c30f3a3ac9d908ccd72fa04bb5cda266240e72bc22433a095d9de8a62" }, "downloads": -1, "filename": "graphlayer-0.1.8-py3-none-any.whl", "has_sig": false, "md5_digest": "f67521ed5a54004324d1fc719d15ba5f", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 24269, "upload_time": "2019-06-03T19:52:29", "url": "https://files.pythonhosted.org/packages/a8/f3/ad880f652f6c2c56c565710c1601221a681d19a0bd745044175e0e390bba/graphlayer-0.1.8-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3caf210e59a12e714a5979401e3f2d8f", "sha256": "153990ada227386bc38e27ba34427da3bbf1e0d61a8f953d488cdf97a6697e19" }, "downloads": -1, "filename": "graphlayer-0.1.8.tar.gz", "has_sig": false, "md5_digest": "3caf210e59a12e714a5979401e3f2d8f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32525, "upload_time": "2019-06-03T19:52:26", "url": "https://files.pythonhosted.org/packages/dd/db/9a083563666024678415dd25c0e67bc0d60ac56e0c4e6f7bdd7b57a50ded/graphlayer-0.1.8.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "529e4288d7864527ed6ab04fc6b3e1a9", "sha256": "6dafdae44033dcf9d9b6c4d94a54b6bfe2ecd81195eb34f398f56b32f64b381a" }, "downloads": -1, "filename": "graphlayer-0.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "529e4288d7864527ed6ab04fc6b3e1a9", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 24410, "upload_time": "2019-06-10T19:17:28", "url": "https://files.pythonhosted.org/packages/3d/61/86203e85747227a7984d4544f10ec4c833f020b3de4eb4a66b8483646141/graphlayer-0.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "84f474bd6e44aea6ac0f42689f806949", "sha256": "9ecb40e9920d904dff321a7596d9fd95c61f5d671fa05cb3467a44a13e82fefd" }, "downloads": -1, "filename": "graphlayer-0.2.0.tar.gz", "has_sig": false, "md5_digest": "84f474bd6e44aea6ac0f42689f806949", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32627, "upload_time": "2019-06-10T19:17:24", "url": "https://files.pythonhosted.org/packages/bf/09/080bb81e043cd7910202efb755786245f6c5949c94c0392214e96b1e037a/graphlayer-0.2.0.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "4f509ee57b29679af06e5e4a32d53146", "sha256": "82cc544e4e2bd3f6b18f3380948885c3619b27d92d1aeda4fce87b22919964b3" }, "downloads": -1, "filename": "graphlayer-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "4f509ee57b29679af06e5e4a32d53146", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 24413, "upload_time": "2019-06-15T13:59:39", "url": "https://files.pythonhosted.org/packages/51/71/1ea360d4b592e9ccceb1d566e86e12d0ed2cee1b4ed8413b77c19ce20ee3/graphlayer-0.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3ae61e52714371b8ff8581c218b7fc67", "sha256": "b32a93821e2039f304812c6b223256527fd7081f566f7af2c60563ce89c9d86a" }, "downloads": -1, "filename": "graphlayer-0.2.1.tar.gz", "has_sig": false, "md5_digest": "3ae61e52714371b8ff8581c218b7fc67", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32641, "upload_time": "2019-06-15T13:59:36", "url": "https://files.pythonhosted.org/packages/02/17/9883d98306d996c59260a57d1bd91a17d263a27d2c2eb682f244e405fc08/graphlayer-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "d2780f8e4b0a17e5914e7d285cec2228", "sha256": "c092b7b270cf1812487b739fdbdda0cfb2a6d9c7ef9108b30b15230673273c16" }, "downloads": -1, "filename": "graphlayer-0.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "d2780f8e4b0a17e5914e7d285cec2228", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 25724, "upload_time": "2019-06-19T21:29:05", "url": "https://files.pythonhosted.org/packages/db/6f/c06963d6af77f731ca49886f2c796504e594180b8de0381efbe9136e49e8/graphlayer-0.2.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3431d3ada433efe483d71522b15c7a3d", "sha256": "54e1df8d9ffe860b803250b3dc3fbf4b4aa76309d4737c1ebdce9ad9ddc8cecf" }, "downloads": -1, "filename": "graphlayer-0.2.2.tar.gz", "has_sig": false, "md5_digest": "3431d3ada433efe483d71522b15c7a3d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33637, "upload_time": "2019-06-19T21:29:02", "url": "https://files.pythonhosted.org/packages/e8/48/2953d3e6cd66d76e620044e2c7ab07fa841fd16b60bb62deee3174eb35fa/graphlayer-0.2.2.tar.gz" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "4c10119e5a6b5184aa385913c88367da", "sha256": "23fde1579f1a79c3af17821516f25abbf5aae813e4488971facfe528171233c7" }, "downloads": -1, "filename": "graphlayer-0.2.3-py3-none-any.whl", "has_sig": false, "md5_digest": "4c10119e5a6b5184aa385913c88367da", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 25964, "upload_time": "2019-06-21T19:27:27", "url": "https://files.pythonhosted.org/packages/ff/c2/ccb28d0f03e7a5ae42aa73e5d3ce79e7f17950222b3b182c82cb9b1e75f1/graphlayer-0.2.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "155a95069e5fd64c61f2d432daffcd3a", "sha256": "6cad0700aefc7f7f0e9d8cb83cf6f611844ef5fe67d814d3f793099c5d69e8c0" }, "downloads": -1, "filename": "graphlayer-0.2.3.tar.gz", "has_sig": false, "md5_digest": "155a95069e5fd64c61f2d432daffcd3a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33813, "upload_time": "2019-06-21T19:27:24", "url": "https://files.pythonhosted.org/packages/04/e5/f2d7f238747e8067fce1f4dcd690904baefaeb333c53df9e880ab3647243/graphlayer-0.2.3.tar.gz" } ], "0.2.4": [ { "comment_text": "", "digests": { "md5": "3f538741a9753489cc058deaf5a06fcc", "sha256": "ad9a84957f0331d44b8b955bebfb360be081ce12b507dfd4a1198d2f02da0160" }, "downloads": -1, "filename": "graphlayer-0.2.4-py3-none-any.whl", "has_sig": false, "md5_digest": "3f538741a9753489cc058deaf5a06fcc", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26120, "upload_time": "2019-07-12T08:58:41", "url": "https://files.pythonhosted.org/packages/f2/6a/1c2c59781762fec5b14196591adae40d81b7a0a82c296d34181c56e09e71/graphlayer-0.2.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f5008b4afe00a328cde9c0cfb68e6392", "sha256": "20431d1d3e2072a98645c3709c548953a1ec57466ea3aa53a2013d1ee6ea1b26" }, "downloads": -1, "filename": "graphlayer-0.2.4.tar.gz", "has_sig": false, "md5_digest": "f5008b4afe00a328cde9c0cfb68e6392", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33952, "upload_time": "2019-07-12T08:58:37", "url": "https://files.pythonhosted.org/packages/3e/ac/c0386afed8f744abf22f58e04222629bbcb4b720d9536a9b11091892322d/graphlayer-0.2.4.tar.gz" } ], "0.2.5": [ { "comment_text": "", "digests": { "md5": "1808aa84ee9543524041e895db1ce39e", "sha256": "a30bde1208c093fd19e3a59538d5d7f78263ab7cc08b0bc5e5ef00b866d2dcdb" }, "downloads": -1, "filename": "graphlayer-0.2.5-py3-none-any.whl", "has_sig": false, "md5_digest": "1808aa84ee9543524041e895db1ce39e", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26186, "upload_time": "2019-07-29T08:36:44", "url": "https://files.pythonhosted.org/packages/0f/2d/f8370b512521231295bbcdc23bdb22d30d0dad0a4497ae74da1f2449413c/graphlayer-0.2.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "cceb307a2e4ec9d2e22512c8992c6846", "sha256": "1201f14ed713e77b74b06d5af4a0cd23527eee46bb2d21f04b07fd29bc7f675e" }, "downloads": -1, "filename": "graphlayer-0.2.5.tar.gz", "has_sig": false, "md5_digest": "cceb307a2e4ec9d2e22512c8992c6846", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34035, "upload_time": "2019-07-29T08:36:41", "url": "https://files.pythonhosted.org/packages/b9/84/8b2fe65d3a401a55588d8c179a4b6dae3c888948f86f9ea8690781b978fe/graphlayer-0.2.5.tar.gz" } ], "0.2.6": [ { "comment_text": "", "digests": { "md5": "7db15fbd2e5f506f2b9205ec0a99b177", "sha256": "9b0ee4aaea9c80ec5ce951abd7e08c349d0ce92514f6ab0bd05f9bd1e0167a89" }, "downloads": -1, "filename": "graphlayer-0.2.6-py3-none-any.whl", "has_sig": false, "md5_digest": "7db15fbd2e5f506f2b9205ec0a99b177", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26185, "upload_time": "2019-07-29T08:48:21", "url": "https://files.pythonhosted.org/packages/50/60/2221593d00f725a893daa58519c579ced8c9806664773ac21224e09db460/graphlayer-0.2.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ee16fd663ad155645963efce3a26a27f", "sha256": "76e39090b85aa28024ca887adce184831798cb4f7e047514bf48e0f0a95c406f" }, "downloads": -1, "filename": "graphlayer-0.2.6.tar.gz", "has_sig": false, "md5_digest": "ee16fd663ad155645963efce3a26a27f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34037, "upload_time": "2019-07-29T08:48:18", "url": "https://files.pythonhosted.org/packages/de/69/e0e63046afc5cded37a410977c9f54bbdee363c4051f19446f99a51f343f/graphlayer-0.2.6.tar.gz" } ], "0.2.7": [ { "comment_text": "", "digests": { "md5": "9290a87ea959bb75a43540dfdc5f737a", "sha256": "ebefcc283234657cbe0e34cbf195490c0b7b4de74d099a90d838dbd12cfe7935" }, "downloads": -1, "filename": "graphlayer-0.2.7-py3-none-any.whl", "has_sig": false, "md5_digest": "9290a87ea959bb75a43540dfdc5f737a", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26217, "upload_time": "2019-08-10T16:16:44", "url": "https://files.pythonhosted.org/packages/84/62/ad3b0521d6aef0ce10397b22b8684f3c61da01b0db4cf83512fe66980e70/graphlayer-0.2.7-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d9dd30bf16b9335c2a103d6050100c62", "sha256": "eac96b564e2e57c0758971024a9fa252be7313f6d6568ec5a3e484959e4ea0b1" }, "downloads": -1, "filename": "graphlayer-0.2.7.tar.gz", "has_sig": false, "md5_digest": "d9dd30bf16b9335c2a103d6050100c62", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34063, "upload_time": "2019-08-10T16:16:41", "url": "https://files.pythonhosted.org/packages/6a/19/b5960116350a3130a21aec12b822fbbf18b01609369576e430632c4cb237/graphlayer-0.2.7.tar.gz" } ], "0.2.8": [ { "comment_text": "", "digests": { "md5": "3bf42cdeb8e0425b57dca1a4555eabe6", "sha256": "e78e821ba9653d6e1cd44a20e7ef5aab3f0169b4d834deb1ccdac7ef446e9e47" }, "downloads": -1, "filename": "graphlayer-0.2.8-py3-none-any.whl", "has_sig": false, "md5_digest": "3bf42cdeb8e0425b57dca1a4555eabe6", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26316, "upload_time": "2019-10-18T10:52:31", "url": "https://files.pythonhosted.org/packages/55/72/851cc0e3c0ab4dc59bc7c07250ba1ea40358b2ba70f81182d6a590aaa024/graphlayer-0.2.8-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "55901f7445c96541668c21933e126bf0", "sha256": "1e09ed9ef00cdd6b7b490b89b582e2bfb139742ca98d7b8fee1919e18c5024c5" }, "downloads": -1, "filename": "graphlayer-0.2.8.tar.gz", "has_sig": false, "md5_digest": "55901f7445c96541668c21933e126bf0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34166, "upload_time": "2019-10-18T10:52:28", "url": "https://files.pythonhosted.org/packages/ff/05/d1ac4a6514c8d4fd9f2e6c65569a7c45c3e3cd352a882756f1c984c79641/graphlayer-0.2.8.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "3bf42cdeb8e0425b57dca1a4555eabe6", "sha256": "e78e821ba9653d6e1cd44a20e7ef5aab3f0169b4d834deb1ccdac7ef446e9e47" }, "downloads": -1, "filename": "graphlayer-0.2.8-py3-none-any.whl", "has_sig": false, "md5_digest": "3bf42cdeb8e0425b57dca1a4555eabe6", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 26316, "upload_time": "2019-10-18T10:52:31", "url": "https://files.pythonhosted.org/packages/55/72/851cc0e3c0ab4dc59bc7c07250ba1ea40358b2ba70f81182d6a590aaa024/graphlayer-0.2.8-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "55901f7445c96541668c21933e126bf0", "sha256": "1e09ed9ef00cdd6b7b490b89b582e2bfb139742ca98d7b8fee1919e18c5024c5" }, "downloads": -1, "filename": "graphlayer-0.2.8.tar.gz", "has_sig": false, "md5_digest": "55901f7445c96541668c21933e126bf0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34166, "upload_time": "2019-10-18T10:52:28", "url": "https://files.pythonhosted.org/packages/ff/05/d1ac4a6514c8d4fd9f2e6c65569a7c45c3e3cd352a882756f1c984c79641/graphlayer-0.2.8.tar.gz" } ] }