Merge pull request #87 from graphql-python/sqlalchemy

Added SQLAlchemy support into graphene 💪
This commit is contained in:
Syrus Akbary 2016-01-26 11:27:26 -08:00
commit 908c18fdeb
31 changed files with 1303 additions and 5 deletions

View File

@ -25,6 +25,7 @@ install:
if [ "$TEST_TYPE" = build ]; then
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter
pip install --download-cache $HOME/.cache/pip/ -e .[django]
pip install --download-cache $HOME/.cache/pip/ -e .[sqlalchemy]
pip install django==$DJANGO_VERSION
python setup.py develop
elif [ "$TEST_TYPE" = build_website ]; then

View File

@ -7,6 +7,7 @@
- **Relay:** Graphene has builtin support for Relay
- **Django:** Automatic *Django model* mapping to Graphene Types. Check a fully working [Django](http://github.com/graphql-python/swapi-graphene) implementation
Graphene also supports *SQLAlchemy*!
*What is supported in this Python version?* **Everything**: Interfaces, ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition to queries, mutations and subscriptions.
@ -18,8 +19,10 @@ For instaling graphene, just run this command in your shell
```bash
pip install graphene
# Or in case of need Django model support
# In case of need Django model support
pip install graphene[django]
# Or in case of need SQLAlchemy support
pip install graphene[sqlalchemy]
```
@ -59,6 +62,7 @@ If you want to learn even more, you can also check the following [examples](exam
* **Basic Schema**: [Starwars example](examples/starwars)
* **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
* **Django model mapping**: [Starwars Django example](examples/starwars_django)
* **SQLAlchemy model mapping**: [Flask SQLAlchemy example](examples/flask_sqlalchemy)
## Contributing

View File

@ -12,6 +12,8 @@ building GraphQL schemas/types fast and easily.
`Django <http://github.com/graphql-python/swapi-graphene>`__
implementation
Graphene also supports *SQLAlchemy*!
*What is supported in this Python version?* **Everything**: Interfaces,
ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition
to queries, mutations and subscriptions.
@ -27,8 +29,10 @@ For instaling graphene, just run this command in your shell
.. code:: bash
pip install graphene
# Or in case of need Django model support
# In case of need Django model support
pip install graphene[django]
# Or in case of need SQLAlchemy support
pip install graphene[sqlalchemy]
Examples
--------
@ -70,6 +74,8 @@ If you want to learn even more, you can also check the following
example <examples/starwars_relay>`__
- **Django model mapping**: `Starwars Django
example <examples/starwars_django>`__
- **SQLAlchemy model mapping**: `Flask SQLAlchemy
example <examples/flask_sqlalchemy>`__
Contributing
------------

View File

@ -23,3 +23,10 @@ ga = "UA-12613282-7"
"/docs/django/tutorial/",
"/docs/django/filtering/",
]
[docs.sqlalchemy]
name = "SQLAlchemy"
pages = [
"/docs/sqlalchemy/tutorial/",
"/docs/sqlalchemy/tips/",
]

View File

@ -97,6 +97,7 @@
}
#graphiql-container .resultWrap {
position: relative;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
@ -1010,6 +1011,52 @@ span.CodeMirror-selectedtext { background: none; }
background-position: right bottom;
width: 100%; height: 100%;
}
#graphiql-container .spinner-container {
position: absolute;
top: 50%;
height: 36px;
width: 36px;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
#graphiql-container .spinner {
vertical-align: middle;
display: inline-block;
height: 24px;
width: 24px;
position: absolute;
-webkit-animation: rotation .6s infinite linear;
-moz-animation: rotation .6s infinite linear;
-o-animation: rotation .6s infinite linear;
animation: rotation .6s infinite linear;
border-left: 6px solid rgba(150, 150, 150, .15);
border-right: 6px solid rgba(150, 150, 150, .15);
border-bottom: 6px solid rgba(150, 150, 150, .15);
border-top: 6px solid rgba(150, 150, 150, .8);
border-radius: 100%;
}
@-webkit-keyframes rotation {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(359deg); }
}
@-moz-keyframes rotation {
from { -moz-transform: rotate(0deg); }
to { -moz-transform: rotate(359deg); }
}
@-o-keyframes rotation {
from { -o-transform: rotate(0deg); }
to { -o-transform: rotate(359deg); }
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); }
}
.CodeMirror-hints {
background: white;
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);

View File

@ -18,7 +18,7 @@
"es6-promise": "^3.0.2",
"extract-text-webpack-plugin": "^0.9.1",
"gatsby": "^0.7.7",
"graphiql": "^0.4.2",
"graphiql": "^0.4.5",
"graphql": "^0.4.13",
"jeet": "^6.1.2",
"lodash": "^3.10.1",

View File

@ -21,6 +21,9 @@ Django integration:
- **graphql-django-view**: [Source Code][5] - [PyPI package][6]
- **django-graphiql**: [Source Code][7] - [PyPI package][8]
Flask integration:
- **graphql-flask**: [Source Code][9] - [PyPI package][10]
## Other related projects
- [Flask GraphQL Demo](https://github.com/amitsaha/flask-graphql-demo) by [@echorand](https://twitter.com/echorand)
@ -37,3 +40,5 @@ Django integration:
[6]: https://pypi.python.org/pypi/graphql-django-view
[7]: https://github.com/graphql-python/django-graphiql
[8]: https://pypi.python.org/pypi/django-graphiql
[9]: https://github.com/graphql-python/graphql-flask
[10]: https://pypi.python.org/pypi/graphql-flask

View File

@ -0,0 +1,30 @@
---
title: Tips
description: Tips when SQLAlchemy in Graphene
---
# Tips
## Querying
For make querying to the database work, there are two alternatives:
* Expose the db session when you create the `graphene.Schema`:
```python
schema = graphene.Schema(session=session)
```
* Create a query for the models.
```python
Base = declarative_base()
Base.query = db_session.query_property()
class MyModel(Base):
# ...
```
If you don't specify any, the following error will be displayed:
`A query in the model Base or a session in the schema is required for querying.`

View File

@ -0,0 +1,199 @@
---
title: Tutorial
description: Using SQLAlchemy with Graphene
---
# SQLAlchemy + Flask Tutorial
Graphene comes with builtin support to SQLAlchemy, which makes quite easy to operate with your current models.
**Note: The code in this tutorial is pulled from the
[Flask SQLAlchemy example app](https://github.com/graphql-python/graphene/tree/master/examples/flask_sqlalchemy)**.
## Setup the Project
We will setup the project, execute the following:
```bash
# Create the project directory
mkdir flask_sqlalchemy
cd flask_sqlalchemy
# Create a virtualenv to isolate our package dependencies locally
virtualenv env
source env/bin/activate # On Windows use `env\Scripts\activate`
# SQLAlchemy and Graphene with SQLAlchemy support
pip install SQLAlchemy
pip install graphene[sqlalchemy]
# Install Flask and GraphQL Flask for exposing the schema through HTTP
pip install Flask
pip install graphql-flask
```
## Defining our models
Let's get started with these models:
```python
# flask_sqlalchemy/models.py
from sqlalchemy import *
from sqlalchemy.orm import (scoped_session, sessionmaker, relationship,
backref)
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
# We will need this for querying
Base.query = db_session.query_property()
class Department(Base):
__tablename__ = 'department'
id = Column(Integer, primary_key=True)
name = Column(String)
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(String)
hired_on = Column(DateTime, default=func.now())
department_id = Column(Integer, ForeignKey('department.id'))
department = relationship(
Department,
backref=backref('employees',
uselist=True,
cascade='delete,all'))
```
## Schema
GraphQL presents your objects to the world as a graph structure rather than a more
hierarchical structure to which you may be accustomed. In order to create this
representation, Graphene needs to know about each *type* of object which will appear in
the graph.
This graph also has a *root type* through which all access begins. This is the `Query` class below.
In this example, we provide the ability to list all employees via `all_employees`, and the
ability to obtain a specific node via `node`.
Create `flask_sqlalchemy/schema.py` and type the following:
```python
# flask_sqlalchemy/schema.py
import graphene
from graphene import relay
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField
from models import db_session, Department as DepartmentModel, Employee as EmployeeModel
schema = graphene.Schema()
@schema.register
class Department(SQLAlchemyNode):
class Meta:
model = DepartmentModel
@schema.register
class Employee(SQLAlchemyNode):
class Meta:
model = EmployeeModel
class Query(graphene.ObjectType):
node = relay.NodeField()
all_employees = SQLAlchemyConnectionField(Employee)
schema.query = Query
```
## Creating GraphQL and GraphiQL views in Flask
Unlike a RESTful API, there is only a single URL from which GraphQL is accessed.
We are going to use Flask to create a server that expose the GraphQL schema under `/graphql` and a interface for querying it easily: GraphiQL under `/graphiql`.
Afortunately for us, the library `graphql-flask` that we installed previously is making the task quite easy.
```python
# flask_sqlalchemy/app.py
from flask import Flask
from graphql_flask import GraphQL
from models import db_session
from schema import schema, Department
app = Flask(__name__)
app.debug = True
# This is creating the `/graphql` and `/graphiql` endpoints
GraphQL(app, schema=schema)
@app.teardown_appcontext
def shutdown_session(exception=None):
db_session.remove()
if __name__ == '__main__':
app.run()
```
## Creating some data
```bash
$ python
>>> from models import engine, db_session, Base, Department, Employee
>>> Base.metadata.create_all(bind=engine)
>>> # Fill the tables with some data
>>> engineering = Department(name='Engineering')
>>> db_session.add(engineering)
>>> hr = Department(name='Human Resources')
>>> db_session.add(hr)
>>> peter = Employee(name='Peter', department=engineering)
>>> db_session.add(peter)
>>> roy = Employee(name='Roy', department=engineering)
>>> db_session.add(roy)
>>> tracy = Employee(name='Tracy', department=hr)
>>> db_session.add(tracy)
>>> db_session.commit()
```
## Testing our GraphQL schema
We're now ready to test the API we've built. Let's fire up the server from the command line.
```bash
$ python ./app.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
```
Go to [localhost:5000/graphiql](http://localhost:5000/graphiql) and type your first query!
```graphql
{
allEmployees {
edges {
node {
id
name
department {
name
}
}
}
}
}
```

View File

@ -0,0 +1,50 @@
Example Flask+SQLAlchemy Project
================================
This example project demos integration between Graphene, Flask and SQLAlchemy.
The project contains two models, one named `Department` and another
named `Employee`.
Getting started
---------------
First you'll need to get the source of the project. Do this by cloning the
whole Graphene repository:
```bash
# Get the example project code
git clone https://github.com/graphql-python/graphene.git
cd graphene/examples/flask_sqlalchemy
```
It is good idea (but not required) to create a virtual environment
for this project. We'll do this using
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
to keep things simple,
but you may also find something like
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
to be useful:
```bash
# Create a virtualenv in which we can install the dependencies
virtualenv env
source env/bin/activate
```
Now we can install our dependencies:
```bash
pip install -r requirements.txt
```
Now the following command will setup the database, and start the server:
```bash
./app.py
```
Now head on over to
[http://127.0.0.1:5000/graphiql](http://127.0.0.1:5000/graphiql)
and run some queries!

View File

View File

@ -0,0 +1,34 @@
from flask import Flask
from database import db_session, init_db
from schema import schema
from graphql_flask import GraphQL
app = Flask(__name__)
app.debug = True
default_query = '''
{
allEmployees {
edges {
node {
id
name
department {
name
}
}
}
}
}'''.strip()
GraphQL(app, schema=schema, default_query=default_query)
@app.teardown_appcontext
def shutdown_session(exception=None):
db_session.remove()
if __name__ == '__main__':
init_db()
app.run()

View File

@ -0,0 +1,33 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
def init_db():
# import all modules here that might define models so that
# they will be registered properly on the metadata. Otherwise
# you will have to import them first before calling init_db()
from models import Department, Employee
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
# Create the fixtures
engineering = Department(name='Engineering')
db_session.add(engineering)
hr = Department(name='Human Resources')
db_session.add(hr)
peter = Employee(name='Peter', department=engineering)
db_session.add(peter)
roy = Employee(name='Roy', department=engineering)
db_session.add(roy)
tracy = Employee(name='Tracy', department=hr)
db_session.add(tracy)
db_session.commit()

View File

@ -0,0 +1,26 @@
from database import Base
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func
from sqlalchemy.orm import relationship, backref
class Department(Base):
__tablename__ = 'department'
id = Column(Integer, primary_key=True)
name = Column(String)
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(String)
# Use default=func.now() to set the default hiring time
# of an Employee to be the current time when an
# Employee record was created
hired_on = Column(DateTime, default=func.now())
department_id = Column(Integer, ForeignKey('department.id'))
# Use cascade='delete,all' to propagate the deletion of a Department onto its Employees
department = relationship(
Department,
backref=backref('employees',
uselist=True,
cascade='delete,all'))

View File

@ -0,0 +1,4 @@
graphene[sqlalchemy]
graphql_flask==1.1.0
SQLAlchemy==1.0.11
Flask==0.10.1

View File

@ -0,0 +1,25 @@
import graphene
from graphene import relay
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField
from models import Department as DepartmentModel, Employee as EmployeeModel
schema = graphene.Schema()
@schema.register
class Department(SQLAlchemyNode):
class Meta:
model = DepartmentModel
@schema.register
class Employee(SQLAlchemyNode):
class Meta:
model = EmployeeModel
class Query(graphene.ObjectType):
node = relay.NodeField()
all_employees = SQLAlchemyConnectionField(Employee)
schema.query = Query

View File

@ -0,0 +1,11 @@
from graphene.contrib.sqlalchemy.types import (
SQLAlchemyObjectType,
SQLAlchemyNode
)
from graphene.contrib.sqlalchemy.fields import (
SQLAlchemyConnectionField,
SQLAlchemyModelField
)
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyNode',
'SQLAlchemyConnectionField', 'SQLAlchemyModelField']

View File

@ -0,0 +1,61 @@
from singledispatch import singledispatch
from sqlalchemy import types
from sqlalchemy.orm import interfaces
from ...core.types.scalars import ID, Boolean, Float, Int, String
from .fields import ConnectionOrListField, SQLAlchemyModelField
def convert_sqlalchemy_relationship(relationship):
direction = relationship.direction
model = relationship.mapper.entity
model_field = SQLAlchemyModelField(model, description=relationship.doc)
if direction == interfaces.MANYTOONE:
return model_field
elif (direction == interfaces.ONETOMANY or
direction == interfaces.MANYTOMANY):
return ConnectionOrListField(model_field)
def convert_sqlalchemy_column(column):
return convert_sqlalchemy_type(getattr(column, 'type', None), column)
@singledispatch
def convert_sqlalchemy_type(type, column):
raise Exception(
"Don't know how to convert the SQLAlchemy field %s (%s)" % (column, column.__class__))
@convert_sqlalchemy_type.register(types.Date)
@convert_sqlalchemy_type.register(types.DateTime)
@convert_sqlalchemy_type.register(types.Time)
@convert_sqlalchemy_type.register(types.String)
@convert_sqlalchemy_type.register(types.Text)
@convert_sqlalchemy_type.register(types.Unicode)
@convert_sqlalchemy_type.register(types.UnicodeText)
@convert_sqlalchemy_type.register(types.Enum)
def convert_column_to_string(type, column):
return String(description=column.doc)
@convert_sqlalchemy_type.register(types.SmallInteger)
@convert_sqlalchemy_type.register(types.BigInteger)
@convert_sqlalchemy_type.register(types.Integer)
def convert_column_to_int_or_id(type, column):
if column.primary_key:
return ID(description=column.doc)
else:
return Int(description=column.doc)
@convert_sqlalchemy_type.register(types.Boolean)
def convert_column_to_boolean(type, column):
return Boolean(description=column.doc)
@convert_sqlalchemy_type.register(types.Float)
@convert_sqlalchemy_type.register(types.Numeric)
def convert_column_to_float(type, column):
return Float(description=column.doc)

View File

@ -0,0 +1,66 @@
from ...core.exceptions import SkipField
from ...core.fields import Field
from ...core.types.base import FieldType
from ...core.types.definitions import List
from ...relay import ConnectionField
from ...relay.utils import is_node
from .utils import get_type_for_model, maybe_query, get_query
class SQLAlchemyConnectionField(ConnectionField):
def __init__(self, *args, **kwargs):
return super(SQLAlchemyConnectionField, self).__init__(*args, **kwargs)
@property
def model(self):
return self.type._meta.model
def get_query(self, resolved_query, args, info):
return resolved_query or get_query(self.model, info)
def from_list(self, connection_type, resolved, args, info):
query = self.get_query(resolved, args, info)
query = maybe_query(query)
return super(SQLAlchemyConnectionField, self).from_list(connection_type, query, args, info)
class ConnectionOrListField(Field):
def internal_type(self, schema):
model_field = self.type
field_object_type = model_field.get_object_type(schema)
if not field_object_type:
raise SkipField()
if is_node(field_object_type):
field = SQLAlchemyConnectionField(field_object_type)
else:
field = Field(List(field_object_type))
field.contribute_to_class(self.object_type, self.attname)
return schema.T(field)
class SQLAlchemyModelField(FieldType):
def __init__(self, model, *args, **kwargs):
self.model = model
super(SQLAlchemyModelField, self).__init__(*args, **kwargs)
def internal_type(self, schema):
_type = self.get_object_type(schema)
if not _type and self.parent._meta.only_fields:
raise Exception(
"Table %r is not accessible by the schema. "
"You can either register the type manually "
"using @schema.register. "
"Or disable the field in %s" % (
self.model,
self.parent,
)
)
if not _type:
raise SkipField()
return schema.T(_type)
def get_object_type(self, schema):
return get_type_for_model(schema, self.model)

View File

@ -0,0 +1,23 @@
from ...core.classtypes.objecttype import ObjectTypeOptions
from ...relay.types import Node
from ...relay.utils import is_node
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
class SQLAlchemyOptions(ObjectTypeOptions):
def __init__(self, *args, **kwargs):
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
self.model = None
self.valid_attrs += VALID_ATTRS
self.only_fields = None
self.exclude_fields = []
self.filter_fields = None
self.filter_order_by = None
def contribute_to_class(self, cls, name):
super(SQLAlchemyOptions, self).contribute_to_class(cls, name)
if is_node(cls):
self.exclude_fields = list(self.exclude_fields) + ['id']
self.interfaces.append(Node)

View File

@ -0,0 +1,36 @@
from __future__ import absolute_import
from sqlalchemy import Column, Date, ForeignKey, Integer, String, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
association_table = Table('association', Base.metadata,
Column('pet_id', Integer, ForeignKey('pets.id')),
Column('reporter_id', Integer, ForeignKey('reporters.id')))
class Pet(Base):
__tablename__ = 'pets'
id = Column(Integer(), primary_key=True)
name = Column(String(30))
reporter_id = Column(Integer(), ForeignKey('reporters.id'))
class Reporter(Base):
__tablename__ = 'reporters'
id = Column(Integer(), primary_key=True)
first_name = Column(String(30))
last_name = Column(String(30))
email = Column(String())
pets = relationship('Pet', secondary=association_table, backref='reporters')
articles = relationship('Article', backref='reporter')
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer(), primary_key=True)
headline = Column(String(100))
pub_date = Column(Date())
reporter_id = Column(Integer(), ForeignKey('reporters.id'))

View File

@ -0,0 +1,105 @@
from py.test import raises
import graphene
from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column,
convert_sqlalchemy_relationship)
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
SQLAlchemyModelField)
from sqlalchemy import Column, types
from .models import Article, Pet, Reporter
def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
column = Column(sqlalchemy_type, doc='Custom Help Text', **kwargs)
graphene_type = convert_sqlalchemy_column(column)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.as_field()
assert field.description == 'Custom Help Text'
return field
def test_should_unknown_sqlalchemy_field_raise_exception():
with raises(Exception) as excinfo:
convert_sqlalchemy_column(None)
assert 'Don\'t know how to convert the SQLAlchemy field' in str(excinfo.value)
def test_should_date_convert_string():
assert_column_conversion(types.Date(), graphene.String)
def test_should_datetime_convert_string():
assert_column_conversion(types.DateTime(), graphene.String)
def test_should_time_convert_string():
assert_column_conversion(types.Time(), graphene.String)
def test_should_string_convert_string():
assert_column_conversion(types.String(), graphene.String)
def test_should_text_convert_string():
assert_column_conversion(types.Text(), graphene.String)
def test_should_unicode_convert_string():
assert_column_conversion(types.Unicode(), graphene.String)
def test_should_unicodetext_convert_string():
assert_column_conversion(types.UnicodeText(), graphene.String)
def test_should_enum_convert_string():
assert_column_conversion(types.Enum(), graphene.String)
def test_should_small_integer_convert_int():
assert_column_conversion(types.SmallInteger(), graphene.Int)
def test_should_big_integer_convert_int():
assert_column_conversion(types.BigInteger(), graphene.Int)
def test_should_integer_convert_int():
assert_column_conversion(types.Integer(), graphene.Int)
def test_should_integer_convert_id():
assert_column_conversion(types.Integer(), graphene.ID, primary_key=True)
def test_should_boolean_convert_boolean():
assert_column_conversion(types.Boolean(), graphene.Boolean)
def test_should_float_convert_float():
assert_column_conversion(types.Float(), graphene.Float)
def test_should_numeric_convert_float():
assert_column_conversion(types.Numeric(), graphene.Float)
def test_should_manytomany_convert_connectionorlist():
graphene_type = convert_sqlalchemy_relationship(Reporter.pets.property)
assert isinstance(graphene_type, ConnectionOrListField)
assert isinstance(graphene_type.type, SQLAlchemyModelField)
assert graphene_type.type.model == Pet
def test_should_manytoone_convert_connectionorlist():
field = convert_sqlalchemy_relationship(Article.reporter.property)
assert isinstance(field, SQLAlchemyModelField)
assert field.model == Reporter
def test_should_onetomany_convert_model():
graphene_type = convert_sqlalchemy_relationship(Reporter.articles.property)
assert isinstance(graphene_type, ConnectionOrListField)
assert isinstance(graphene_type.type, SQLAlchemyModelField)
assert graphene_type.type.model == Article

View File

@ -0,0 +1,173 @@
import pytest
import graphene
from graphene import relay
from graphene.contrib.sqlalchemy import SQLAlchemyObjectType, SQLAlchemyNode
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from .models import Base, Reporter, Article
db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
@pytest.yield_fixture(scope='function')
def session():
connection = db.engine.connect()
transaction = connection.begin()
Base.metadata.create_all(connection)
# options = dict(bind=connection, binds={})
session_factory = sessionmaker(bind=connection)
session = scoped_session(session_factory)
yield session
# Finalize test here
transaction.rollback()
connection.close()
session.remove()
def setup_fixtures(session):
reporter = Reporter(first_name='ABA', last_name='X')
session.add(reporter)
reporter2 = Reporter(first_name='ABO', last_name='Y')
session.add(reporter2)
article = Article(headline='Hi!')
session.add(article)
session.commit()
def test_should_query_well(session):
setup_fixtures(session)
class ReporterType(SQLAlchemyObjectType):
class Meta:
model = Reporter
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
reporters = ReporterType.List()
def resolve_reporter(self, *args, **kwargs):
return session.query(Reporter).first()
def resolve_reporters(self, *args, **kwargs):
return session.query(Reporter)
query = '''
query ReporterQuery {
reporter {
firstName,
lastName,
email
}
reporters {
firstName
}
}
'''
expected = {
'reporter': {
'firstName': 'ABA',
'lastName': 'X',
'email': None
},
'reporters': [{
'firstName': 'ABA',
}, {
'firstName': 'ABO',
}]
}
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_node(session):
setup_fixtures(session)
class ReporterNode(SQLAlchemyNode):
class Meta:
model = Reporter
@classmethod
def get_node(cls, id, info):
return Reporter(id=2, first_name='Cookie Monster')
def resolve_articles(self, *args, **kwargs):
return [Article(headline='Hi!')]
class ArticleNode(SQLAlchemyNode):
class Meta:
model = Article
# @classmethod
# def get_node(cls, id, info):
# return Article(id=1, headline='Article node')
class Query(graphene.ObjectType):
node = relay.NodeField()
reporter = graphene.Field(ReporterNode)
article = graphene.Field(ArticleNode)
def resolve_reporter(self, *args, **kwargs):
return Reporter(id=1, first_name='ABA', last_name='X')
def resolve_article(self, *args, **kwargs):
return Article(id=1, headline='Article node')
query = '''
query ReporterQuery {
reporter {
id,
firstName,
articles {
edges {
node {
headline
}
}
}
lastName,
email
}
myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") {
id
... on ReporterNode {
firstName
}
... on ArticleNode {
headline
}
}
}
'''
expected = {
'reporter': {
'id': 'UmVwb3J0ZXJOb2RlOjE=',
'firstName': 'ABA',
'lastName': 'X',
'email': None,
'articles': {
'edges': [{
'node': {
'headline': 'Hi!'
}
}]
},
},
'myArticle': {
'id': 'QXJ0aWNsZU5vZGU6MQ==',
'headline': 'Hi!'
}
}
schema = graphene.Schema(query=Query, session=session)
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -0,0 +1,45 @@
from py.test import raises
from graphene.contrib.sqlalchemy import SQLAlchemyObjectType
from tests.utils import assert_equal_lists
from .models import Reporter
def test_should_raise_if_no_model():
with raises(Exception) as excinfo:
class Character1(SQLAlchemyObjectType):
pass
assert 'model in the Meta' in str(excinfo.value)
def test_should_raise_if_model_is_invalid():
with raises(Exception) as excinfo:
class Character2(SQLAlchemyObjectType):
class Meta:
model = 1
assert 'not a SQLAlchemy model' in str(excinfo.value)
def test_should_map_fields_correctly():
class ReporterType2(SQLAlchemyObjectType):
class Meta:
model = Reporter
assert_equal_lists(
ReporterType2._meta.fields_map.keys(),
['articles', 'first_name', 'last_name', 'email', 'pets', 'id']
)
def test_should_map_only_few_fields():
class Reporter2(SQLAlchemyObjectType):
class Meta:
model = Reporter
only_fields = ('id', 'email')
assert_equal_lists(
Reporter2._meta.fields_map.keys(),
['id', 'email']
)

View File

@ -0,0 +1,102 @@
from graphql.core.type import GraphQLObjectType
from pytest import raises
from graphene import Schema
from graphene.contrib.sqlalchemy.types import (SQLAlchemyNode,
SQLAlchemyObjectType)
from graphene.core.fields import Field
from graphene.core.types.scalars import Int
from graphene.relay.fields import GlobalIDField
from tests.utils import assert_equal_lists
from .models import Article, Reporter
schema = Schema()
class Character(SQLAlchemyObjectType):
'''Character description'''
class Meta:
model = Reporter
@schema.register
class Human(SQLAlchemyNode):
'''Human description'''
pub_date = Int()
class Meta:
model = Article
exclude_fields = ('id', )
def test_sqlalchemy_interface():
assert SQLAlchemyNode._meta.interface is True
# @patch('graphene.contrib.sqlalchemy.tests.models.Article.filter', return_value=Article(id=1))
# def test_sqlalchemy_get_node(get):
# human = Human.get_node(1, None)
# get.assert_called_with(id=1)
# assert human.id == 1
def test_objecttype_registered():
object_type = schema.T(Character)
assert isinstance(object_type, GraphQLObjectType)
assert Character._meta.model == Reporter
assert_equal_lists(
object_type.get_fields().keys(),
['articles', 'firstName', 'lastName', 'email', 'id']
)
def test_sqlalchemynode_idfield():
idfield = SQLAlchemyNode._meta.fields_map['id']
assert isinstance(idfield, GlobalIDField)
def test_node_idfield():
idfield = Human._meta.fields_map['id']
assert isinstance(idfield, GlobalIDField)
def test_node_replacedfield():
idfield = Human._meta.fields_map['pub_date']
assert isinstance(idfield, Field)
assert schema.T(idfield).type == schema.T(Int())
def test_interface_objecttype_init_none():
h = Human()
assert h._root is None
def test_interface_objecttype_init_good():
instance = Article()
h = Human(instance)
assert h._root == instance
def test_interface_objecttype_init_unexpected():
with raises(AssertionError) as excinfo:
Human(object())
assert str(excinfo.value) == "Human received a non-compatible instance (object) when expecting Article"
def test_object_type():
object_type = schema.T(Human)
Human._meta.fields_map
assert Human._meta.interface is False
assert isinstance(object_type, GraphQLObjectType)
assert_equal_lists(
object_type.get_fields().keys(),
['headline', 'id', 'reporter', 'reporterId', 'pubDate']
)
assert schema.T(SQLAlchemyNode) in object_type.get_interfaces()
def test_node_notinterface():
assert Human._meta.interface is False
assert SQLAlchemyNode in Human._meta.interfaces

View File

@ -0,0 +1,25 @@
from graphene import Schema, ObjectType, String
from ..utils import get_session
def test_get_session():
session = 'My SQLAlchemy session'
schema = Schema(session=session)
class Query(ObjectType):
x = String()
def resolve_x(self, args, info):
return get_session(info)
query = '''
query ReporterQuery {
x
}
'''
schema = Schema(query=Query, session=session)
result = schema.execute(query)
assert not result.errors
assert result.data['x'] == session

View File

@ -0,0 +1,125 @@
import inspect
import six
from sqlalchemy.inspection import inspect as sqlalchemyinspect
from sqlalchemy.orm.exc import NoResultFound
from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
from ...relay.types import Connection, Node, NodeMeta
from .converter import (convert_sqlalchemy_column,
convert_sqlalchemy_relationship)
from .options import SQLAlchemyOptions
from .utils import is_mapped, get_query
class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
options_class = SQLAlchemyOptions
def construct_fields(cls):
only_fields = cls._meta.only_fields
exclude_fields = cls._meta.exclude_fields
already_created_fields = {f.attname for f in cls._meta.local_fields}
inspected_model = sqlalchemyinspect(cls._meta.model)
# Get all the columns for the relationships on the model
for relationship in inspected_model.relationships:
is_not_in_only = only_fields and relationship.key not in only_fields
is_already_created = relationship.key in already_created_fields
is_excluded = relationship.key in exclude_fields or is_already_created
if is_not_in_only or is_excluded:
# We skip this field if we specify only_fields and is not
# in there. Or when we excldue this field in exclude_fields
continue
converted_relationship = convert_sqlalchemy_relationship(relationship)
cls.add_to_class(relationship.key, converted_relationship)
for column in inspected_model.columns:
is_not_in_only = only_fields and column.name not in only_fields
is_already_created = column.name in already_created_fields
is_excluded = column.name in exclude_fields or is_already_created
if is_not_in_only or is_excluded:
# We skip this field if we specify only_fields and is not
# in there. Or when we excldue this field in exclude_fields
continue
converted_column = convert_sqlalchemy_column(column)
cls.add_to_class(column.name, converted_column)
def construct(cls, *args, **kwargs):
cls = super(SQLAlchemyObjectTypeMeta, cls).construct(*args, **kwargs)
if not cls._meta.abstract:
if not cls._meta.model:
raise Exception(
'SQLAlchemy ObjectType %s must have a model in the Meta class attr' %
cls)
elif not inspect.isclass(cls._meta.model) or not is_mapped(cls._meta.model):
raise Exception('Provided model in %s is not a SQLAlchemy model' % cls)
cls.construct_fields()
return cls
class InstanceObjectType(ObjectType):
class Meta:
abstract = True
def __init__(self, _root=None):
if _root:
assert isinstance(_root, self._meta.model), (
'{} received a non-compatible instance ({}) '
'when expecting {}'.format(
self.__class__.__name__,
_root.__class__.__name__,
self._meta.model.__name__
))
super(InstanceObjectType, self).__init__(_root=_root)
@property
def instance(self):
return self._root
@instance.setter
def instance(self, value):
self._root = value
def __getattr__(self, attr):
return getattr(self._root, attr)
class SQLAlchemyObjectType(six.with_metaclass(
SQLAlchemyObjectTypeMeta, InstanceObjectType)):
class Meta:
abstract = True
class SQLAlchemyConnection(Connection):
pass
class SQLAlchemyNodeMeta(SQLAlchemyObjectTypeMeta, NodeMeta):
pass
class NodeInstance(Node, InstanceObjectType):
class Meta:
abstract = True
class SQLAlchemyNode(six.with_metaclass(
SQLAlchemyNodeMeta, NodeInstance)):
class Meta:
abstract = True
@classmethod
def get_node(cls, id, info=None):
try:
model = cls._meta.model
query = get_query(model, info)
instance = query.filter(model.id == id).one()
return cls(instance)
except NoResultFound:
return None

View File

@ -0,0 +1,49 @@
from sqlalchemy.ext.declarative.api import DeclarativeMeta
from sqlalchemy.orm.query import Query
from graphene.utils import LazyList
def get_type_for_model(schema, model):
schema = schema
types = schema.types.values()
for _type in types:
type_model = hasattr(_type, '_meta') and getattr(
_type._meta, 'model', None)
if model == type_model:
return _type
def get_session(info):
schema = info.schema.graphene_schema
return schema.options.get('session')
def get_query(model, info):
query = getattr(model, 'query', None)
if not query:
session = get_session(info)
if not session:
raise Exception('A query in the model Base or a session in the schema is required for querying.\n'
'Read more http://graphene-python.org/docs/sqlalchemy/tips/#querying')
query = session.query(model)
return query
class WrappedQuery(LazyList):
def __len__(self):
# Dont calculate the length using len(query), as this will
# evaluate the whole queryset and return it's length.
# Use .count() instead
return self._origin.count()
def maybe_query(value):
if isinstance(value, Query):
return WrappedQuery(value)
return value
def is_mapped(obj):
return isinstance(obj, DeclarativeMeta)

View File

@ -26,7 +26,7 @@ class Schema(object):
_executor = None
def __init__(self, query=None, mutation=None, subscription=None,
name='Schema', executor=None, plugins=None, auto_camelcase=True):
name='Schema', executor=None, plugins=None, auto_camelcase=True, **options):
self._types_names = {}
self._types = {}
self.mutation = mutation
@ -38,6 +38,7 @@ class Schema(object):
if auto_camelcase:
plugins.append(CamelCase())
self.plugins = PluginManager(self, plugins)
self.options = options
signals.init_schema.send(self)
def __repr__(self):

View File

@ -62,6 +62,7 @@ setup(
'django-filter>=0.10.0',
'pytest>=2.7.2',
'pytest-django',
'sqlalchemy',
'mock',
],
extras_require={
@ -70,6 +71,10 @@ setup(
'singledispatch>=3.4.0.3',
'graphql-django-view>=1.1.0',
],
'sqlalchemy': [
'sqlalchemy',
'singledispatch>=3.4.0.3',
]
},
cmdclass={'test': PyTest},