Skip to content

Commit 017f6ae

Browse files
committed
Updated SQLAlchemy integration in graphene
1 parent 961cb1a commit 017f6ae

File tree

12 files changed

+160
-184
lines changed

12 files changed

+160
-184
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ install:
2525
if [ "$TEST_TYPE" = build ]; then
2626
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter
2727
pip install --download-cache $HOME/.cache/pip/ -e .[django]
28+
pip install --download-cache $HOME/.cache/pip/ -e .[sqlalchemy]
2829
pip install django==$DJANGO_VERSION
2930
python setup.py develop
3031
elif [ "$TEST_TYPE" = build_website ]; then
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from graphene.contrib.sqlalchemy.types import (
22
SQLAlchemyObjectType,
3-
SQLAlchemyInterface,
43
SQLAlchemyNode
54
)
65
from graphene.contrib.sqlalchemy.fields import (
76
SQLAlchemyConnectionField,
87
SQLAlchemyModelField
98
)
109

11-
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyInterface', 'SQLAlchemyNode',
10+
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyNode',
1211
'SQLAlchemyConnectionField', 'SQLAlchemyModelField']

graphene/contrib/sqlalchemy/converter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from singledispatch import singledispatch
2+
13
from sqlalchemy import types
24
from sqlalchemy.orm import interfaces
3-
from singledispatch import singledispatch
45

5-
from ...core.types.scalars import Boolean, Float, ID, Int, String
6+
from ...core.types.scalars import ID, Boolean, Float, Int, String
67
from .fields import ConnectionOrListField, SQLAlchemyModelField
78

89

graphene/contrib/sqlalchemy/fields.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
from sqlalchemy.orm import Query
21
from ...core.exceptions import SkipField
32
from ...core.fields import Field
43
from ...core.types.base import FieldType
54
from ...core.types.definitions import List
65
from ...relay import ConnectionField
76
from ...relay.utils import is_node
8-
from ...utils import LazyMap
9-
107
from .utils import get_type_for_model
118

129

1310
class SQLAlchemyConnectionField(ConnectionField):
1411

15-
def wrap_resolved(self, value, instance, args, info):
16-
if isinstance(value, Query):
17-
return LazyMap(value, self.type)
18-
return value
12+
def __init__(self, *args, **kwargs):
13+
self.session = kwargs.pop('session', None)
14+
return super(SQLAlchemyConnectionField, self).__init__(*args, **kwargs)
1915

16+
@property
17+
def model(self):
18+
return self.type._meta.model
2019

21-
class LazyListField(Field):
20+
def get_session(self, args, info):
21+
return self.session
2222

23-
def get_type(self, schema):
24-
return List(self.type)
23+
def get_query(self, resolved_query, args, info):
24+
self.get_session(args, info)
25+
return resolved_query
2526

26-
def resolver(self, instance, args, info):
27-
resolved = super(LazyListField, self).resolver(instance, args, info)
28-
return LazyMap(resolved, self.type)
27+
def from_list(self, connection_type, resolved, args, info):
28+
qs = self.get_query(resolved, args, info)
29+
return super(SQLAlchemyConnectionField, self).from_list(connection_type, qs, args, info)
2930

3031

3132
class ConnectionOrListField(Field):
@@ -38,7 +39,7 @@ def internal_type(self, schema):
3839
if is_node(field_object_type):
3940
field = SQLAlchemyConnectionField(field_object_type)
4041
else:
41-
field = LazyListField(field_object_type)
42+
field = Field(List(field_object_type))
4243
field.contribute_to_class(self.object_type, self.attname)
4344
return schema.T(field)
4445

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,23 @@
1-
import inspect
2-
3-
from sqlalchemy.ext.declarative.api import DeclarativeMeta
4-
5-
from ...core.options import Options
1+
from ...core.classtypes.objecttype import ObjectTypeOptions
62
from ...relay.types import Node
73
from ...relay.utils import is_node
84

95
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
106

117

12-
def is_base(cls):
13-
from graphene.contrib.sqlalchemy.types import SQLAlchemyObjectType
14-
return SQLAlchemyObjectType in cls.__bases__
15-
16-
17-
class SQLAlchemyOptions(Options):
8+
class SQLAlchemyOptions(ObjectTypeOptions):
189

