Skip to content

Commit 4875432

Browse files
author
Jeremy Feinstein
committed
Adding support for sqlalchemy
1 parent b1e0c3b commit 4875432

File tree

7 files changed

+259
-0
lines changed

7 files changed

+259
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from graphene.contrib.sqlalchemy.types import (
2+
SQLAlchemyObjectType,
3+
SQLAlchemyInterface,
4+
SQLAlchemyNode
5+
)
6+
from graphene.contrib.sqlalchemy.fields import (
7+
SQLAlchemyConnectionField,
8+
SQLAlchemyModelField
9+
)
10+
11+
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyInterface', 'SQLAlchemyNode',
12+
'SQLAlchemyConnectionField', 'SQLAlchemyModelField']
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from sqlalchemy import types
2+
from sqlalchemy.orm import interfaces
3+
from singledispatch import singledispatch
4+
5+
from graphene.contrib.sqlalchemy.fields import ConnectionOrListField, SQLAlchemyModelField
6+
from graphene.core.fields import BooleanField, FloatField, IDField, IntField, StringField
7+
8+
9+
def convert_sqlalchemy_relationship(relationship):
10+
model_field = SQLAlchemyModelField(field.table, description=relationship.key)
11+
if relationship.direction == interfaces.ONETOMANY:
12+
return model_field
13+
elif (relationship.direction == interfaces.MANYTOONE or
14+
relationship.direction == interfaces.MANYTOMANY):
15+
return ConnectionOrListField(model_field)
16+
17+
18+
def convert_sqlalchemy_column(column):
19+
try:
20+
return convert_sqlalchemy_type(column.type, column)
21+
except Exception:
22+
raise
23+
24+
25+
@singledispatch
26+
def convert_sqlalchemy_type():
27+
raise Exception(
28+
"Don't know how to convert the SQLAlchemy column %s (%s)" % (column, column.__class__))
29+
30+
31+
@convert_sqlalchemy_type.register(types.Date)
32+
@convert_sqlalchemy_type.register(types.DateTime)
33+
@convert_sqlalchemy_type.register(types.Time)
34+
@convert_sqlalchemy_type.register(types.Text)
35+
@convert_sqlalchemy_type.register(types.String)
36+
@convert_sqlalchemy_type.register(types.Unicode)
37+
@convert_sqlalchemy_type.register(types.UnicodeText)
38+
@convert_sqlalchemy_type.register(types.Enum)
39+
def convert_column_to_string(type, column):
40+
return StringField(description=column.description)
41+
42+
43+
@convert_sqlalchemy_type.register(types.SmallInteger)
44+
@convert_sqlalchemy_type.register(types.BigInteger)
45+
@convert_sqlalchemy_type.register(types.Integer)
46+
def convert_column_to_int_or_id(column):
47+
if column.primary_key:
48+
return IDField(description=column.description)
49+
else:
50+
return IntField(description=column.description)
51+
52+
53+
@convert_sqlalchemy_type.register(types.Boolean)
54+
def convert_column_to_boolean(column):
55+
return BooleanField(description=column.description)
56+
57+
58+
@convert_sqlalchemy_type.register(types.Float)
59+
@convert_sqlalchemy_type.register(types.Numeric)
60+
def convert_column_to_float(column):
61+
return FloatField(description=column.description)

