mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-09 08:00:39 +03:00
Added initial basic SQLAlchemy example
This commit is contained in:
parent
4cadf33b4f
commit
79d7636ab6
10
graphene-sqlalchemy/graphene_sqlalchemy/__init__.py
Normal file
10
graphene-sqlalchemy/graphene_sqlalchemy/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from graphene.contrib.sqlalchemy.types import (
|
||||||
|
SQLAlchemyObjectType,
|
||||||
|
SQLAlchemyNode
|
||||||
|
)
|
||||||
|
from graphene.contrib.sqlalchemy.fields import (
|
||||||
|
SQLAlchemyConnectionField
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyNode',
|
||||||
|
'SQLAlchemyConnectionField']
|
89
graphene-sqlalchemy/graphene_sqlalchemy/converter.py
Normal file
89
graphene-sqlalchemy/graphene_sqlalchemy/converter.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
from singledispatch import singledispatch
|
||||||
|
from sqlalchemy import types
|
||||||
|
from sqlalchemy.orm import interfaces
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from graphene import Enum, ID, Boolean, Float, Int, String, List
|
||||||
|
from graphene.types.json import JSONString
|
||||||
|
from .fields import ConnectionOrListField, SQLAlchemyModelField
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sqlalchemy_utils.types.choice import ChoiceType
|
||||||
|
except ImportError:
|
||||||
|
class ChoiceType(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.ENUM)
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.UUID)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_sqlalchemy_type.register(ChoiceType)
|
||||||
|
def convert_column_to_enum(type, column):
|
||||||
|
name = '{}_{}'.format(column.table.name, column.name).upper()
|
||||||
|
return Enum(name, type.choices, description=column.doc)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.ARRAY)
|
||||||
|
def convert_postgres_array_to_list(type, column):
|
||||||
|
graphene_type = convert_sqlalchemy_type(column.type.item_type, column)
|
||||||
|
return List(graphene_type, description=column.doc)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.HSTORE)
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.JSON)
|
||||||
|
@convert_sqlalchemy_type.register(postgresql.JSONB)
|
||||||
|
def convert_json_to_string(type, column):
|
||||||
|
return JSONString(description=column.doc)
|
69
graphene-sqlalchemy/graphene_sqlalchemy/fields.py
Normal file
69
graphene-sqlalchemy/graphene_sqlalchemy/fields.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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_query, get_type_for_model, maybe_query
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultQuery(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SQLAlchemyConnectionField(ConnectionField):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['default'] = kwargs.pop('default', lambda: DefaultQuery)
|
||||||
|
return super(SQLAlchemyConnectionField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
return self.type._meta.model
|
||||||
|
|
||||||
|
def from_list(self, connection_type, resolved, args, context, info):
|
||||||
|
if resolved is DefaultQuery:
|
||||||
|
resolved = get_query(self.model, info)
|
||||||
|
query = maybe_query(resolved)
|
||||||
|
return super(SQLAlchemyConnectionField, self).from_list(connection_type, query, args, context, 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)
|
24
graphene-sqlalchemy/graphene_sqlalchemy/options.py
Normal file
24
graphene-sqlalchemy/graphene_sqlalchemy/options.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from ...core.classtypes.objecttype import ObjectTypeOptions
|
||||||
|
from ...relay.types import Node
|
||||||
|
from ...relay.utils import is_node
|
||||||
|
|
||||||
|
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields', 'identifier')
|
||||||
|
|
||||||
|
|
||||||
|
class SQLAlchemyOptions(ObjectTypeOptions):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
|
||||||
|
self.model = None
|
||||||
|
self.identifier = "id"
|
||||||
|
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)
|
42
graphene-sqlalchemy/graphene_sqlalchemy/tests/models.py
Normal file
42
graphene-sqlalchemy/graphene_sqlalchemy/tests/models.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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 Editor(Base):
|
||||||
|
__tablename__ = 'editors'
|
||||||
|
editor_id = Column(Integer(), primary_key=True)
|
||||||
|
name = Column(String(100))
|
||||||
|
|
||||||
|
|
||||||
|
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'))
|
150
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_converter.py
Normal file
150
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_converter.py
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
from py.test import raises
|
||||||
|
from sqlalchemy import Column, Table, types
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy_utils.types.choice import ChoiceType
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene.core.types.custom_scalars import JSONString
|
||||||
|
from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column,
|
||||||
|
convert_sqlalchemy_relationship)
|
||||||
|
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
|
||||||
|
SQLAlchemyModelField)
|
||||||
|
|
||||||
|
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_choice_convert_enum():
|
||||||
|
TYPES = [
|
||||||
|
(u'es', u'Spanish'),
|
||||||
|
(u'en', u'English')
|
||||||
|
]
|
||||||
|
column = Column(ChoiceType(TYPES), doc='Language', name='language')
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
Table('translatedmodel', Base.metadata, column)
|
||||||
|
graphene_type = convert_sqlalchemy_column(column)
|
||||||
|
assert issubclass(graphene_type, graphene.Enum)
|
||||||
|
assert graphene_type._meta.type_name == 'TRANSLATEDMODEL_LANGUAGE'
|
||||||
|
assert graphene_type._meta.description == 'Language'
|
||||||
|
assert graphene_type.__enum__.__members__['es'].value == 'Spanish'
|
||||||
|
assert graphene_type.__enum__.__members__['en'].value == 'English'
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_uuid_convert():
|
||||||
|
assert_column_conversion(postgresql.UUID(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_enum_convert():
|
||||||
|
assert_column_conversion(postgresql.ENUM(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_array_convert():
|
||||||
|
assert_column_conversion(postgresql.ARRAY(types.Integer), graphene.List)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_json_convert():
|
||||||
|
assert_column_conversion(postgresql.JSON(), JSONString)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_jsonb_convert():
|
||||||
|
assert_column_conversion(postgresql.JSONB(), JSONString)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_postgresql_hstore_convert():
|
||||||
|
assert_column_conversion(postgresql.HSTORE(), JSONString)
|
239
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_query.py
Normal file
239
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_query.py
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
from graphene.contrib.sqlalchemy import (SQLAlchemyConnectionField,
|
||||||
|
SQLAlchemyNode, SQLAlchemyObjectType)
|
||||||
|
|
||||||
|
from .models import Article, Base, Editor, Reporter
|
||||||
|
|
||||||
|
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)
|
||||||
|
editor = Editor(name="John")
|
||||||
|
session.add(editor)
|
||||||
|
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)
|
||||||
|
all_articles = SQLAlchemyConnectionField(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
|
||||||
|
}
|
||||||
|
allArticles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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!'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'allArticles': {
|
||||||
|
'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
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_custom_identifier(session):
|
||||||
|
setup_fixtures(session)
|
||||||
|
|
||||||
|
class EditorNode(SQLAlchemyNode):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Editor
|
||||||
|
identifier = "editor_id"
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
node = relay.NodeField(EditorNode)
|
||||||
|
all_editors = SQLAlchemyConnectionField(EditorNode)
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query EditorQuery {
|
||||||
|
allEditors {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
node(id: "RWRpdG9yTm9kZTox") {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'allEditors': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'RWRpdG9yTm9kZTox',
|
||||||
|
'name': 'John'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
'node': {
|
||||||
|
'name': 'John'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query, session=session)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
45
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_schema.py
Normal file
45
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_schema.py
Normal 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']
|
||||||
|
)
|
102
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_types.py
Normal file
102
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_types.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
from graphql.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
|
25
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_utils.py
Normal file
25
graphene-sqlalchemy/graphene_sqlalchemy/tests/test_utils.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from graphene import ObjectType, Schema, 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
|
125
graphene-sqlalchemy/graphene_sqlalchemy/types.py
Normal file
125
graphene-sqlalchemy/graphene_sqlalchemy/types.py
Normal 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 get_query, is_mapped
|
||||||
|
|
||||||
|
|
||||||
|
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 name, column in inspected_model.columns.items():
|
||||||
|
is_not_in_only = only_fields and name not in only_fields
|
||||||
|
is_already_created = name in already_created_fields
|
||||||
|
is_excluded = 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(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):
|
||||||
|
super(InstanceObjectType, self).__init__(_root=_root)
|
||||||
|
assert not self._root or isinstance(self._root, self._meta.model), (
|
||||||
|
'{} received a non-compatible instance ({}) '
|
||||||
|
'when expecting {}'.format(
|
||||||
|
self.__class__.__name__,
|
||||||
|
self._root.__class__.__name__,
|
||||||
|
self._meta.model.__name__
|
||||||
|
))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance(self):
|
||||||
|
return self._root
|
||||||
|
|
||||||
|
@instance.setter
|
||||||
|
def instance(self, value):
|
||||||
|
self._root = value
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def to_global_id(self):
|
||||||
|
id_ = getattr(self.instance, self._meta.identifier)
|
||||||
|
return self.global_id(id_)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id, info=None):
|
||||||
|
try:
|
||||||
|
model = cls._meta.model
|
||||||
|
identifier = cls._meta.identifier
|
||||||
|
query = get_query(model, info)
|
||||||
|
instance = query.filter(getattr(model, identifier) == id).one()
|
||||||
|
return cls(instance)
|
||||||
|
except NoResultFound:
|
||||||
|
return None
|
49
graphene-sqlalchemy/graphene_sqlalchemy/utils.py
Normal file
49
graphene-sqlalchemy/graphene_sqlalchemy/utils.py
Normal 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)
|
43
graphene-sqlalchemy/setup.py
Normal file
43
graphene-sqlalchemy/setup.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='graphene-sqlalchemy',
|
||||||
|
version='1.0',
|
||||||
|
|
||||||
|
description='Graphene SQLAlchemy integration',
|
||||||
|
# long_description=open('README.rst').read(),
|
||||||
|
|
||||||
|
url='https://github.com/graphql-python/graphene-sqlalchemy',
|
||||||
|
|
||||||
|
author='Syrus Akbary',
|
||||||
|
author_email='me@syrusakbary.com',
|
||||||
|
|
||||||
|
license='MIT',
|
||||||
|
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Topic :: Software Development :: Libraries',
|
||||||
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: Implementation :: PyPy',
|
||||||
|
],
|
||||||
|
|
||||||
|
keywords='api graphql protocol rest relay graphene',
|
||||||
|
|
||||||
|
packages=find_packages(exclude=['tests']),
|
||||||
|
|
||||||
|
install_requires=[
|
||||||
|
'six>=1.10.0',
|
||||||
|
'graphene>=1.0',
|
||||||
|
'singledispatch>=3.4.0.3',
|
||||||
|
],
|
||||||
|
tests_require=[
|
||||||
|
'pytest>=2.7.2',
|
||||||
|
'mock',
|
||||||
|
],
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user