mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-11 12:16:58 +03:00
Merge pull request #87 from graphql-python/sqlalchemy
Added SQLAlchemy support into graphene 💪
This commit is contained in:
commit
908c18fdeb
|
@ -25,6 +25,7 @@ install:
|
|||
if [ "$TEST_TYPE" = build ]; then
|
||||
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter
|
||||
pip install --download-cache $HOME/.cache/pip/ -e .[django]
|
||||
pip install --download-cache $HOME/.cache/pip/ -e .[sqlalchemy]
|
||||
pip install django==$DJANGO_VERSION
|
||||
python setup.py develop
|
||||
elif [ "$TEST_TYPE" = build_website ]; then
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
- **Relay:** Graphene has builtin support for Relay
|
||||
- **Django:** Automatic *Django model* mapping to Graphene Types. Check a fully working [Django](http://github.com/graphql-python/swapi-graphene) implementation
|
||||
|
||||
Graphene also supports *SQLAlchemy*!
|
||||
|
||||
*What is supported in this Python version?* **Everything**: Interfaces, ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition to queries, mutations and subscriptions.
|
||||
|
||||
|
@ -18,8 +19,10 @@ For instaling graphene, just run this command in your shell
|
|||
|
||||
```bash
|
||||
pip install graphene
|
||||
# Or in case of need Django model support
|
||||
# In case of need Django model support
|
||||
pip install graphene[django]
|
||||
# Or in case of need SQLAlchemy support
|
||||
pip install graphene[sqlalchemy]
|
||||
```
|
||||
|
||||
|
||||
|
@ -59,6 +62,7 @@ If you want to learn even more, you can also check the following [examples](exam
|
|||
* **Basic Schema**: [Starwars example](examples/starwars)
|
||||
* **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
|
||||
* **Django model mapping**: [Starwars Django example](examples/starwars_django)
|
||||
* **SQLAlchemy model mapping**: [Flask SQLAlchemy example](examples/flask_sqlalchemy)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
|
|
@ -12,6 +12,8 @@ building GraphQL schemas/types fast and easily.
|
|||
`Django <http://github.com/graphql-python/swapi-graphene>`__
|
||||
implementation
|
||||
|
||||
Graphene also supports *SQLAlchemy*!
|
||||
|
||||
*What is supported in this Python version?* **Everything**: Interfaces,
|
||||
ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition
|
||||
to queries, mutations and subscriptions.
|
||||
|
@ -27,8 +29,10 @@ For instaling graphene, just run this command in your shell
|
|||
.. code:: bash
|
||||
|
||||
pip install graphene
|
||||
# Or in case of need Django model support
|
||||
# In case of need Django model support
|
||||
pip install graphene[django]
|
||||
# Or in case of need SQLAlchemy support
|
||||
pip install graphene[sqlalchemy]
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
@ -70,6 +74,8 @@ If you want to learn even more, you can also check the following
|
|||
example <examples/starwars_relay>`__
|
||||
- **Django model mapping**: `Starwars Django
|
||||
example <examples/starwars_django>`__
|
||||
- **SQLAlchemy model mapping**: `Flask SQLAlchemy
|
||||
example <examples/flask_sqlalchemy>`__
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
|
|
@ -23,3 +23,10 @@ ga = "UA-12613282-7"
|
|||
"/docs/django/tutorial/",
|
||||
"/docs/django/filtering/",
|
||||
]
|
||||
|
||||
[docs.sqlalchemy]
|
||||
name = "SQLAlchemy"
|
||||
pages = [
|
||||
"/docs/sqlalchemy/tutorial/",
|
||||
"/docs/sqlalchemy/tips/",
|
||||
]
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
}
|
||||
|
||||
#graphiql-container .resultWrap {
|
||||
position: relative;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
|
@ -1010,6 +1011,52 @@ span.CodeMirror-selectedtext { background: none; }
|
|||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
#graphiql-container .spinner-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#graphiql-container .spinner {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
position: absolute;
|
||||
-webkit-animation: rotation .6s infinite linear;
|
||||
-moz-animation: rotation .6s infinite linear;
|
||||
-o-animation: rotation .6s infinite linear;
|
||||
animation: rotation .6s infinite linear;
|
||||
border-left: 6px solid rgba(150, 150, 150, .15);
|
||||
border-right: 6px solid rgba(150, 150, 150, .15);
|
||||
border-bottom: 6px solid rgba(150, 150, 150, .15);
|
||||
border-top: 6px solid rgba(150, 150, 150, .8);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from { -webkit-transform: rotate(0deg); }
|
||||
to { -webkit-transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
@-moz-keyframes rotation {
|
||||
from { -moz-transform: rotate(0deg); }
|
||||
to { -moz-transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
@-o-keyframes rotation {
|
||||
from { -o-transform: rotate(0deg); }
|
||||
to { -o-transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(359deg); }
|
||||
}
|
||||
.CodeMirror-hints {
|
||||
background: white;
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"es6-promise": "^3.0.2",
|
||||
"extract-text-webpack-plugin": "^0.9.1",
|
||||
"gatsby": "^0.7.7",
|
||||
"graphiql": "^0.4.2",
|
||||
"graphiql": "^0.4.5",
|
||||
"graphql": "^0.4.13",
|
||||
"jeet": "^6.1.2",
|
||||
"lodash": "^3.10.1",
|
||||
|
|
|
@ -21,6 +21,9 @@ Django integration:
|
|||
- **graphql-django-view**: [Source Code][5] - [PyPI package][6]
|
||||
- **django-graphiql**: [Source Code][7] - [PyPI package][8]
|
||||
|
||||
Flask integration:
|
||||
- **graphql-flask**: [Source Code][9] - [PyPI package][10]
|
||||
|
||||
## Other related projects
|
||||
|
||||
- [Flask GraphQL Demo](https://github.com/amitsaha/flask-graphql-demo) by [@echorand](https://twitter.com/echorand)
|
||||
|
@ -37,3 +40,5 @@ Django integration:
|
|||
[6]: https://pypi.python.org/pypi/graphql-django-view
|
||||
[7]: https://github.com/graphql-python/django-graphiql
|
||||
[8]: https://pypi.python.org/pypi/django-graphiql
|
||||
[9]: https://github.com/graphql-python/graphql-flask
|
||||
[10]: https://pypi.python.org/pypi/graphql-flask
|
||||
|
|
30
docs/pages/docs/sqlalchemy/tips.md
Normal file
30
docs/pages/docs/sqlalchemy/tips.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
title: Tips
|
||||
description: Tips when SQLAlchemy in Graphene
|
||||
---
|
||||
|
||||
# Tips
|
||||
|
||||
## Querying
|
||||
|
||||
For make querying to the database work, there are two alternatives:
|
||||
|
||||
* Expose the db session when you create the `graphene.Schema`:
|
||||
|
||||
```python
|
||||
schema = graphene.Schema(session=session)
|
||||
```
|
||||
|
||||
* Create a query for the models.
|
||||
|
||||
```python
|
||||
Base = declarative_base()
|
||||
Base.query = db_session.query_property()
|
||||
|
||||
class MyModel(Base):
|
||||
# ...
|
||||
```
|
||||
|
||||
If you don't specify any, the following error will be displayed:
|
||||
|
||||
`A query in the model Base or a session in the schema is required for querying.`
|
199
docs/pages/docs/sqlalchemy/tutorial.md
Normal file
199
docs/pages/docs/sqlalchemy/tutorial.md
Normal file
|
@ -0,0 +1,199 @@
|
|||
---
|
||||
title: Tutorial
|
||||
description: Using SQLAlchemy with Graphene
|
||||
---
|
||||
|
||||
# SQLAlchemy + Flask Tutorial
|
||||
|
||||
Graphene comes with builtin support to SQLAlchemy, which makes quite easy to operate with your current models.
|
||||
|
||||
**Note: The code in this tutorial is pulled from the
|
||||
[Flask SQLAlchemy example app](https://github.com/graphql-python/graphene/tree/master/examples/flask_sqlalchemy)**.
|
||||
|
||||
|
||||
## Setup the Project
|
||||
|
||||
We will setup the project, execute the following:
|
||||
|
||||
```bash
|
||||
# Create the project directory
|
||||
mkdir flask_sqlalchemy
|
||||
cd flask_sqlalchemy
|
||||
|
||||
# Create a virtualenv to isolate our package dependencies locally
|
||||
virtualenv env
|
||||
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||
|
||||
# SQLAlchemy and Graphene with SQLAlchemy support
|
||||
pip install SQLAlchemy
|
||||
pip install graphene[sqlalchemy]
|
||||
|
||||
# Install Flask and GraphQL Flask for exposing the schema through HTTP
|
||||
pip install Flask
|
||||
pip install graphql-flask
|
||||
```
|
||||
|
||||
## Defining our models
|
||||
|
||||
Let's get started with these models:
|
||||
|
||||
```python
|
||||
# flask_sqlalchemy/models.py
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import (scoped_session, sessionmaker, relationship,
|
||||
backref)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
|
||||
db_session = scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine))
|
||||
|
||||
Base = declarative_base()
|
||||
# We will need this for querying
|
||||
Base.query = db_session.query_property()
|
||||
|
||||
|
||||
class Department(Base):
|
||||
__tablename__ = 'department'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
|
||||
|
||||
class Employee(Base):
|
||||
__tablename__ = 'employee'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
hired_on = Column(DateTime, default=func.now())
|
||||
department_id = Column(Integer, ForeignKey('department.id'))
|
||||
department = relationship(
|
||||
Department,
|
||||
backref=backref('employees',
|
||||
uselist=True,
|
||||
cascade='delete,all'))
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
GraphQL presents your objects to the world as a graph structure rather than a more
|
||||
hierarchical structure to which you may be accustomed. In order to create this
|
||||
representation, Graphene needs to know about each *type* of object which will appear in
|
||||
the graph.
|
||||
|
||||
This graph also has a *root type* through which all access begins. This is the `Query` class below.
|
||||
In this example, we provide the ability to list all employees via `all_employees`, and the
|
||||
ability to obtain a specific node via `node`.
|
||||
|
||||
Create `flask_sqlalchemy/schema.py` and type the following:
|
||||
|
||||
```python
|
||||
# flask_sqlalchemy/schema.py
|
||||
import graphene
|
||||
from graphene import relay
|
||||
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField
|
||||
from models import db_session, Department as DepartmentModel, Employee as EmployeeModel
|
||||
|
||||
schema = graphene.Schema()
|
||||
|
||||
|
||||
@schema.register
|
||||
class Department(SQLAlchemyNode):
|
||||
class Meta:
|
||||
model = DepartmentModel
|
||||
|
||||
|
||||
@schema.register
|
||||
class Employee(SQLAlchemyNode):
|
||||
class Meta:
|
||||
model = EmployeeModel
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
node = relay.NodeField()
|
||||
all_employees = SQLAlchemyConnectionField(Employee)
|
||||
|
||||
schema.query = Query
|
||||
```
|
||||
|
||||
## Creating GraphQL and GraphiQL views in Flask
|
||||
|
||||
Unlike a RESTful API, there is only a single URL from which GraphQL is accessed.
|
||||
|
||||
We are going to use Flask to create a server that expose the GraphQL schema under `/graphql` and a interface for querying it easily: GraphiQL under `/graphiql`.
|
||||
|
||||
Afortunately for us, the library `graphql-flask` that we installed previously is making the task quite easy.
|
||||
|
||||
```python
|
||||
# flask_sqlalchemy/app.py
|
||||
from flask import Flask
|
||||
from graphql_flask import GraphQL
|
||||
|
||||
from models import db_session
|
||||
from schema import schema, Department
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
# This is creating the `/graphql` and `/graphiql` endpoints
|
||||
GraphQL(app, schema=schema)
|
||||
|
||||
@app.teardown_appcontext
|
||||
def shutdown_session(exception=None):
|
||||
db_session.remove()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
```
|
||||
|
||||
|
||||
## Creating some data
|
||||
|
||||
```bash
|
||||
$ python
|
||||
|
||||
>>> from models import engine, db_session, Base, Department, Employee
|
||||
>>> Base.metadata.create_all(bind=engine)
|
||||
|
||||
>>> # Fill the tables with some data
|
||||
>>> engineering = Department(name='Engineering')
|
||||
>>> db_session.add(engineering)
|
||||
>>> hr = Department(name='Human Resources')
|
||||
>>> db_session.add(hr)
|
||||
|
||||
>>> peter = Employee(name='Peter', department=engineering)
|
||||
>>> db_session.add(peter)
|
||||
>>> roy = Employee(name='Roy', department=engineering)
|
||||
>>> db_session.add(roy)
|
||||
>>> tracy = Employee(name='Tracy', department=hr)
|
||||
>>> db_session.add(tracy)
|
||||
>>> db_session.commit()
|
||||
```
|
||||
|
||||
|
||||
## Testing our GraphQL schema
|
||||
|
||||
We're now ready to test the API we've built. Let's fire up the server from the command line.
|
||||
|
||||
```bash
|
||||
$ python ./app.py
|
||||
|
||||
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
Go to [localhost:5000/graphiql](http://localhost:5000/graphiql) and type your first query!
|
||||
|
||||
```graphql
|
||||
{
|
||||
allEmployees {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
department {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
50
examples/flask_sqlalchemy/README.md
Normal file
50
examples/flask_sqlalchemy/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
Example Flask+SQLAlchemy Project
|
||||
================================
|
||||
|
||||
This example project demos integration between Graphene, Flask and SQLAlchemy.
|
||||
The project contains two models, one named `Department` and another
|
||||
named `Employee`.
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
First you'll need to get the source of the project. Do this by cloning the
|
||||
whole Graphene repository:
|
||||
|
||||
```bash
|
||||
# Get the example project code
|
||||
git clone https://github.com/graphql-python/graphene.git
|
||||
cd graphene/examples/flask_sqlalchemy
|
||||
```
|
||||
|
||||
It is good idea (but not required) to create a virtual environment
|
||||
for this project. We'll do this using
|
||||
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
|
||||
to keep things simple,
|
||||
but you may also find something like
|
||||
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
|
||||
to be useful:
|
||||
|
||||
```bash
|
||||
# Create a virtualenv in which we can install the dependencies
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
Now we can install our dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Now the following command will setup the database, and start the server:
|
||||
|
||||
```bash
|
||||
./app.py
|
||||
|
||||
```
|
||||
|
||||
|
||||
Now head on over to
|
||||
[http://127.0.0.1:5000/graphiql](http://127.0.0.1:5000/graphiql)
|
||||
and run some queries!
|
0
examples/flask_sqlalchemy/__init__.py
Normal file
0
examples/flask_sqlalchemy/__init__.py
Normal file
34
examples/flask_sqlalchemy/app.py
Normal file
34
examples/flask_sqlalchemy/app.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from flask import Flask
|
||||
from database import db_session, init_db
|
||||
|
||||
from schema import schema
|
||||
from graphql_flask import GraphQL
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
default_query = '''
|
||||
{
|
||||
allEmployees {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
department {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'''.strip()
|
||||
|
||||
GraphQL(app, schema=schema, default_query=default_query)
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def shutdown_session(exception=None):
|
||||
db_session.remove()
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
app.run()
|
33
examples/flask_sqlalchemy/database.py
Normal file
33
examples/flask_sqlalchemy/database.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
|
||||
db_session = scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine))
|
||||
Base = declarative_base()
|
||||
Base.query = db_session.query_property()
|
||||
|
||||
|
||||
def init_db():
|
||||
# import all modules here that might define models so that
|
||||
# they will be registered properly on the metadata. Otherwise
|
||||
# you will have to import them first before calling init_db()
|
||||
from models import Department, Employee
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Create the fixtures
|
||||
engineering = Department(name='Engineering')
|
||||
db_session.add(engineering)
|
||||
hr = Department(name='Human Resources')
|
||||
db_session.add(hr)
|
||||
|
||||
peter = Employee(name='Peter', department=engineering)
|
||||
db_session.add(peter)
|
||||
roy = Employee(name='Roy', department=engineering)
|
||||
db_session.add(roy)
|
||||
tracy = Employee(name='Tracy', department=hr)
|
||||
db_session.add(tracy)
|
||||
db_session.commit()
|
26
examples/flask_sqlalchemy/models.py
Normal file
26
examples/flask_sqlalchemy/models.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from database import Base
|
||||
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
|
||||
|
||||
class Department(Base):
|
||||
__tablename__ = 'department'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
|
||||
|
||||
class Employee(Base):
|
||||
__tablename__ = 'employee'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
# Use default=func.now() to set the default hiring time
|
||||
# of an Employee to be the current time when an
|
||||
# Employee record was created
|
||||
hired_on = Column(DateTime, default=func.now())
|
||||
department_id = Column(Integer, ForeignKey('department.id'))
|
||||
# Use cascade='delete,all' to propagate the deletion of a Department onto its Employees
|
||||
department = relationship(
|
||||
Department,
|
||||
backref=backref('employees',
|
||||
uselist=True,
|
||||
cascade='delete,all'))
|
4
examples/flask_sqlalchemy/requirements.txt
Normal file
4
examples/flask_sqlalchemy/requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
graphene[sqlalchemy]
|
||||
graphql_flask==1.1.0
|
||||
SQLAlchemy==1.0.11
|
||||
Flask==0.10.1
|
25
examples/flask_sqlalchemy/schema.py
Normal file
25
examples/flask_sqlalchemy/schema.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField
|
||||
from models import Department as DepartmentModel, Employee as EmployeeModel
|
||||
|
||||
schema = graphene.Schema()
|
||||
|
||||
|
||||
@schema.register
|
||||
class Department(SQLAlchemyNode):
|
||||
class Meta:
|
||||
model = DepartmentModel
|
||||
|
||||
|
||||
@schema.register
|
||||
class Employee(SQLAlchemyNode):
|
||||
class Meta:
|
||||
model = EmployeeModel
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
node = relay.NodeField()
|
||||
all_employees = SQLAlchemyConnectionField(Employee)
|
||||
|
||||
schema.query = Query
|
11
graphene/contrib/sqlalchemy/__init__.py
Normal file
11
graphene/contrib/sqlalchemy/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from graphene.contrib.sqlalchemy.types import (
|
||||
SQLAlchemyObjectType,
|
||||
SQLAlchemyNode
|
||||
)
|
||||
from graphene.contrib.sqlalchemy.fields import (
|
||||
SQLAlchemyConnectionField,
|
||||
SQLAlchemyModelField
|
||||
)
|
||||
|
||||
__all__ = ['SQLAlchemyObjectType', 'SQLAlchemyNode',
|
||||
'SQLAlchemyConnectionField', 'SQLAlchemyModelField']
|
61
graphene/contrib/sqlalchemy/converter.py
Normal file
61
graphene/contrib/sqlalchemy/converter.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from singledispatch import singledispatch
|
||||
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy.orm import interfaces
|
||||
|
||||
from ...core.types.scalars import ID, Boolean, Float, Int, String
|
||||
from .fields import ConnectionOrListField, SQLAlchemyModelField
|
||||
|
||||
|
||||
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)
|
||||
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)
|
66
graphene/contrib/sqlalchemy/fields.py
Normal file
66
graphene/contrib/sqlalchemy/fields.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
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_type_for_model, maybe_query, get_query
|
||||
|
||||
|
||||
class SQLAlchemyConnectionField(ConnectionField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(SQLAlchemyConnectionField, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self.type._meta.model
|
||||
|
||||
def get_query(self, resolved_query, args, info):
|
||||
return resolved_query or get_query(self.model, info)
|
||||
|
||||
def from_list(self, connection_type, resolved, args, info):
|
||||
query = self.get_query(resolved, args, info)
|
||||
query = maybe_query(query)
|
||||
return super(SQLAlchemyConnectionField, self).from_list(connection_type, query, args, 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)
|
23
graphene/contrib/sqlalchemy/options.py
Normal file
23
graphene/contrib/sqlalchemy/options.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from ...core.classtypes.objecttype import ObjectTypeOptions
|
||||
from ...relay.types import Node
|
||||
from ...relay.utils import is_node
|
||||
|
||||
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
|
||||
|
||||
|
||||
class SQLAlchemyOptions(ObjectTypeOptions):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
|
||||
self.model = None
|
||||
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)
|
0
graphene/contrib/sqlalchemy/tests/__init__.py
Normal file
0
graphene/contrib/sqlalchemy/tests/__init__.py
Normal file
36
graphene/contrib/sqlalchemy/tests/models.py
Normal file
36
graphene/contrib/sqlalchemy/tests/models.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
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 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'))
|
105
graphene/contrib/sqlalchemy/tests/test_converter.py
Normal file
105
graphene/contrib/sqlalchemy/tests/test_converter.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
from py.test import raises
|
||||
|
||||
import graphene
|
||||
from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column,
|
||||
convert_sqlalchemy_relationship)
|
||||
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
|
||||
SQLAlchemyModelField)
|
||||
from sqlalchemy import Column, types
|
||||
|
||||
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_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
|
173
graphene/contrib/sqlalchemy/tests/test_query.py
Normal file
173
graphene/contrib/sqlalchemy/tests/test_query.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
import pytest
|
||||
|
||||
import graphene
|
||||
from graphene import relay
|
||||
from graphene.contrib.sqlalchemy import SQLAlchemyObjectType, SQLAlchemyNode
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
|
||||
from .models import Base, Reporter, Article
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
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!'
|
||||
}
|
||||
}]
|
||||
},
|
||||
},
|
||||
'myArticle': {
|
||||
'id': 'QXJ0aWNsZU5vZGU6MQ==',
|
||||
'headline': 'Hi!'
|
||||
}
|
||||
}
|
||||
schema = graphene.Schema(query=Query, session=session)
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
45
graphene/contrib/sqlalchemy/tests/test_schema.py
Normal file
45
graphene/contrib/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/contrib/sqlalchemy/tests/test_types.py
Normal file
102
graphene/contrib/sqlalchemy/tests/test_types.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
from graphql.core.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/contrib/sqlalchemy/tests/test_utils.py
Normal file
25
graphene/contrib/sqlalchemy/tests/test_utils.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from graphene import Schema, ObjectType, 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/contrib/sqlalchemy/types.py
Normal file
125
graphene/contrib/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 is_mapped, get_query
|
||||
|
||||
|
||||
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 column in inspected_model.columns:
|
||||
is_not_in_only = only_fields and column.name not in only_fields
|
||||
is_already_created = column.name in already_created_fields
|
||||
is_excluded = column.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(column.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):
|
||||
if _root:
|
||||
assert isinstance(_root, self._meta.model), (
|
||||
'{} received a non-compatible instance ({}) '
|
||||
'when expecting {}'.format(
|
||||
self.__class__.__name__,
|
||||
_root.__class__.__name__,
|
||||
self._meta.model.__name__
|
||||
))
|
||||
super(InstanceObjectType, self).__init__(_root=_root)
|
||||
|
||||
@property
|
||||
def instance(self):
|
||||
return self._root
|
||||
|
||||
@instance.setter
|
||||
def instance(self, value):
|
||||
self._root = value
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._root, attr)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_node(cls, id, info=None):
|
||||
try:
|
||||
model = cls._meta.model
|
||||
query = get_query(model, info)
|
||||
instance = query.filter(model.id == id).one()
|
||||
return cls(instance)
|
||||
except NoResultFound:
|
||||
return None
|
49
graphene/contrib/sqlalchemy/utils.py
Normal file
49
graphene/contrib/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)
|
|
@ -26,7 +26,7 @@ class Schema(object):
|
|||
_executor = None
|
||||
|
||||
def __init__(self, query=None, mutation=None, subscription=None,
|
||||
name='Schema', executor=None, plugins=None, auto_camelcase=True):
|
||||
name='Schema', executor=None, plugins=None, auto_camelcase=True, **options):
|
||||
self._types_names = {}
|
||||
self._types = {}
|
||||
self.mutation = mutation
|
||||
|
@ -38,6 +38,7 @@ class Schema(object):
|
|||
if auto_camelcase:
|
||||
plugins.append(CamelCase())
|
||||
self.plugins = PluginManager(self, plugins)
|
||||
self.options = options
|
||||
signals.init_schema.send(self)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
5
setup.py
5
setup.py
|
@ -62,6 +62,7 @@ setup(
|
|||
'django-filter>=0.10.0',
|
||||
'pytest>=2.7.2',
|
||||
'pytest-django',
|
||||
'sqlalchemy',
|
||||
'mock',
|
||||
],
|
||||
extras_require={
|
||||
|
@ -70,6 +71,10 @@ setup(
|
|||
'singledispatch>=3.4.0.3',
|
||||
'graphql-django-view>=1.1.0',
|
||||
],
|
||||
'sqlalchemy': [
|
||||
'sqlalchemy',
|
||||
'singledispatch>=3.4.0.3',
|
||||
]
|
||||
},
|
||||
|
||||
cmdclass={'test': PyTest},
|
||||
|
|
Loading…
Reference in New Issue
Block a user