Skip to content

Commit ea22b37

Browse files
authored
Merge branch 'master' into master
2 parents b7de36d + 8e84f39 commit ea22b37

File tree

11 files changed

+100
-41
lines changed

11 files changed

+100
-41
lines changed

.travis.yml

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ python:
66
- 3.5
77
- pypy
88
before_install:
9-
- |
10-
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
11-
export PYENV_ROOT="$HOME/.pyenv"
12-
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
13-
cd "$PYENV_ROOT" && git pull
14-
else
15-
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
16-
fi
17-
export PYPY_VERSION="4.0.1"
18-
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
19-
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
20-
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
21-
fi
9+
- |
10+
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
11+
export PYENV_ROOT="$HOME/.pyenv"
12+
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
13+
cd "$PYENV_ROOT" && git pull
14+
else
15+
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
16+
fi
17+
export PYPY_VERSION="4.0.1"
18+
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
19+
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
20+
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
21+
fi
2222
install:
2323
- |
2424
if [ "$TEST_TYPE" = build ]; then
@@ -50,3 +50,10 @@ matrix:
5050
include:
5151
- python: '2.7'
5252
env: TEST_TYPE=lint
53+
deploy:
54+
provider: pypi
55+
user: syrusakbary
56+
on:
57+
tags: true
58+
password:
59+
secure: q0ey31cWljGB30l43aEd1KIPuAHRutzmsd2lBb/2zvD79ReBrzvCdFAkH2xcyo4Volk3aazQQTNUIurnTuvBxmtqja0e+gUaO5LdOcokVdOGyLABXh7qhd2kdvbTDWgSwA4EWneLGXn/SjXSe0f3pCcrwc6WDcLAHxtffMvO9gulpYQtUoOqXfMipMOkRD9iDWTJBsSo3trL70X1FHOVr6Yqi0mfkX2Y/imxn6wlTWRz28Ru94xrj27OmUnCv7qcG0taO8LNlUCquNFAr2sZ+l+U/GkQrrM1y+ehPz3pmI0cCCd7SX/7+EG9ViZ07BZ31nk4pgnqjmj3nFwqnCE/4IApGnduqtrMDF63C9TnB1TU8oJmbbUCu4ODwRpBPZMnwzaHsLnrpdrB89/98NtTfujdrh3U5bVB+t33yxrXVh+FjgLYj9PVeDixpFDn6V/Xcnv4BbRMNOhXIQT7a7/5b99RiXBjCk6KRu+Jdu5DZ+3G4Nbr4oim3kZFPUHa555qbzTlwAfkrQxKv3C3OdVJR7eGc9ADsbHyEJbdPNAh/T+xblXTXLS3hPYDvgM+WEGy3CytBDG3JVcXm25ZP96EDWjweJ7MyfylubhuKj/iR1Y1wiHeIsYq9CqRrFQUWL8gFJBfmgjs96xRXXXnvyLtKUKpKw3wFg5cR/6FnLeYZ8k=

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Here is a simple SQLAlchemy model:
2222