graphene/contrib/sqlalchemy/fields.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from graphene import relay
2+
from graphene.contrib.sqlalchemy.utils import get_type_for_model, lazy_map
3+
from graphene.core.fields import Field, LazyField, ListField
4+
from graphene.relay.utils import is_node
5+
6+
7+
class SQLAlchemyConnectionField(relay.ConnectionField):
8+
9+
def wrap_resolved(self, value, instance, args, info):
10+
schema = info.schema.graphene_schema
11+
return lazy_map(value, self.get_object_type(schema))
12+
13+
14+
class LazyListField(ListField):
15+
16+
def resolve(self, instance, args, info):
17+
schema = info.schema.graphene_schema
18+
resolved = super(LazyListField, self).resolve(instance, args, info)
19+
return lazy_map(resolved, self.get_object_type(schema))
20+
21+
22+
class ConnectionOrListField(LazyField):
23+
24+
def get_field(self, schema):
25+
model_field = self.field_type
26+
field_object_type = model_field.get_object_type(schema)
27+
if is_node(field_object_type):
28+
field = SQLAlchemyConnectionField(model_field)
29+
else:
30+
field = LazyListField(model_field)
31+
field.contribute_to_class(self.object_type, self.name)
32+
return field
33+
34+
35+
class SQLAlchemyModelField(Field):
36+
37+
def __init__(self, model, *args, **kwargs):
38+
super(SQLAlchemyModelField, self).__init__(None, *args, **kwargs)
39+
self.model = model
40+
41+
def resolve(self, instance, args, info):
42+
resolved = super(SQLAlchemyModelField, self).resolve(instance, args, info)
43+
schema = info.schema.graphene_schema
44+
_type = self.get_object_type(schema)
45+
assert _type, ("Field %s cannot be retrieved as the "
46+
"ObjectType is not registered by the schema" % (
47+
self.attname
48+
))
49+
return _type(resolved)
50+
51+
def internal_type(self, schema):
52+
_type = self.get_object_type(schema)
53+
if not _type and self.object_type._meta.only_fields:
54+
raise Exception(
55+
"Model %r is not accessible by the schema. "
56+
"You can either register the type manually "
57+
"using @schema.register. "
58+
"Or disable the field %s in %s" % (
59+
self.model,
60+
self.attname,
61+
self.object_type
62+
)
63+
)
64+
return schema.T(_type) or Field.SKIP
65+
66+
def get_object_type(self, schema):
67+
return get_type_for_model(schema, self.model)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import inspect
2+
3+
from sqlalchemy import Table
4+
5+
from graphene.core.options import Options
6+
from graphene.relay.types import Node
7+
from graphene.relay.utils import is_node
8+
9+
VALID_ATTRS = ('table', 'only_columns', 'exclude_columns')
10+
11+
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):
18+
19+
def __init__(self, *args, **kwargs):
20+
self.table = None
21+
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
22+
self.valid_attrs += VALID_ATTRS
23+
self.only_fields = None
24+
self.exclude_fields = []
25+
26+
def contribute_to_class(self, cls, name):
27+
super(SQLAlchemyOptions, self).contribute_to_class(cls, name)
28+
if is_node(cls):
29+
self.exclude_fields = list(self.exclude_fields)
30+
self.interfaces.append(Node)
31+
if not is_node(cls) and not is_base(cls):
32+
return
33+
if not self.table:
34+
raise Exception(
35+
'SQLAlchemy ObjectType %s must have a table in the Meta class attr' % cls)
36+
elif not inspect.isclass(self.table) or not issubclass(self.table, Table):
37+
raise Exception('Provided table in %s is not a SQLAlchemy table' % cls)

graphene/contrib/sqlalchemy/types.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import six
2+
from sqlalchemy.inspection import inspect
3+
4+
from graphene.contrib.sqlalchemy.converter import convert_sqlalchemy_column,
5+
convert_sqlalchemy_relationship
6+
from graphene.contrib.sqlalchemy.options import SQLAlchemyOptions
7+
from graphene.contrib.sqlalchemy.utils import get_reverse_columns
8+
from graphene.core.types import BaseObjectType, ObjectTypeMeta
9+
from graphene.relay.fields import GlobalIDField
10+
from graphene.relay.types import BaseNode
11+
12+
13+
class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
14+
options_cls = SQLAlchemyOptions
15+
16+
def is_interface(cls, parents):
17+
return SQLAlchemyInterface in parents
18+
19+
def add_extra_fields(cls):
20+
if not cls._meta.table:
21+
return
22+
inspected_table = inspect(cls._meta.table)
23+
# Get all the columns for the relationships on the table
24+
for relationship in inspected_table.relationships:
25+
converted_relationship = convert_sqlalchemy_relationship(relationship)
26+
cls.add_to_class(relationship.key, converted_relationship)
27+
for column in inspected_table.columns:
28+
converted_column = convert_sqlalchemy_column(column)
29+
cls.add_to_class(column.name, converted_column)
30+
31+
32+
class InstanceObjectType(BaseObjectType):
33+
34+
def __init__(self, instance=None):
35+
self.instance = instance
36+
super(InstanceObjectType, self).__init__()
37+
38+
def __getattr__(self, attr):
39+
return getattr(self.instance, attr)
40+
41+
42+
class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, InstanceObjectType)):
43+
pass
44+
45+
46+
class SQLAlchemyInterface(six.with_metaclass(SQLAlchemyObjectTypeMeta, InstanceObjectType)):
47+
pass
48+
49+
50+
class SQLAlchemyNode(BaseNode, SQLAlchemyInterface):
51+
id = GlobalIDField()
52+
53+
@classmethod
54+
def get_node(cls, id):
55+
instance = cls._meta.table.objects.filter(id=id).first()
56+
return cls(instance)

graphene/contrib/sqlalchemy/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.db import models
2+
from django.db.models.manager import Manager
3+
from django.db.models.query import QuerySet
4+
5+
from graphene.utils import LazyMap
6+
7+
8+
def get_type_for_model(schema, model):
9+
schema = schema
10+
types = schema.types.values()
11+
for _type in types:
12+
type_model = hasattr(_type, '_meta') and getattr(
13+
_type._meta, 'model', None)
14+
if model == type_model:
15+
return _type
16+
17+
18+
def lazy_map(value, func):
19+
if isinstance(value, Manager):
20+
value = value.get_queryset()
21+
if isinstance(value, QuerySet):
22+
return LazyMap(value, func)
23+
return value

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def run_tests(self):
7070
'singledispatch>=3.4.0.3',
7171
'graphql-django-view>=1.0.0',
7272
],
73+
'sqlalchemy': [
74+
'SQLAlchemy'
75+
]
7376
},
7477

7578
cmdclass={'test': PyTest},

0 commit comments

Comments
 (0)