mirror of
				https://github.com/graphql-python/graphene.git
				synced 2025-10-31 16:07:27 +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