1910
def __init__(self, *args, **kwargs):
20-
self.model = None
2111
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
12+
self.model = None
2213
self.valid_attrs += VALID_ATTRS
2314
self.only_fields = None
2415
self.exclude_fields = []
16+
self.filter_fields = None
17+
self.filter_order_by = None
2518

2619
def contribute_to_class(self, cls, name):
2720
super(SQLAlchemyOptions, self).contribute_to_class(cls, name)
2821
if is_node(cls):
29-
self.exclude_fields = list(self.exclude_fields)
22+
self.exclude_fields = list(self.exclude_fields) + ['id']
3023
self.interfaces.append(Node)
31-
if not is_node(cls) and not is_base(cls):
32-
return
33-
if not self.model:
34-
raise Exception(
35-
'SQLAlchemy ObjectType %s must have a model in the Meta class attr' % cls)
36-
elif not inspect.isclass(self.model) or not isinstance(self.model, DeclarativeMeta):
37-
raise Exception('Provided model in %s is not a SQLAlchemy model' % cls)

graphene/contrib/sqlalchemy/tests/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from __future__ import absolute_import
22

3-
from sqlalchemy import Table, Column, Integer, String, Date, ForeignKey
3+
from sqlalchemy import Column, Date, ForeignKey, Integer, String, Table
44
from sqlalchemy.ext.declarative import declarative_base
55
from sqlalchemy.orm import relationship
66

7-
87
Base = declarative_base()
98

109
association_table = Table('association', Base.metadata,
11-
Column('pet_id', Integer, ForeignKey('pets.id')),
12-
Column('reporter_id', Integer, ForeignKey('reporters.id')))
10+
Column('pet_id', Integer, ForeignKey('pets.id')),
11+
Column('reporter_id', Integer, ForeignKey('reporters.id')))
1312

1413

1514
class Pet(Base):

graphene/contrib/sqlalchemy/tests/test_converter.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from sqlalchemy import types, Column
21
from py.test import raises
32

43
import graphene
5-
from graphene.contrib.sqlalchemy.converter import convert_sqlalchemy_column, convert_sqlalchemy_relationship
6-
from graphene.contrib.sqlalchemy.fields import ConnectionOrListField, SQLAlchemyModelField
4+
from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column,
5+
convert_sqlalchemy_relationship)
6+
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
7+
SQLAlchemyModelField)
8+
from sqlalchemy import Column, types
79

8-
from .models import Article, Reporter, Pet
10+
from .models import Article, Pet, Reporter
911

1012

1113
def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
@@ -72,7 +74,7 @@ def test_should_integer_convert_id():
7274

7375

7476
def test_should_boolean_convert_boolean():
75-
field = assert_column_conversion(types.Boolean(), graphene.Boolean)
77+
assert_column_conversion(types.Boolean(), graphene.Boolean)
7678

7779

7880
def test_should_float_convert_float():

graphene/contrib/sqlalchemy/tests/test_query.py

Lines changed: 42 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,82 @@
1-
from py.test import raises
1+
import pytest
22

33
import graphene
4-
from graphene import relay
5-
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyObjectType
6-
from .models import Article, Reporter
4+
from graphene.contrib.sqlalchemy import SQLAlchemyObjectType
5+
from sqlalchemy import create_engine
6+
from sqlalchemy.orm import scoped_session, sessionmaker
77

8+
from .models import Base, Reporter
89

9-
def test_should_query_only_fields():
10-
with raises(Exception):
11-
class ReporterType(SQLAlchemyObjectType):
10+
db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
1211

13-
class Meta:
14-
model = Reporter
15-
only_fields = ('articles', )
1612

17-
schema = graphene.Schema(query=ReporterType)
18-
query = '''
19-
query ReporterQuery {
20-
articles
21-
}
22-
'''
23-
result = schema.execute(query)
24-
assert not result.errors
13+
@pytest.yield_fixture(scope='function')
14+
def session():
15+
connection = db.engine.connect()
16+
transaction = connection.begin()
17+
Base.metadata.create_all(connection)
2518