2323
```python
2424
from sqlalchemy import Column, Integer, String
25-
from sqlalchemy.orm import backref, relationship
25+
from sqlalchemy.orm import relationship
2626

2727
from sqlalchemy.ext.declarative import declarative_base
2828

docs/tutorial.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ Create ``flask_sqlalchemy/schema.py`` and type the following:
9595
from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
9696
from models import db_session, Department as DepartmentModel, Employee as EmployeeModel
9797
98-
schema = graphene.Schema()
99-
10098
10199
class Department(SQLAlchemyObjectType):
102100
class Meta:
@@ -114,7 +112,7 @@ Create ``flask_sqlalchemy/schema.py`` and type the following:
114112
node = relay.Node.Field()
115113
all_employees = SQLAlchemyConnectionField(Employee)
116114
117-
schema.query = Query
115+
schema = graphene.Schema(query=Query)
118116
119117
Creating GraphQL and GraphiQL views in Flask
120118
--------------------------------------------

examples/flask_sqlalchemy/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ whole Graphene repository:
1313

1414
```bash
1515
# Get the example project code
16-
git clone https://github.com/graphql-python/graphene.git
17-
cd graphene/examples/flask_sqlalchemy
16+
git clone https://github.com/graphql-python/graphene-sqlalchemy.git
17+
cd graphene-sqlalchemy/examples/flask_sqlalchemy
1818
```
1919

2020
It is good idea (but not required) to create a virtual environment
@@ -46,5 +46,5 @@ Now the following command will setup the database, and start the server:
4646

4747

4848
Now head on over to
49-
[http://127.0.0.1:5000/graphiql](http://127.0.0.1:5000/graphiql)
49+
[http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql)
5050
and run some queries!

examples/flask_sqlalchemy/app.py

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/usr/bin/env python
2+
13
from flask import Flask
24

35
from database import db_session, init_db

graphene_sqlalchemy/converter.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .fields import SQLAlchemyConnectionField
1212

1313
try:
14-
from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType
14+
from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType
1515
except ImportError:
1616
class ChoiceType(object):
1717
pass
@@ -23,6 +23,14 @@ class JSONType(object):
2323
pass
2424

2525

26+
def get_column_doc(column):
27+
return getattr(column, 'doc', None)
28+
29+
30+
def is_column_nullable(column):
31+
return bool(getattr(column, 'nullable', True))
32+
33+
2634
def convert_sqlalchemy_relationship(relationship, registry):
2735
direction = relationship.direction
2836
model = relationship.mapper.entity
@@ -40,7 +48,10 @@ def dynamic_type():
4048
return Field(List(_type))
4149

4250
return Dynamic(dynamic_type)
43-
51+
52+
def convert_sqlalchemy_hybrid_method(hybrid_item):
53+
return String(description=getattr(hybrid_item, '__doc__', None),
54+
required=False)
4455

4556
def convert_sqlalchemy_composite(composite, registry):
4657
converter = registry.get_converter_for_composite(composite.composite_class)
@@ -66,6 +77,7 @@ def inner(fn):
6677
registry.register_composite_converter(cls, fn)
6778
return inner
6879

80+
6981
convert_sqlalchemy_composite.register = _register_composite_class
7082

7183

@@ -80,7 +92,6 @@ def convert_sqlalchemy_type(type, column, registry=None):
8092

8193

8294
@convert_sqlalchemy_type.register(types.Date)
83-
@convert_sqlalchemy_type.register(types.DateTime)
8495
@convert_sqlalchemy_type.register(types.Time)
8596
@convert_sqlalchemy_type.register(types.String)
8697
@convert_sqlalchemy_type.register(types.Text)
@@ -89,56 +100,66 @@ def convert_sqlalchemy_type(type, column, registry=None):
89100
@convert_sqlalchemy_type.register(types.Enum)
90101
@convert_sqlalchemy_type.register(postgresql.ENUM)
91102
@convert_sqlalchemy_type.register(postgresql.UUID)
103+
@convert_sqlalchemy_type.register(TSVectorType)
92104
def convert_column_to_string(type, column, registry=None):
93-
return String(description=getattr(column, 'doc', None),
94-
required=not(getattr(column, 'nullable', True)))
105+
return String(description=get_column_doc(column),
106+
required=not(is_column_nullable(column)))
107+
108+
109+
@convert_sqlalchemy_type.register(types.DateTime)
110+
def convert_column_to_datetime(type, column, registry=None):
111+
from graphene.types.datetime import DateTime
112+
return DateTime(description=get_column_doc(column),
113+
required=not(is_column_nullable(column)))
95114

96115

97116
@convert_sqlalchemy_type.register(types.SmallInteger)
98-
@convert_sqlalchemy_type.register(types.BigInteger)
99117
@convert_sqlalchemy_type.register(types.Integer)
100118
def convert_column_to_int_or_id(type, column, registry=None):
101119
if column.primary_key:
102-
return ID(description=column.doc, required=not(column.nullable))
120+
return ID(description=get_column_doc(column), required=not (is_column_nullable(column)))
103121
else:
104-
return Int(description=column.doc, required=not(column.nullable))
122+
return Int(description=get_column_doc(column),
123+
required=not (is_column_nullable(column)))
105124

106125

107126
@convert_sqlalchemy_type.register(types.Boolean)
108127
def convert_column_to_boolean(type, column, registry=None):
109-
return Boolean(description=column.doc, required=not(column.nullable))
128+
return Boolean(description=get_column_doc(column), required=not(is_column_nullable(column)))
110129

111130

112131
@convert_sqlalchemy_type.register(types.Float)
113132
@convert_sqlalchemy_type.register(types.Numeric)
133+
@convert_sqlalchemy_type.register(types.BigInteger)
114134
def convert_column_to_float(type, column, registry=None):
115-
return Float(description=column.doc, required=not(column.nullable))
135+
return Float(description=get_column_doc(column), required=not(is_column_nullable(column)))
116136

117137

118138
@convert_sqlalchemy_type.register(ChoiceType)
119139
def convert_column_to_enum(type, column, registry=None):
120140
name = '{}_{}'.format(column.table.name, column.name).upper()
121-
return Enum(name, type.choices, description=column.doc)
141+
return Enum(name, type.choices, description=get_column_doc(column))
122142

123143

124144
@convert_sqlalchemy_type.register(ScalarListType)
125145
def convert_scalar_list_to_list(type, column, registry=None):
126-
return List(String, description=column.doc)
146+
return List(String, description=get_column_doc(column))
127147

128148

129149
@convert_sqlalchemy_type.register(postgresql.ARRAY)
130-
def convert_postgres_array_to_list(type, column, registry=None):
150+
def convert_postgres_array_to_list(_type, column, registry=None):
131151
graphene_type = convert_sqlalchemy_type(column.type.item_type, column)
132-
return List(graphene_type, description=column.doc, required=not(column.nullable))
152+
inner_type = type(graphene_type)
153+
return List(inner_type, description=get_column_doc(column), required=not(is_column_nullable(column)))
133154

134155

135156
@convert_sqlalchemy_type.register(postgresql.HSTORE)
136157
@convert_sqlalchemy_type.register(postgresql.JSON)
137158
@convert_sqlalchemy_type.register(postgresql.JSONB)
138159
def convert_json_to_string(type, column, registry=None):
139-
return JSONString(description=column.doc, required=not(column.nullable))
160+
return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column)))
140161

141162

142163
@convert_sqlalchemy_type.register(JSONType)
143164
def convert_json_type_to_string(type, column, registry=None):
144-
return JSONString(description=column.doc, required=not(column.nullable))
165+
return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column)))

graphene_sqlalchemy/fields.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def connection_resolver(cls, resolver, connection, model, root, args, context, i
3939
edge_type=connection.Edge,
4040
)
4141
connection.iterable = iterable
42+
connection.length = _len
4243
return connection
4344

4445
def get_resolver(self, parent_resolver):

graphene_sqlalchemy/tests/test_converter.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import graphene
1010
from graphene.relay import Node
11+
from graphene.types.datetime import DateTime
1112
from graphene.types.json import JSONString
1213

1314
from ..converter import (convert_sqlalchemy_column,
@@ -52,7 +53,7 @@ def test_should_date_convert_string():
5253

5354

5455
def test_should_datetime_convert_string():
55-
assert_column_conversion(types.DateTime(), graphene.String)
56+
assert_column_conversion(types.DateTime(), DateTime)
5657

5758

5859
def test_should_time_convert_string():
@@ -84,7 +85,7 @@ def test_should_small_integer_convert_int():
8485

8586

8687
def test_should_big_integer_convert_int():
87-
assert_column_conversion(types.BigInteger(), graphene.Int)
88+
assert_column_conversion(types.BigInteger(), graphene.Float)
8889

8990

9091
def test_should_integer_convert_int():
@@ -113,6 +114,11 @@ def test_should_label_convert_string():
113114
assert isinstance(graphene_type, graphene.String)
114115

115116

117+
def test_should_label_convert_int():
118+
label = Label('int_label_test', case([], else_="foo"), type_=types.Integer())
119+
graphene_type = convert_sqlalchemy_column(label)
120+
assert isinstance(graphene_type, graphene.Int)
121+
116122
def test_should_choice_convert_enum():
117123
TYPES = [
118124
(u'es', u'Spanish'),

graphene_sqlalchemy/tests/test_query.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import graphene
66
from graphene.relay import Node
77

8+
from ..registry import reset_global_registry
89
from ..fields import SQLAlchemyConnectionField
910
from ..types import SQLAlchemyObjectType
1011
from .models import Article, Base, Editor, Reporter
@@ -14,6 +15,7 @@
1415

1516
@pytest.yield_fixture(scope='function')
1617
def session():
18+
reset_global_registry()
1719
connection = db.engine.connect()
1820
transaction = connection.begin()
1921
Base.metadata.create_all(connection)

graphene_sqlalchemy/types.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import six
44
from sqlalchemy.inspection import inspect as sqlalchemyinspect
5+
from sqlalchemy.ext.hybrid import hybrid_property
56
from sqlalchemy.orm.exc import NoResultFound
67

78
from graphene import Field, ObjectType
@@ -13,7 +14,8 @@
1314

1415
from .converter import (convert_sqlalchemy_column,
1516
convert_sqlalchemy_composite,
16-
convert_sqlalchemy_relationship)
17+
convert_sqlalchemy_relationship,
18+
convert_sqlalchemy_hybrid_method)
1719
from .registry import Registry, get_global_registry
1820
from .utils import get_query, is_mapped
1921

@@ -47,6 +49,24 @@ def construct_fields(options):
4749
converted_composite = convert_sqlalchemy_composite(composite, options.registry)
4850
fields[name] = converted_composite
4951

52+
for hybrid_item in inspected_model.all_orm_descriptors:
53+
54+
if type(hybrid_item) == hybrid_property:
55+
name = hybrid_item.__name__
56+
57+
is_not_in_only = only_fields and name not in only_fields
58+
is_already_created = name in options.fields
59+
is_excluded = name in exclude_fields or is_already_created
60+
61+
if is_not_in_only or is_excluded:
62+
# We skip this field if we specify only_fields and is not
63+
# in there. Or when we excldue this field in exclude_fields
64+
65+
continue
66+
converted_hybrid_property = convert_sqlalchemy_hybrid_method(
67+
hybrid_item)
68+
fields[name] = converted_hybrid_property
69+
5070
# Get all the columns for the relationships on the model
5171
for relationship in inspected_model.relationships:
5272
is_not_in_only = only_fields and relationship.key not in only_fields
@@ -141,5 +161,6 @@ def get_node(cls, id, context, info):
141161
def resolve_id(self, args, context, info):
142162
graphene_type = info.parent_type.graphene_type
143163
if is_node(graphene_type):
144-
return self.__mapper__.primary_key_from_instance(self)[0]
164+
keys = self.__mapper__.primary_key_from_instance(self)
165+
return tuple(keys) if len(keys) > 1 else keys[0]
145166
return getattr(self, graphene_type._meta.id, None)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='graphene-sqlalchemy',
5-
version='1.0',
5+
version='1.1.1',
66

77
description='Graphene SQLAlchemy integration',
88
long_description=open('README.rst').read(),
@@ -36,6 +36,7 @@
3636
'graphene>=1.0',
3737
'SQLAlchemy',
3838
'singledispatch>=3.4.0.3',
39+
'iso8601',
3940
],
4041
tests_require=[
4142
'pytest>=2.7.2',

0 commit comments

Comments
 (0)