Skip to content

Commit

Permalink
First working version of graphql_relay with connections 😃
Browse files Browse the repository at this point in the history
  • Loading branch information
syrusakbary committed Sep 16, 2015
0 parents commit 0165590
Show file tree
Hide file tree
Showing 14 changed files with 1,091 additions and 0 deletions.
61 changes: 61 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Created by https://www.gitignore.io

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: python
sudo: false
python:
- 2.7
install:
- pip install pytest pytest-cov coveralls flake8
- pip install -e .[django]
script:
- py.test --cov=graphql
- flake8
after_success:
- coveralls
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 Syrus Akbary

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# graphql-relay-py

GraphQL-Relay implementation for Python

[![Build Status](https://travis-ci.org/syrusakbary/graphql-relay-py.svg?branch=master)](https://travis-ci.org/syrusakbary/graphql-relay-py)
[![Coverage Status](https://coveralls.io/repos/syrusakbary/graphql-relay-py/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphql-relay-py?branch=master)
14 changes: 14 additions & 0 deletions graphql_relay/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .connection.connection import (
connectionArgs,
connectionDefinitions
)
from .connection.arrayconnection import (
connectionFromArray,
connectionFromPromisedArray,
cursorForObjectInConnection
)

__all__ = [
'connectionArgs', 'connectionDefinitions', 'connectionFromArray',
'connectionFromPromisedArray', 'cursorForObjectInConnection'
]
Empty file.
122 changes: 122 additions & 0 deletions graphql_relay/connection/arrayconnection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from base64 import b64encode as base64, b64decode as unbase64

from .connectiontypes import Connection, PageInfo, Edge


def connectionFromArray(data, args={}, **kwargs):
'''
A simple function that accepts an array and connection arguments, and returns
a connection object for use in GraphQL. It uses array offsets as pagination,
so pagination will only work if the array is static.
'''
edges = [Edge(value, cursor=offsetToCursor(index)) for index, value in enumerate(data)]
full_args = dict(args, **kwargs)

before = full_args.get('before')
after = full_args.get('after')
first = full_args.get('first')
last = full_args.get('last')

# Slice with cursors
begin = max(getOffset(after, -1), -1) + 1;
end = min(getOffset(before, len(edges) + 1), len(edges) + 1);
edges = edges[begin:end]
if len(edges) == 0:
return emptyConnection()

# Save the pre-slice cursors
firstPresliceCursor = edges[0].cursor
lastPresliceCursor = edges[len(edges) - 1].cursor

# Slice with limits
if first != None:
edges = edges[0:first]
if last != None:
edges = edges[-last:]

if len(edges) == 0:
return emptyConnection()

# Construct the connection
firstEdge = edges[0];
lastEdge = edges[len(edges) - 1];
return Connection(
edges,
PageInfo(
startCursor=firstEdge.cursor,
endCursor=lastEdge.cursor,
hasPreviousPage= (firstEdge.cursor != firstPresliceCursor),
hasNextPage= (lastEdge.cursor != lastPresliceCursor)
)
)


def connectionFromPromisedArray(dataPromise, args={}, **kwargs):
'''
A version of the above that takes a promised array, and returns a promised
connection.
'''
# TODO: Promises not implemented
raise Exception('connectionFromPromisedArray is not implemented yet')
# return dataPromise.then(lambda data:connectionFromArray(data, args))


def emptyConnection():
'''
Helper to get an empty connection.
'''
return Connection(
[],
PageInfo(
startCursor=None,
endCursor=None,
hasPreviousPage=False,
hasNextPage=False,
)
)


PREFIX = 'arrayconnection:';


def offsetToCursor(offset):
'''
Creates the cursor string from an offset.
'''
return base64(PREFIX + str(offset));


def cursorToOffset(cursor):
'''
Rederives the offset from the cursor string.
'''
try:
return int(unbase64(cursor)[len(PREFIX):len(PREFIX)+10])
except:
return None

def cursorForObjectInConnection(data, _object):
'''
Return the cursor associated with an object in an array.
'''
if _object not in data:
return None

offset = data.index(_object)
return offsetToCursor(offset)


def getOffset(cursor, defaultOffset=0):
'''
Given an optional cursor and a default offset, returns the offset
to use; if the cursor contains a valid offset, that will be used,
otherwise it will be the default.
'''
if cursor == None:
return defaultOffset

offset = cursorToOffset(cursor)
try:
return int(offset)
except:
return defaultOffset
109 changes: 109 additions & 0 deletions graphql_relay/connection/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from graphql.core.type import (
GraphQLArgument,
GraphQLBoolean,
GraphQLInt,
GraphQLNonNull,
GraphQLList,
GraphQLObjectType,
GraphQLString,
GraphQLField
)


class ConnectionConfig(object):
'''
Returns a GraphQLFieldConfigArgumentMap appropriate to include
on a field whose return type is a connection type.
'''
def __init__(self, name, nodeType, edgeFields=None, connectionFields=None):
self.name = name
self.nodeType = nodeType
self.edgeFields = edgeFields
self.connectionFields = connectionFields


class GraphQLConnection(object):
def __init__(self, edgeType, connectionType):
self.edgeType = edgeType
self.connectionType = connectionType


connectionArgs = {
'before': GraphQLArgument(GraphQLString),
'after': GraphQLArgument(GraphQLString),
'first': GraphQLArgument(GraphQLInt),
'last': GraphQLArgument(GraphQLInt),
}


def resolveMaybeThunk(f):
if hasattr(f, '__call__'):
return f()
return f


def connectionDefinitions(*args, **kwargs):
if len(args) == 1 and not kwargs and isinstance(args[0], ConnectionConfig):
config = args[0]
else:
config = ConnectionConfig(*args, **kwargs)
name, nodeType = config.name, config.nodeType
edgeFields = config.edgeFields or {}
connectionFields = config.connectionFields or {}
edgeType = GraphQLObjectType(
name+'Edge',
description='An edge in a connection.',
fields=lambda: dict({
'node': GraphQLField(
nodeType,
description='The item at the end of the edge',
),
'cursor': GraphQLField(
GraphQLNonNull(GraphQLString),
description='A cursor for use in pagination',
)
}, **resolveMaybeThunk(edgeFields))
)

connectionType = GraphQLObjectType(
name+'Connection',
description='A connection to a list of items.',
fields=lambda: dict({
'pageInfo': GraphQLField(
GraphQLNonNull(pageInfoType),
description='The Information to aid in pagination',
),
'edges': GraphQLField(
GraphQLList(edgeType),
description='Information to aid in pagination.',
)
}, **resolveMaybeThunk(connectionFields))
)

return GraphQLConnection(edgeType, connectionType)


# The common page info type used by all connections.

pageInfoType = GraphQLObjectType(
'PageInfo',
description='Information about pagination in a connection.',
fields=lambda:{
'hasNextPage': GraphQLField(
GraphQLNonNull(GraphQLBoolean),
description='When paginating forwards, are there more items?',
),
'hasPreviousPage': GraphQLField(
GraphQLNonNull(GraphQLBoolean),
description='When paginating backwards, are there more items?',
),
'startCursor': GraphQLField(
GraphQLString,
description='When paginating backwards, the cursor to continue.',
),
'endCursor': GraphQLField(
GraphQLString,
description='When paginating forwards, the cursor to continue.',
),
}
)
36 changes: 36 additions & 0 deletions graphql_relay/connection/connectiontypes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class Connection(object):
def __init__(self, edges, pageInfo):
self.edges = edges
self.pageInfo = pageInfo

def to_dict(self):
return {
'edges': [e.to_dict() for e in self.edges],
'pageInfo': self.pageInfo.to_dict(),
}

class PageInfo(object):
def __init__(self, startCursor="", endCursor="", hasPreviousPage=False, hasNextPage=False):
self.startCursor = startCursor
self.endCursor = endCursor
self.hasPreviousPage = hasPreviousPage
self.hasNextPage = hasNextPage

def to_dict(self):
return {
'startCursor': self.startCursor,
'endCursor': self.endCursor,
'hasPreviousPage': self.hasPreviousPage,
'hasNextPage': self.hasNextPage,
}

class Edge(object):
def __init__(self, node, cursor):
self.node = node
self.cursor = cursor

def to_dict(self):
return {
'node': self.node,
'cursor': self.cursor,
}
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
exclude = tests/*,setup.py
max-line-length = 160
Loading

0 comments on commit 0165590

Please sign in to comment.