19+
# options = dict(bind=connection, binds={})
20+
session_factory = sessionmaker(bind=connection)
21+
session = scoped_session(session_factory)
2622

27-
def test_should_query_well():
28-
class ReporterType(SQLAlchemyObjectType):
23+
yield session
2924

30-
class Meta:
31-
model = Reporter
25+
# Finalize test here
26+
transaction.rollback()
27+
connection.close()
28+
session.remove()
3229

33-
class Query(graphene.ObjectType):
34-
reporter = graphene.Field(ReporterType)
3530

36-
def resolve_reporter(self, *args, **kwargs):
37-
return ReporterType(Reporter(first_name='ABA', last_name='X'))
31+
def setup_fixtures(session):
32+
reporter = Reporter(first_name='ABA', last_name='X')
33+
session.add(reporter)
34+
reporter2 = Reporter(first_name='ABO', last_name='Y')
35+
session.add(reporter2)
36+
session.commit()
3837

39-
query = '''
40-
query ReporterQuery {
41-
reporter {
42-
firstName,
43-
lastName,
44-
email
45-
}
46-
}
47-
'''
48-
expected = {
49-
'reporter': {
50-
'firstName': 'ABA',
51-
'lastName': 'X',
52-
'email': None
53-
}
54-
}
55-
schema = graphene.Schema(query=Query)
56-
result = schema.execute(query)
57-
assert not result.errors
58-
assert result.data == expected
5938

39+
def test_should_query_well(session):
40+
setup_fixtures(session)
6041

61-
def test_should_node():
62-
class ReporterNode(SQLAlchemyNode):
42+
class ReporterType(SQLAlchemyObjectType):
6343

6444
class Meta:
6545
model = Reporter
66-
exclude_fields = ('id', )
67-
68-
@classmethod
69-
def get_node(cls, id, info):
70-
return ReporterNode(Reporter(id=2, first_name='Cookie Monster'))
71-
72-
def resolve_articles(self, *args, **kwargs):
73-
return [ArticleNode(Article(headline='Hi!'))]
74-
75-
class ArticleNode(SQLAlchemyNode):
76-
77-
class Meta:
78-
model = Article
79-
exclude_fields = ('id', )
80-
81-
@classmethod
82-
def get_node(cls, id, info):
83-
return ArticleNode(Article(id=1, headline='Article node'))
8446

8547
class Query(graphene.ObjectType):
86-
node = relay.NodeField()
87-
reporter = graphene.Field(ReporterNode)
88-
article = graphene.Field(ArticleNode)
48+
reporter = graphene.Field(ReporterType)
49+
reporters = ReporterType.List()
8950

9051
def resolve_reporter(self, *args, **kwargs):
91-
return ReporterNode(Reporter(id=1, first_name='ABA', last_name='X'))
52+
return session.query(Reporter).first()
53+
54+
def resolve_reporters(self, *args, **kwargs):
55+
return session.query(Reporter)
9256

9357
query = '''
9458
query ReporterQuery {
9559
reporter {
96-
id,
9760
firstName,
98-
articles {
99-
edges {
100-
node {
101-
headline
102-
}
103-
}
104-
}
10561
lastName,
10662
email
10763
}
108-
myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") {
109-
id
110-
... on ReporterNode {
111-
firstName
112-
}
113-
... on ArticleNode {
114-
headline
115-
}
64+
reporters {
65+
firstName
11666
}
11767
}
11868
'''
11969
expected = {
12070
'reporter': {
121-
'id': 'UmVwb3J0ZXJOb2RlOjE=',
12271
'firstName': 'ABA',
12372
'lastName': 'X',
124-
'email': None,
125-
'articles': {
126-
'edges': [{
127-
'node': {
128-
'headline': 'Hi!'
129-
}
130-
}]
131-
},
73+
'email': None
13274
},
133-
'myArticle': {
134-
'id': 'QXJ0aWNsZU5vZGU6MQ==',
135-
'headline': 'Article node'
136-
}
75+
'reporters': [{
76+
'firstName': 'ABA',
77+
}, {
78+
'firstName': 'ABO',
79+
}]
13780
}
13881
schema = graphene.Schema(query=Query)
13982
result = schema.execute(query)

0 commit comments

Comments
 (0)