Codebase list python-graphene-sqlalchemy / 338cba6
New upstream version 2.1.2 Sophie Brun 5 years ago
46 changed file(s) with 1744 addition(s) and 524 deletion(s). Raw diff Collapse all Expand all
4444 nosetests.xml
4545 coverage.xml
4646 *,cover
47 .pytest_cache/
4748
4849 # Translations
4950 *.mo
0 default_language_version:
1 python: python3.7
2 repos:
3 - repo: git://github.com/pre-commit/pre-commit-hooks
4 rev: c8bad492e1b1d65d9126dba3fe3bd49a5a52b9d6 # v2.1.0
5 hooks:
6 - id: check-merge-conflict
7 - id: check-yaml
8 - id: debug-statements
9 - id: end-of-file-fixer
10 exclude: ^docs/.*$
11 - id: trailing-whitespace
12 exclude: README.md
13 - repo: git://github.com/PyCQA/flake8
14 rev: 88caf5ac484f5c09aedc02167c59c66ff0af0068 # 3.7.7
15 hooks:
16 - id: flake8
17 - repo: git://github.com/asottile/seed-isort-config
18 rev: v1.7.0
19 hooks:
20 - id: seed-isort-config
21 - repo: git://github.com/pre-commit/mirrors-isort
22 rev: v4.3.4
23 hooks:
24 - id: isort
00 language: python
1 sudo: false
2 python:
3 - 2.7
4 - 3.4
5 - 3.5
6 - 3.6
7 before_install:
8 install:
9 - |
10 if [ "$TEST_TYPE" = build ]; then
11 pip install pytest==3.0.2 pytest-cov pytest-benchmark coveralls six mock sqlalchemy_utils
12 pip install -e .
13 python setup.py develop
14 elif [ "$TEST_TYPE" = lint ]; then
15 pip install flake8
16 fi
17 script:
18 - |
19 if [ "$TEST_TYPE" = lint ]; then
20 echo "Checking Python code lint."
21 flake8 graphene_sqlalchemy
22 exit
23 elif [ "$TEST_TYPE" = build ]; then
24 py.test --cov=graphene_sqlalchemy graphene_sqlalchemy examples
25 fi
26 after_success:
27 - |
28 if [ "$TEST_TYPE" = build ]; then
29 coveralls
30 fi
31 env:
32 matrix:
33 - TEST_TYPE=build
341 matrix:
35 fast_finish: true
362 include:
37 - python: '2.7'
38 env: TEST_TYPE=lint
3 # Python 2.7
4 - env: TOXENV=py27
5 python: 2.7
6 # Python 3.5
7 - env: TOXENV=py34
8 python: 3.4
9 # Python 3.5
10 - env: TOXENV=py35
11 python: 3.5
12 # Python 3.6
13 - env: TOXENV=py36
14 python: 3.6
15 # Python 3.7
16 - env: TOXENV=py37
17 python: 3.7
18 dist: xenial
19 # SQLAlchemy 1.1
20 - env: TOXENV=py37-sql11
21 python: 3.7
22 dist: xenial
23 # SQLAlchemy 1.2
24 - env: TOXENV=py37-sql12
25 python: 3.7
26 dist: xenial
27 # SQLAlchemy 1.3
28 - env: TOXENV=py37-sql13
29 python: 3.7
30 dist: xenial
31 # Pre-commit
32 - env: TOXENV=pre-commit
33 python: 3.7
34 dist: xenial
35 install: pip install .[dev]
36 script: tox
37 after_success: coveralls
38 cache:
39 directories:
40 - $HOME/.cache/pip
41 - $HOME/.cache/pre-commit
3942 deploy:
4043 provider: pypi
4144 user: syrusakbary
0 ## Local Development
1
2 Set up our development dependencies:
3
4 ```sh
5 pip install -e ".[dev]"
6 pre-commit install
7 ```
8
9 We use `tox` to test this library against different versions of `python` and `SQLAlchemy`.
10 While developping locally, it is usually fine to run the tests against the most recent versions:
11
12 ```sh
13 tox -e py37 # Python 3.7, SQLAlchemy < 2.0
14 tox -e py37 -- -v -s # Verbose output
15 tox -e py37 -- -k test_query # Only test_query.py
16 ```
17
18 Our linters will run automatically when committing via git hooks but you can also run them manually:
19
20 ```sh
21 tox -e pre-commit
22 ```
23
24 ## Release Process
25
26 1. Update the version number in graphene_sqlalchemy/__init__.py via a PR.
27
28 2. Once the PR is merged, tag the commit on master with the new version (only maintainers of the repo can do this). For example, "v2.1.2". Travis will then automatically build this tag and release it to Pypi.
29
30 3. Make sure to create a new release on github (via the release tab) that lists all the changes that went into the new version.
0 The MIT License (MIT)
1
2 Copyright (c) 2016 Syrus Akbary
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
2121
2222 ```python
2323 from sqlalchemy import Column, Integer, String
24 from sqlalchemy.orm import relationship
2524
2625 from sqlalchemy.ext.declarative import declarative_base
2726
2827 Base = declarative_base()
2928
3029 class UserModel(Base):
31 __tablename__ = 'department'
30 __tablename__ = 'user'
3231 id = Column(Integer, primary_key=True)
3332 name = Column(String)
3433 last_name = Column(String)
3736 To create a GraphQL schema for it you simply have to write the following:
3837
3938 ```python
39 import graphene
4040 from graphene_sqlalchemy import SQLAlchemyObjectType
4141
4242 class User(SQLAlchemyObjectType):
4343 class Meta:
4444 model = UserModel
45 # only return specified fields
46 only_fields = ("name",)
47 # exclude specified fields
48 exclude_fields = ("last_name",)
4549
4650 class Query(graphene.ObjectType):
4751 users = graphene.List(User)
97101 schema = graphene.Schema(query=Query)
98102 ```
99103
104 ### Full Examples
105
100106 To learn more check out the following [examples](examples/):
101107
102 * **Full example**: [Flask SQLAlchemy example](examples/flask_sqlalchemy)
103
108 - [Flask SQLAlchemy example](examples/flask_sqlalchemy)
109 - [Nameko SQLAlchemy example](examples/nameko_sqlalchemy)
104110
105111 ## Contributing
106112
107 After cloning this repo, ensure dependencies are installed by running:
108
109 ```sh
110 python setup.py install
111 ```
112
113 After developing, the full test suite can be evaluated by running:
114
115 ```sh
116 python setup.py test # Use --pytest-args="-v -s" for verbose mode
117 ```
113 See [CONTRIBUTING.md](/CONTRIBUTING.md)
135135 # html_theme = 'alabaster'
136136 # if on_rtd:
137137 # html_theme = 'sphinx_rtd_theme'
138 import sphinx_graphene_theme
138 import sphinx_graphene_theme # isort:skip
139139
140140 html_theme = "sphinx_graphene_theme"
141141
0 Schema Examples
1 ===========================
2
3
4 Search all Models with Union
5 ----------------------------
6
7 .. code:: python
8
9 class Book(SQLAlchemyObjectType):
10 class Meta:
11 model = BookModel
12 interfaces = (relay.Node,)
13
14
15 class BookConnection(relay.Connection):
16 class Meta:
17 node = Book
18
19
20 class Author(SQLAlchemyObjectType):
21 class Meta:
22 model = AuthorModel
23 interfaces = (relay.Node,)
24
25
26 class AuthorConnection(relay.Connection):
27 class Meta:
28 node = Author
29
30
31 class SearchResult(graphene.Union):
32 class Meta:
33 types = (Book, Author)
34
35
36 class Query(graphene.ObjectType):
37 node = relay.Node.Field()
38 search = graphene.List(SearchResult, q=graphene.String()) # List field for search results
39
40 # Normal Fields
41 all_books = SQLAlchemyConnectionField(BookConnection)
42 all_authors = SQLAlchemyConnectionField(AuthorConnection)
43
44 def resolve_search(self, info, **args):
45 q = args.get("q") # Search query
46
47 # Get queries
48 bookdata_query = BookData.get_query(info)
49 author_query = Author.get_query(info)
50
51 # Query Books
52 books = bookdata_query.filter((BookModel.title.contains(q)) |
53 (BookModel.isbn.contains(q)) |
54 (BookModel.authors.any(AuthorModel.name.contains(q)))).all()
55
56 # Query Authors
57 authors = author_query.filter(AuthorModel.name.contains(q)).all()
58
59 return authors + books # Combine lists
60
61 schema = graphene.Schema(query=Query, types=[Book, Author, SearchResult])
62
63 Example GraphQL query
64
65 .. code::
66
67 book(id: "Qm9vazow") {
68 id
69 title
70 }
71 search(q: "Making Games") {
72 __typename
73 ... on Author {
74 fname
75 lname
76 }
77 ... on Book {
78 title
79 isbn
80 }
81 }
77
88 tutorial
99 tips
10 examples
00 # Docs template
1 https://github.com/graphql-python/graphene-python.org/archive/docs.zip
1 http://graphene-python.org/sphinx_graphene_theme.zip
00 ====
1 Tips
2 ====
3
41 Tips
52 ====
63
74 Querying
85 --------
96
10 For make querying to the database work, there are two alternatives:
7 In order to make querying against the database work, there are two alternatives:
118
129 - Set the db session when you do the execution:
1310
2926 If you don't specify any, the following error will be displayed:
3027
3128 ``A query in the model Base or a session in the schema is required for querying.``
29
30 Sorting
31 -------
32
33 By default the SQLAlchemyConnectionField sorts the result elements over the primary key(s).
34 The query has a `sort` argument which allows to sort over a different column(s)
35
36 Given the model
37
38 .. code:: python
39
40 class Pet(Base):
41 __tablename__ = 'pets'
42 id = Column(Integer(), primary_key=True)
43 name = Column(String(30))
44 pet_kind = Column(Enum('cat', 'dog', name='pet_kind'), nullable=False)
45
46
47 class PetNode(SQLAlchemyObjectType):
48 class Meta:
49 model = Pet
50
51
52 class PetConnection(Connection):
53 class Meta:
54 node = PetNode
55
56
57 class Query(ObjectType):
58 allPets = SQLAlchemyConnectionField(PetConnection)
59
60 some of the allowed queries are
61
62 - Sort in ascending order over the `name` column
63
64 .. code::
65
66 allPets(sort: name_asc){
67 edges {
68 node {
69 name
70 }
71 }
72 }
73
74 - Sort in descending order over the `per_kind` column and in ascending order over the `name` column
75
76 .. code::
77
78 allPets(sort: [pet_kind_desc, name_asc]) {
79 edges {
80 node {
81 name
82 petKind
83 }
84 }
85 }
86
9292 import graphene
9393 from graphene import relay
9494 from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
95 from models import db_session, Department as DepartmentModel, Employee as EmployeeModel
95 from .models import db_session, Department as DepartmentModel, Employee as EmployeeModel
9696
9797
9898 class Department(SQLAlchemyObjectType):
101101 interfaces = (relay.Node, )
102102
103103
104 class DepartmentConnection(relay.Connection):
105 class Meta:
106 node = Department
107
108
104109 class Employee(SQLAlchemyObjectType):
105110 class Meta:
106111 model = EmployeeModel
107112 interfaces = (relay.Node, )
108113
109114
115 class EmployeeConnection(relay.Connection):
116 class Meta:
117 node = Employee
118
119
110120 class Query(graphene.ObjectType):
111121 node = relay.Node.Field()
112 all_employees = SQLAlchemyConnectionField(Employee)
122 # Allows sorting over multiple columns, by default over the primary key
123 all_employees = SQLAlchemyConnectionField(EmployeeConnection)
124 # Disable sorting over this field
125 all_departments = SQLAlchemyConnectionField(DepartmentConnection, sort=None)
113126
114127 schema = graphene.Schema(query=Query)
115128
132145 from flask import Flask
133146 from flask_graphql import GraphQLView
134147
135 from models import db_session
136 from schema import schema, Department
148 from .models import db_session
149 from .schema import schema, Department
137150
138151 app = Flask(__name__)
139152 app.debug = True
161174
162175 $ python
163176
164 >>> from models import engine, db_session, Base, Department, Employee
177 >>> from .models import engine, db_session, Base, Department, Employee
165178 >>> Base.metadata.create_all(bind=engine)
166179
167180 >>> # Fill the tables with some data
11
22 from flask import Flask
33
4 from database import db_session, init_db
54 from flask_graphql import GraphQLView
6 from schema import schema
5
6 from .database import db_session, init_db
7 from .schema import schema
78
89 app = Flask(__name__)
910 app.debug = True
1313 # import all modules here that might define models so that
1414 # they will be registered properly on the metadata. Otherwise
1515 # you will have to import them first before calling init_db()
16 from models import Department, Employee, Role
16 from .models import Department, Employee, Role
1717 Base.metadata.drop_all(bind=engine)
1818 Base.metadata.create_all(bind=engine)
1919
00 from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func
11 from sqlalchemy.orm import backref, relationship
22
3 from database import Base
3 from .database import Base
44
55
66 class Department(Base):
00 graphene[sqlalchemy]
11 SQLAlchemy==1.0.11
2 Flask==0.10.1
2 Flask==0.12.4
33 Flask-GraphQL==1.3.0
00 import graphene
11 from graphene import relay
2 from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType
3 from models import Department as DepartmentModel
4 from models import Employee as EmployeeModel
5 from models import Role as RoleModel
2 from graphene_sqlalchemy import (SQLAlchemyConnectionField,
3 SQLAlchemyObjectType, utils)
4
5 from .models import Department as DepartmentModel
6 from .models import Employee as EmployeeModel
7 from .models import Role as RoleModel
68
79
810 class Department(SQLAlchemyObjectType):
9
1011 class Meta:
1112 model = DepartmentModel
1213 interfaces = (relay.Node, )
1314
1415
1516 class Employee(SQLAlchemyObjectType):
16
1717 class Meta:
1818 model = EmployeeModel
1919 interfaces = (relay.Node, )
2020
2121
2222 class Role(SQLAlchemyObjectType):
23
2423 class Meta:
2524 model = RoleModel
2625 interfaces = (relay.Node, )
2726
2827
28 SortEnumEmployee = utils.sort_enum_for_model(EmployeeModel, 'SortEnumEmployee',
29 lambda c, d: c.upper() + ('_ASC' if d else '_DESC'))
30
31
2932 class Query(graphene.ObjectType):
3033 node = relay.Node.Field()
31 all_employees = SQLAlchemyConnectionField(Employee)
34 # Allow only single column sorting
35 all_employees = SQLAlchemyConnectionField(
36 Employee,
37 sort=graphene.Argument(
38 SortEnumEmployee,
39 default_value=utils.EnumValue('id_asc', EmployeeModel.id.asc())))
40 # Allows sorting over multiple columns, by default over the primary key
3241 all_roles = SQLAlchemyConnectionField(Role)
33 role = graphene.Field(Role)
42 # Disable sorting over this field
43 all_departments = SQLAlchemyConnectionField(Department, sort=None)
3444
3545
3646 schema = graphene.Schema(query=Query, types=[Department, Employee, Role])
0 Example Nameko+Graphene-SQLAlchemy Project
1 ================================
2
3 This example is for those who are not using frameworks like Flask | Django which already have a View wrapper implemented to handle graphql request and response accordingly
4
5 If you need a [graphiql](https://github.com/graphql/graphiql) interface on your application, kindly look at [flask_sqlalchemy](../flask_sqlalchemy).
6
7 Using [nameko](https://github.com/nameko/nameko) as an example, but you can get rid of `service.py`
8
9 The project contains two models, one named `Department` and another
10 named `Employee`.
11
12 Getting started
13 ---------------
14
15 First you'll need to get the source of the project. Do this by cloning the
16 whole Graphene repository:
17
18 ```bash
19 # Get the example project code
20 git clone https://github.com/graphql-python/graphene-sqlalchemy.git
21 cd graphene-sqlalchemy/examples/nameko_sqlalchemy
22 ```
23
24 It is good idea (but not required) to create a virtual environment
25 for this project. We'll do this using
26 [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
27 to keep things simple,
28 but you may also find something like
29 [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
30 to be useful:
31
32 ```bash
33 # Create a virtualenv in which we can install the dependencies
34 virtualenv env
35 source env/bin/activate
36 ```
37
38 Now we can install our dependencies:
39
40 ```bash
41 pip install -r requirements.txt
42 ```
43
44 Now the following command will setup the database, and start the server:
45
46 ```bash
47 ./run.sh
48
49 ```
50
51 Now head on over to postman and send POST request to:
52 [http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql)
53 and run some queries!
0 from graphql_server import (HttpQueryError, default_format_error,
1 encode_execution_results, json_encode,
2 load_json_body, run_http_query)
3
4 from .database import db_session, init_db
5 from .schema import schema
6
7
8 class App():
9 def __init__(self):
10 init_db()
11
12 def query(self, request):
13 data = self.parse_body(request)
14 execution_results, params = run_http_query(
15 schema,
16 'post',
17 data)
18 result, status_code = encode_execution_results(
19 execution_results,
20 format_error=default_format_error,is_batch=False, encode=json_encode)
21 return result
22
23 def parse_body(self,request):
24 # We use mimetype here since we don't need the other
25 # information provided by content_type
26 content_type = request.mimetype
27 if content_type == 'application/graphql':
28 return {'query': request.data.decode('utf8')}
29
30 elif content_type == 'application/json':
31 return load_json_body(request.data.decode('utf8'))
32
33 elif content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'):
34 return request.form
35
36 return {}
0 WEB_SERVER_ADDRESS: '0.0.0.0:5000'
0 from sqlalchemy import create_engine
1 from sqlalchemy.ext.declarative import declarative_base
2 from sqlalchemy.orm import scoped_session, sessionmaker
3
4 engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
5 db_session = scoped_session(sessionmaker(autocommit=False,
6 autoflush=False,
7 bind=engine))
8 Base = declarative_base()
9 Base.query = db_session.query_property()
10
11
12 def init_db():
13 # import all modules here that might define models so that
14 # they will be registered properly on the metadata. Otherwise
15 # you will have to import them first before calling init_db()
16 from .models import Department, Employee, Role
17 Base.metadata.drop_all(bind=engine)
18 Base.metadata.create_all(bind=engine)
19
20 # Create the fixtures
21 engineering = Department(name='Engineering')
22 db_session.add(engineering)
23 hr = Department(name='Human Resources')
24 db_session.add(hr)
25
26 manager = Role(name='manager')
27 db_session.add(manager)
28 engineer = Role(name='engineer')
29 db_session.add(engineer)
30
31 peter = Employee(name='Peter', department=engineering, role=engineer)
32 db_session.add(peter)
33 roy = Employee(name='Roy', department=engineering, role=engineer)
34 db_session.add(roy)
35 tracy = Employee(name='Tracy', department=hr, role=manager)
36 db_session.add(tracy)
37 db_session.commit()
0 from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func
1 from sqlalchemy.orm import backref, relationship
2
3 from .database import Base
4
5
6 class Department(Base):
7 __tablename__ = 'department'
8 id = Column(Integer, primary_key=True)
9 name = Column(String)
10
11
12 class Role(Base):
13 __tablename__ = 'roles'
14 role_id = Column(Integer, primary_key=True)
15 name = Column(String)
16
17
18 class Employee(Base):
19 __tablename__ = 'employee'
20 id = Column(Integer, primary_key=True)
21 name = Column(String)
22 # Use default=func.now() to set the default hiring time
23 # of an Employee to be the current time when an
24 # Employee record was created
25 hired_on = Column(DateTime, default=func.now())
26 department_id = Column(Integer, ForeignKey('department.id'))
27 role_id = Column(Integer, ForeignKey('roles.role_id'))
28 # Use cascade='delete,all' to propagate the deletion of a Department onto its Employees
29 department = relationship(
30 Department,
31 backref=backref('employees',
32 uselist=True,
33 cascade='delete,all'))
34 role = relationship(
35 Role,
36 backref=backref('roles',
37 uselist=True,
38 cascade='delete,all'))
0 graphene[sqlalchemy]
1 SQLAlchemy==1.0.11
2 nameko
3 graphql-server-core
0 #!/bin/sh
1 echo "Starting application service server"
2 # Run Service
3 nameko run --config config.yml service
0 import graphene
1 from graphene import relay
2 from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType
3
4 from .models import Department as DepartmentModel
5 from .models import Employee as EmployeeModel
6 from .models import Role as RoleModel
7
8
9 class Department(SQLAlchemyObjectType):
10
11 class Meta:
12 model = DepartmentModel
13 interfaces = (relay.Node, )
14
15
16 class Employee(SQLAlchemyObjectType):
17
18 class Meta:
19 model = EmployeeModel
20 interfaces = (relay.Node, )
21
22
23 class Role(SQLAlchemyObjectType):
24
25 class Meta:
26 model = RoleModel
27 interfaces = (relay.Node, )
28
29
30 class Query(graphene.ObjectType):
31 node = relay.Node.Field()
32 all_employees = SQLAlchemyConnectionField(Employee)
33 all_roles = SQLAlchemyConnectionField(Role)
34 role = graphene.Field(Role)
35
36
37 schema = graphene.Schema(query=Query, types=[Department, Employee, Role])
0 #!/usr/bin/env python
1 from nameko.web.handlers import http
2
3 from .app import App
4
5
6 class DepartmentService:
7 name = 'department'
8
9 @http('POST', '/graphql')
10 def query(self, request):
11 return App().query(request)
0 from .types import (
1 SQLAlchemyObjectType,
2 )
3 from .fields import (
4 SQLAlchemyConnectionField
5 )
6 from .utils import (
7 get_query,
8 get_session
9 )
0 from .types import SQLAlchemyObjectType
1 from .fields import SQLAlchemyConnectionField
2 from .utils import get_query, get_session
103
11 __version__ = '2.0.0'
4 __version__ = "2.1.2"
125
136 __all__ = [
14 '__version__',
15 'SQLAlchemyObjectType',
16 'SQLAlchemyConnectionField',
17 'get_query',
18 'get_session'
7 "__version__",
8 "SQLAlchemyObjectType",
9 "SQLAlchemyConnectionField",
10 "get_query",
11 "get_session",
1912 ]
66 String)
77 from graphene.types.json import JSONString
88
9 from .fields import createConnectionField
10
119 try:
12 from sqlalchemy_utils import (
13 ChoiceType, JSONType, ScalarListType, TSVectorType)
10 from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType
1411 except ImportError:
1512 ChoiceType = JSONType = ScalarListType = TSVectorType = object
1613
1714
1815 def get_column_doc(column):
19 return getattr(column, 'doc', None)
16 return getattr(column, "doc", None)
2017
2118
2219 def is_column_nullable(column):
23 return bool(getattr(column, 'nullable', True))
20 return bool(getattr(column, "nullable", True))
2421
2522
26 def convert_sqlalchemy_relationship(relationship, registry):
23 def convert_sqlalchemy_relationship(relationship, registry, connection_field_factory):
2724 direction = relationship.direction
2825 model = relationship.mapper.entity
2926
3532 return Field(_type)
3633 elif direction in (interfaces.ONETOMANY, interfaces.MANYTOMANY):
3734 if _type._meta.connection:
38 return createConnectionField(_type)
35 return connection_field_factory(relationship, registry)
3936 return Field(List(_type))
4037
4138 return Dynamic(dynamic_type)
4239
4340
4441 def convert_sqlalchemy_hybrid_method(hybrid_item):
45 return String(description=getattr(hybrid_item, '__doc__', None),
46 required=False)
42 return String(description=getattr(hybrid_item, "__doc__", None), required=False)
4743
4844
4945 def convert_sqlalchemy_composite(composite, registry):
5147 if not converter:
5248 try:
5349 raise Exception(
54 "Don't know how to convert the composite field %s (%s)" %
55 (composite, composite.composite_class))
50 "Don't know how to convert the composite field %s (%s)"
51 % (composite, composite.composite_class)
52 )
5653 except AttributeError:
5754 # handle fields that are not attached to a class yet (don't have a parent)
5855 raise Exception(
59 "Don't know how to convert the composite field %r (%s)" %
60 (composite, composite.composite_class))
56 "Don't know how to convert the composite field %r (%s)"
57 % (composite, composite.composite_class)
58 )
6159 return converter(composite, registry)
6260
6361
6462 def _register_composite_class(cls, registry=None):
6563 if registry is None:
6664 from .registry import get_global_registry
65
6766 registry = get_global_registry()
6867
6968 def inner(fn):
7069 registry.register_composite_converter(cls, fn)
70
7171 return inner
7272
7373
7575
7676
7777 def convert_sqlalchemy_column(column, registry=None):
78 return convert_sqlalchemy_type(getattr(column, 'type', None), column, registry)
78 return convert_sqlalchemy_type(getattr(column, "type", None), column, registry)
7979
8080
8181 @singledispatch
8282 def convert_sqlalchemy_type(type, column, registry=None):
8383 raise Exception(
84 "Don't know how to convert the SQLAlchemy field %s (%s)" % (column, column.__class__))
84 "Don't know how to convert the SQLAlchemy field %s (%s)"
85 % (column, column.__class__)
86 )
8587
8688
8789 @convert_sqlalchemy_type.register(types.Date)
9092 @convert_sqlalchemy_type.register(types.Text)
9193 @convert_sqlalchemy_type.register(types.Unicode)
9294 @convert_sqlalchemy_type.register(types.UnicodeText)
93 @convert_sqlalchemy_type.register(types.Enum)
94 @convert_sqlalchemy_type.register(postgresql.ENUM)
9595 @convert_sqlalchemy_type.register(postgresql.UUID)
96 @convert_sqlalchemy_type.register(postgresql.INET)
97 @convert_sqlalchemy_type.register(postgresql.CIDR)
9698 @convert_sqlalchemy_type.register(TSVectorType)
9799 def convert_column_to_string(type, column, registry=None):
98 return String(description=get_column_doc(column),
99 required=not(is_column_nullable(column)))
100 return String(
101 description=get_column_doc(column), required=not (is_column_nullable(column))
102 )
100103
101104
102105 @convert_sqlalchemy_type.register(types.DateTime)
103106 def convert_column_to_datetime(type, column, registry=None):
104107 from graphene.types.datetime import DateTime
105 return DateTime(description=get_column_doc(column),
106 required=not(is_column_nullable(column)))
108
109 return DateTime(
110 description=get_column_doc(column), required=not (is_column_nullable(column))
111 )
107112
108113
109114 @convert_sqlalchemy_type.register(types.SmallInteger)
110115 @convert_sqlalchemy_type.register(types.Integer)
111116 def convert_column_to_int_or_id(type, column, registry=None):
112117 if column.primary_key:
113 return ID(description=get_column_doc(column), required=not (is_column_nullable(column)))
118 return ID(
119 description=get_column_doc(column),
120 required=not (is_column_nullable(column)),
121 )
114122 else:
115 return Int(description=get_column_doc(column),
116 required=not (is_column_nullable(column)))
123 return Int(
124 description=get_column_doc(column),
125 required=not (is_column_nullable(column)),
126 )
117127
118128
119129 @convert_sqlalchemy_type.register(types.Boolean)
120130 def convert_column_to_boolean(type, column, registry=None):
121 return Boolean(description=get_column_doc(column), required=not(is_column_nullable(column)))
131 return Boolean(
132 description=get_column_doc(column), required=not (is_column_nullable(column))
133 )
122134
123135
124136 @convert_sqlalchemy_type.register(types.Float)
125137 @convert_sqlalchemy_type.register(types.Numeric)
126138 @convert_sqlalchemy_type.register(types.BigInteger)
127139 def convert_column_to_float(type, column, registry=None):
128 return Float(description=get_column_doc(column), required=not(is_column_nullable(column)))
140 return Float(
141 description=get_column_doc(column), required=not (is_column_nullable(column))
142 )
143
144
145 @convert_sqlalchemy_type.register(types.Enum)
146 def convert_enum_to_enum(type, column, registry=None):
147 enum_class = getattr(type, 'enum_class', None)
148 if enum_class: # Check if an enum.Enum type is used
149 graphene_type = Enum.from_enum(enum_class)
150 else: # Nope, just a list of string options
151 items = zip(type.enums, type.enums)
152 graphene_type = Enum(type.name, items)
153 return Field(
154 graphene_type,
155 description=get_column_doc(column),
156 required=not (is_column_nullable(column)),
157 )
129158
130159
131160 @convert_sqlalchemy_type.register(ChoiceType)
132161 def convert_column_to_enum(type, column, registry=None):
133 name = '{}_{}'.format(column.table.name, column.name).upper()
162 name = "{}_{}".format(column.table.name, column.name).upper()
134163 return Enum(name, type.choices, description=get_column_doc(column))
135164
136165
143172 def convert_postgres_array_to_list(_type, column, registry=None):
144173 graphene_type = convert_sqlalchemy_type(column.type.item_type, column)
145174 inner_type = type(graphene_type)
146 return List(inner_type, description=get_column_doc(column), required=not(is_column_nullable(column)))
175 return List(
176 inner_type,
177 description=get_column_doc(column),
178 required=not (is_column_nullable(column)),
179 )
147180
148181
149182 @convert_sqlalchemy_type.register(postgresql.HSTORE)
150183 @convert_sqlalchemy_type.register(postgresql.JSON)
151184 @convert_sqlalchemy_type.register(postgresql.JSONB)
152185 def convert_json_to_string(type, column, registry=None):
153 return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column)))
186 return JSONString(
187 description=get_column_doc(column), required=not (is_column_nullable(column))
188 )
154189
155190
156191 @convert_sqlalchemy_type.register(JSONType)
157192 def convert_json_type_to_string(type, column, registry=None):
158 return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column)))
193 return JSONString(
194 description=get_column_doc(column), required=not (is_column_nullable(column))
195 )
0 import logging
01 from functools import partial
12
3 from promise import Promise, is_thenable
24 from sqlalchemy.orm.query import Query
35
4 from graphene.relay import ConnectionField
6 from graphene.relay import Connection, ConnectionField
57 from graphene.relay.connection import PageInfo
68 from graphql_relay.connection.arrayconnection import connection_from_list_slice
79
8 from .utils import get_query
10 from .utils import get_query, sort_argument_for_model
11
12 log = logging.getLogger()
913
1014
11 class SQLAlchemyConnectionField(ConnectionField):
15 class UnsortedSQLAlchemyConnectionField(ConnectionField):
16 @property
17 def type(self):
18 from .types import SQLAlchemyObjectType
19
20 _type = super(ConnectionField, self).type
21 if issubclass(_type, Connection):
22 return _type
23 assert issubclass(_type, SQLAlchemyObjectType), (
24 "SQLALchemyConnectionField only accepts SQLAlchemyObjectType types, not {}"
25 ).format(_type.__name__)
26 assert _type._meta.connection, "The type {} doesn't have a connection".format(
27 _type.__name__
28 )
29 return _type._meta.connection
1230
1331 @property
1432 def model(self):
1533 return self.type._meta.node._meta.model
1634
1735 @classmethod
18 def get_query(cls, model, info, **args):
19 return get_query(model, info.context)
20
21 @property
22 def type(self):
23 from .types import SQLAlchemyObjectType
24 _type = super(ConnectionField, self).type
25 assert issubclass(_type, SQLAlchemyObjectType), (
26 "SQLAlchemyConnectionField only accepts SQLAlchemyObjectType types"
27 )
28 assert _type._meta.connection, "The type {} doesn't have a connection".format(_type.__name__)
29 return _type._meta.connection
36 def get_query(cls, model, info, sort=None, **args):
37 query = get_query(model, info.context)
38 if sort is not None:
39 if isinstance(sort, str):
40 query = query.order_by(sort.value)
41 else:
42 query = query.order_by(*(col.value for col in sort))
43 return query
3044
3145 @classmethod
32 def connection_resolver(cls, resolver, connection, model, root, info, **args):
33 iterable = resolver(root, info, **args)
34 if iterable is None:
35 iterable = cls.get_query(model, info, **args)
36 if isinstance(iterable, Query):
37 _len = iterable.count()
46 def resolve_connection(cls, connection_type, model, info, args, resolved):
47 if resolved is None:
48 resolved = cls.get_query(model, info, **args)
49 if isinstance(resolved, Query):
50 _len = resolved.count()
3851 else:
39 _len = len(iterable)
52 _len = len(resolved)
4053 connection = connection_from_list_slice(
41 iterable,
54 resolved,
4255 args,
4356 slice_start=0,
4457 list_length=_len,
4558 list_slice_length=_len,
46 connection_type=connection,
59 connection_type=connection_type,
4760 pageinfo_type=PageInfo,
48 edge_type=connection.Edge,
61 edge_type=connection_type.Edge,
4962 )
50 connection.iterable = iterable
63 connection.iterable = resolved
5164 connection.length = _len
5265 return connection
66
67 @classmethod
68 def connection_resolver(cls, resolver, connection_type, model, root, info, **args):
69 resolved = resolver(root, info, **args)
70
71 on_resolve = partial(cls.resolve_connection, connection_type, model, info, args)
72 if is_thenable(resolved):
73 return Promise.resolve(resolved).then(on_resolve)
74
75 return on_resolve(resolved)
5376
5477 def get_resolver(self, parent_resolver):
5578 return partial(self.connection_resolver, parent_resolver, self.type, self.model)
5679
5780
58 __connectionFactory = SQLAlchemyConnectionField
81 class SQLAlchemyConnectionField(UnsortedSQLAlchemyConnectionField):
82 def __init__(self, type, *args, **kwargs):
83 if "sort" not in kwargs and issubclass(type, Connection):
84 # Let super class raise if type is not a Connection
85 try:
86 model = type.Edge.node._type._meta.model
87 kwargs.setdefault("sort", sort_argument_for_model(model))
88 except Exception:
89 raise Exception(
90 'Cannot create sort argument for {}. A model is required. Set the "sort" argument'
91 " to None to disabling the creation of the sort query argument".format(
92 type.__name__
93 )
94 )
95 elif "sort" in kwargs and kwargs["sort"] is None:
96 del kwargs["sort"]
97 super(SQLAlchemyConnectionField, self).__init__(type, *args, **kwargs)
98
99
100 def default_connection_field_factory(relationship, registry):
101 model = relationship.mapper.entity
102 model_type = registry.get_type_for_model(model)
103 return createConnectionField(model_type)
104
105
106 # TODO Remove in next major version
107 __connectionFactory = UnsortedSQLAlchemyConnectionField
59108
60109
61110 def createConnectionField(_type):
111 log.warn(
112 'createConnectionField is deprecated and will be removed in the next '
113 'major version. Use SQLAlchemyObjectType.Meta.connection_field_factory instead.'
114 )
62115 return __connectionFactory(_type)
63116
64117
65118 def registerConnectionFieldFactory(factoryMethod):
119 log.warn(
120 'registerConnectionFieldFactory is deprecated and will be removed in the next '
121 'major version. Use SQLAlchemyObjectType.Meta.connection_field_factory instead.'
122 )
66123 global __connectionFactory
67124 __connectionFactory = factoryMethod
68125
69126
70127 def unregisterConnectionFieldFactory():
128 log.warn(
129 'registerConnectionFieldFactory is deprecated and will be removed in the next '
130 'major version. Use SQLAlchemyObjectType.Meta.connection_field_factory instead.'
131 )
71132 global __connectionFactory
72 __connectionFactory = SQLAlchemyConnectionField
133 __connectionFactory = UnsortedSQLAlchemyConnectionField
00 class Registry(object):
1
21 def __init__(self):
32 self._registry = {}
43 self._registry_models = {}
65
76 def register(self, cls):
87 from .types import SQLAlchemyObjectType
8
99 assert issubclass(cls, SQLAlchemyObjectType), (
10 'Only classes of type SQLAlchemyObjectType can be registered, ',
10 "Only classes of type SQLAlchemyObjectType can be registered, "
1111 'received "{}"'
1212 ).format(cls.__name__)
13 assert cls._meta.registry == self, 'Registry for a Model have to match.'
13 assert cls._meta.registry == self, "Registry for a Model have to match."
1414 # assert self.get_type_for_model(cls._meta.model) in [None, cls], (
1515 # 'SQLAlchemy model "{}" already associated with '
1616 # 'another type "{}".'
00 from __future__ import absolute_import
11
2 from sqlalchemy import Column, Date, ForeignKey, Integer, String, Table
2 import enum
3
4 from sqlalchemy import Column, Date, Enum, ForeignKey, Integer, String, Table
35 from sqlalchemy.ext.declarative import declarative_base
46 from sqlalchemy.orm import mapper, relationship
57
8
9 class Hairkind(enum.Enum):
10 LONG = 'long'
11 SHORT = 'short'
12
13
614 Base = declarative_base()
715
8 association_table = Table('association', Base.metadata,
9 Column('pet_id', Integer, ForeignKey('pets.id')),
10 Column('reporter_id', Integer, ForeignKey('reporters.id')))
16 association_table = Table(
17 "association",
18 Base.metadata,
19 Column("pet_id", Integer, ForeignKey("pets.id")),
20 Column("reporter_id", Integer, ForeignKey("reporters.id")),
21 )
1122
1223
1324 class Editor(Base):
14 __tablename__ = 'editors'
25 __tablename__ = "editors"
1526 editor_id = Column(Integer(), primary_key=True)
1627 name = Column(String(100))
1728
1829
1930 class Pet(Base):
20 __tablename__ = 'pets'
31 __tablename__ = "pets"
2132 id = Column(Integer(), primary_key=True)
2233 name = Column(String(30))
23 reporter_id = Column(Integer(), ForeignKey('reporters.id'))
34 pet_kind = Column(Enum("cat", "dog", name="pet_kind"), nullable=False)
35 hair_kind = Column(Enum(Hairkind, name="hair_kind"), nullable=False)
36 reporter_id = Column(Integer(), ForeignKey("reporters.id"))
2437
2538
2639 class Reporter(Base):
27 __tablename__ = 'reporters'
40 __tablename__ = "reporters"
2841 id = Column(Integer(), primary_key=True)
2942 first_name = Column(String(30))
3043 last_name = Column(String(30))
3144 email = Column(String())
32 pets = relationship('Pet', secondary=association_table, backref='reporters')
33 articles = relationship('Article', backref='reporter')
45 pets = relationship("Pet", secondary=association_table, backref="reporters")
46 articles = relationship("Article", backref="reporter")
3447 favorite_article = relationship("Article", uselist=False)
3548
3649 # total = column_property(
4154
4255
4356 class Article(Base):
44 __tablename__ = 'articles'
57 __tablename__ = "articles"
4558 id = Column(Integer(), primary_key=True)
4659 headline = Column(String(100))
4760 pub_date = Column(Date())
48 reporter_id = Column(Integer(), ForeignKey('reporters.id'))
61 reporter_id = Column(Integer(), ForeignKey("reporters.id"))
4962
5063
5164 class ReflectedEditor(type):
5265 """Same as Editor, but using reflected table."""
66
5367 @classmethod
5468 def __subclasses__(cls):
5569 return []
5670
57 editor_table = Table('editors', Base.metadata, autoload=True)
71
72 editor_table = Table("editors", Base.metadata, autoload=True)
5873
5974 mapper(ReflectedEditor, editor_table)
+0
-28
graphene_sqlalchemy/tests/test_connectionfactory.py less more
0 from graphene_sqlalchemy.fields import SQLAlchemyConnectionField, registerConnectionFieldFactory, unregisterConnectionFieldFactory
1 import graphene
2
3 def test_register():
4 class LXConnectionField(SQLAlchemyConnectionField):
5 @classmethod
6 def _applyQueryArgs(cls, model, q, args):
7 return q
8
9 @classmethod
10 def connection_resolver(cls, resolver, connection, model, root, args, context, info):
11
12 def LXResolver(root, args, context, info):
13 iterable = resolver(root, args, context, info)
14 if iterable is None:
15 iterable = cls.get_query(model, context, info, args)
16
17 # We accept always a query here. All LX-queries can be filtered and sorted
18 iterable = cls._applyQueryArgs(model, iterable, args)
19 return iterable
20
21 return SQLAlchemyConnectionField.connection_resolver(LXResolver, connection, model, root, args, context, info)
22
23 def createLXConnectionField(table):
24 return LXConnectionField(table, filter=table.filter(), order_by=graphene.List(of_type=table.order_by))
25
26 registerConnectionFieldFactory(createLXConnectionField)
27 unregisterConnectionFieldFactory()
0 import enum
1
02 from py.test import raises
1 from sqlalchemy import Column, Table, case, types, select, func
3 from sqlalchemy import Column, Table, case, func, select, types
24 from sqlalchemy.dialects import postgresql
35 from sqlalchemy.ext.declarative import declarative_base
4 from sqlalchemy.orm import composite, column_property
6 from sqlalchemy.orm import column_property, composite
57 from sqlalchemy.sql.elements import Label
68 from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType
79
1315 from ..converter import (convert_sqlalchemy_column,
1416 convert_sqlalchemy_composite,
1517 convert_sqlalchemy_relationship)
16 from ..fields import SQLAlchemyConnectionField
18 from ..fields import (UnsortedSQLAlchemyConnectionField,
19 default_connection_field_factory)
1720 from ..registry import Registry
1821 from ..types import SQLAlchemyObjectType
1922 from .models import Article, Pet, Reporter
2023
2124
2225 def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
23 column = Column(sqlalchemy_type, doc='Custom Help Text', **kwargs)
26 column = Column(sqlalchemy_type, doc="Custom Help Text", **kwargs)
2427 graphene_type = convert_sqlalchemy_column(column)
2528 assert isinstance(graphene_type, graphene_field)
26 field = graphene_type.Field()
27 assert field.description == 'Custom Help Text'
29 field = (
30 graphene_type
31 if isinstance(graphene_type, graphene.Field)
32 else graphene_type.Field()
33 )
34 assert field.description == "Custom Help Text"
2835 return field
2936
3037
31 def assert_composite_conversion(composite_class, composite_columns, graphene_field,
32 registry, **kwargs):
33 composite_column = composite(composite_class, *composite_columns,
34 doc='Custom Help Text', **kwargs)
38 def assert_composite_conversion(
39 composite_class, composite_columns, graphene_field, registry, **kwargs
40 ):
41 composite_column = composite(
42 composite_class, *composite_columns, doc="Custom Help Text", **kwargs
43 )
3544 graphene_type = convert_sqlalchemy_composite(composite_column, registry)
3645 assert isinstance(graphene_type, graphene_field)
3746 field = graphene_type.Field()
4453 def test_should_unknown_sqlalchemy_field_raise_exception():
4554 with raises(Exception) as excinfo:
4655 convert_sqlalchemy_column(None)
47 assert 'Don\'t know how to convert the SQLAlchemy field' in str(excinfo.value)
56 assert "Don't know how to convert the SQLAlchemy field" in str(excinfo.value)
4857
4958
5059 def test_should_date_convert_string():
7584 assert_column_conversion(types.UnicodeText(), graphene.String)
7685
7786
78 def test_should_enum_convert_string():
79 assert_column_conversion(types.Enum(), graphene.String)
87 def test_should_enum_convert_enum():
88 field = assert_column_conversion(
89 types.Enum(enum.Enum("one", "two")), graphene.Field
90 )
91 field_type = field.type()
92 assert isinstance(field_type, graphene.Enum)
93 assert hasattr(field_type, "two")
94 field = assert_column_conversion(
95 types.Enum("one", "two", name="two_numbers"), graphene.Field
96 )
97 field_type = field.type()
98 assert field_type.__class__.__name__ == "two_numbers"
99 assert isinstance(field_type, graphene.Enum)
100 assert hasattr(field_type, "two")
80101
81102
82103 def test_should_small_integer_convert_int():
108129
109130
110131 def test_should_label_convert_string():
111 label = Label('label_test', case([], else_="foo"), type_=types.Unicode())
132 label = Label("label_test", case([], else_="foo"), type_=types.Unicode())
112133 graphene_type = convert_sqlalchemy_column(label)
113134 assert isinstance(graphene_type, graphene.String)
114135
115136
116137 def test_should_label_convert_int():
117 label = Label('int_label_test', case([], else_="foo"), type_=types.Integer())
138 label = Label("int_label_test", case([], else_="foo"), type_=types.Integer())
118139 graphene_type = convert_sqlalchemy_column(label)
119140 assert isinstance(graphene_type, graphene.Int)
120141
142
121143 def test_should_choice_convert_enum():
122 TYPES = [
123 (u'es', u'Spanish'),
124 (u'en', u'English')
125 ]
126 column = Column(ChoiceType(TYPES), doc='Language', name='language')
144 TYPES = [(u"es", u"Spanish"), (u"en", u"English")]
145 column = Column(ChoiceType(TYPES), doc="Language", name="language")
127146 Base = declarative_base()
128147
129 Table('translatedmodel', Base.metadata, column)
148 Table("translatedmodel", Base.metadata, column)
130149 graphene_type = convert_sqlalchemy_column(column)
131150 assert issubclass(graphene_type, graphene.Enum)
132 assert graphene_type._meta.name == 'TRANSLATEDMODEL_LANGUAGE'
133 assert graphene_type._meta.description == 'Language'
134 assert graphene_type._meta.enum.__members__['es'].value == 'Spanish'
135 assert graphene_type._meta.enum.__members__['en'].value == 'English'
151 assert graphene_type._meta.name == "TRANSLATEDMODEL_LANGUAGE"
152 assert graphene_type._meta.description == "Language"
153 assert graphene_type._meta.enum.__members__["es"].value == "Spanish"
154 assert graphene_type._meta.enum.__members__["en"].value == "English"
136155
137156
138157 def test_should_columproperty_convert():
140159 Base = declarative_base()
141160
142161 class Test(Base):
143 __tablename__ = 'test'
162 __tablename__ = "test"
144163 id = Column(types.Integer, primary_key=True)
145164 column = column_property(
146 select([func.sum(func.cast(id, types.Integer))]).where(
147 id==1
148 )
165 select([func.sum(func.cast(id, types.Integer))]).where(id == 1)
149166 )
150167
151168 graphene_type = convert_sqlalchemy_column(Test.column)
152 assert graphene_type.kwargs['required'] == False
169 assert not graphene_type.kwargs["required"]
153170
154171
155172 def test_should_scalar_list_convert_list():
162179
163180 def test_should_manytomany_convert_connectionorlist():
164181 registry = Registry()
165 dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, registry)
182 dynamic_field = convert_sqlalchemy_relationship(
183 Reporter.pets.property, registry, default_connection_field_factory
184 )
166185 assert isinstance(dynamic_field, graphene.Dynamic)
167186 assert not dynamic_field.get_type()
168187
169188
170189 def test_should_manytomany_convert_connectionorlist_list():
171190 class A(SQLAlchemyObjectType):
172
173191 class Meta:
174192 model = Pet
175193
176 dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry)
194 dynamic_field = convert_sqlalchemy_relationship(
195 Reporter.pets.property, A._meta.registry, default_connection_field_factory
196 )
177197 assert isinstance(dynamic_field, graphene.Dynamic)
178198 graphene_type = dynamic_field.get_type()
179199 assert isinstance(graphene_type, graphene.Field)
183203
184204 def test_should_manytomany_convert_connectionorlist_connection():
185205 class A(SQLAlchemyObjectType):
186
187206 class Meta:
188207 model = Pet
189 interfaces = (Node, )
190
191 dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry)
192 assert isinstance(dynamic_field, graphene.Dynamic)
193 assert isinstance(dynamic_field.get_type(), SQLAlchemyConnectionField)
208 interfaces = (Node,)
209
210 dynamic_field = convert_sqlalchemy_relationship(
211 Reporter.pets.property, A._meta.registry, default_connection_field_factory
212 )
213 assert isinstance(dynamic_field, graphene.Dynamic)
214 assert isinstance(dynamic_field.get_type(), UnsortedSQLAlchemyConnectionField)
194215
195216
196217 def test_should_manytoone_convert_connectionorlist():
197218 registry = Registry()
198 dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, registry)
219 dynamic_field = convert_sqlalchemy_relationship(
220 Article.reporter.property, registry, default_connection_field_factory
221 )
199222 assert isinstance(dynamic_field, graphene.Dynamic)
200223 assert not dynamic_field.get_type()
201224
202225
203226 def test_should_manytoone_convert_connectionorlist_list():
204227 class A(SQLAlchemyObjectType):
205
206228 class Meta:
207229 model = Reporter
208230
209 dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, A._meta.registry)
231 dynamic_field = convert_sqlalchemy_relationship(
232 Article.reporter.property, A._meta.registry, default_connection_field_factory
233 )
210234 assert isinstance(dynamic_field, graphene.Dynamic)
211235 graphene_type = dynamic_field.get_type()
212236 assert isinstance(graphene_type, graphene.Field)
215239
216240 def test_should_manytoone_convert_connectionorlist_connection():
217241 class A(SQLAlchemyObjectType):
218
219242 class Meta:
220243 model = Reporter
221 interfaces = (Node, )
222
223 dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, A._meta.registry)
244 interfaces = (Node,)
245
246 dynamic_field = convert_sqlalchemy_relationship(
247 Article.reporter.property, A._meta.registry, default_connection_field_factory
248 )
224249 assert isinstance(dynamic_field, graphene.Dynamic)
225250 graphene_type = dynamic_field.get_type()
226251 assert isinstance(graphene_type, graphene.Field)
229254
230255 def test_should_onetoone_convert_field():
231256 class A(SQLAlchemyObjectType):
232
233257 class Meta:
234258 model = Article
235 interfaces = (Node, )
236
237 dynamic_field = convert_sqlalchemy_relationship(Reporter.favorite_article.property, A._meta.registry)
259 interfaces = (Node,)
260
261 dynamic_field = convert_sqlalchemy_relationship(
262 Reporter.favorite_article.property, A._meta.registry, default_connection_field_factory
263 )
238264 assert isinstance(dynamic_field, graphene.Dynamic)
239265 graphene_type = dynamic_field.get_type()
240266 assert isinstance(graphene_type, graphene.Field)
246272
247273
248274 def test_should_postgresql_enum_convert():
249 assert_column_conversion(postgresql.ENUM(), graphene.String)
275 field = assert_column_conversion(
276 postgresql.ENUM("one", "two", name="two_numbers"), graphene.Field
277 )
278 field_type = field.type()
279 assert field_type.__class__.__name__ == "two_numbers"
280 assert isinstance(field_type, graphene.Enum)
281 assert hasattr(field_type, "two")
282
283
284 def test_should_postgresql_py_enum_convert():
285 field = assert_column_conversion(
286 postgresql.ENUM(enum.Enum("TwoNumbers", "one two"), name="two_numbers"), graphene.Field
287 )
288 field_type = field.type()
289 assert field_type.__class__.__name__ == "TwoNumbers"
290 assert isinstance(field_type, graphene.Enum)
291 assert hasattr(field_type, "two")
250292
251293
252294 def test_should_postgresql_array_convert():
266308
267309
268310 def test_should_composite_convert():
269
270311 class CompositeClass(object):
271
272312 def __init__(self, col1, col2):
273313 self.col1 = col1
274314 self.col2 = col2
279319 def convert_composite_class(composite, registry):
280320 return graphene.String(description=composite.doc)
281321
282 assert_composite_conversion(CompositeClass,
283 (Column(types.Unicode(50)),
284 Column(types.Unicode(50))),
285 graphene.String,
286 registry)
322 assert_composite_conversion(
323 CompositeClass,
324 (Column(types.Unicode(50)), Column(types.Unicode(50))),
325 graphene.String,
326 registry,
327 )
287328
288329
289330 def test_should_unknown_sqlalchemy_composite_raise_exception():
292333 with raises(Exception) as excinfo:
293334
294335 class CompositeClass(object):
295
296336 def __init__(self, col1, col2):
297337 self.col1 = col1
298338 self.col2 = col2
299339
300 assert_composite_conversion(CompositeClass,
301 (Column(types.Unicode(50)),
302 Column(types.Unicode(50))),
303 graphene.String,
304 registry)
305
306 assert 'Don\'t know how to convert the composite field' in str(excinfo.value)
340 assert_composite_conversion(
341 CompositeClass,
342 (Column(types.Unicode(50)), Column(types.Unicode(50))),
343 graphene.String,
344 registry,
345 )
346
347 assert "Don't know how to convert the composite field" in str(excinfo.value)
0 import pytest
1
2 from graphene.relay import Connection
3
4 from ..fields import SQLAlchemyConnectionField
5 from ..types import SQLAlchemyObjectType
6 from ..utils import sort_argument_for_model
7 from .models import Editor
8 from .models import Pet as PetModel
9
10
11 class Pet(SQLAlchemyObjectType):
12 class Meta:
13 model = PetModel
14
15
16 class PetConn(Connection):
17 class Meta:
18 node = Pet
19
20
21 def test_sort_added_by_default():
22 arg = SQLAlchemyConnectionField(PetConn)
23 assert "sort" in arg.args
24 assert arg.args["sort"] == sort_argument_for_model(PetModel)
25
26
27 def test_sort_can_be_removed():
28 arg = SQLAlchemyConnectionField(PetConn, sort=None)
29 assert "sort" not in arg.args
30
31
32 def test_custom_sort():
33 arg = SQLAlchemyConnectionField(PetConn, sort=sort_argument_for_model(Editor))
34 assert arg.args["sort"] == sort_argument_for_model(Editor)
35
36
37 def test_init_raises():
38 with pytest.raises(Exception, match="Cannot create sort"):
39 SQLAlchemyConnectionField(Connection)
22 from sqlalchemy.orm import scoped_session, sessionmaker
33
44 import graphene
5 from graphene.relay import Node
6
5 from graphene.relay import Connection, Node
6
7 from ..fields import SQLAlchemyConnectionField
78 from ..registry import reset_global_registry
8 from ..fields import SQLAlchemyConnectionField
99 from ..types import SQLAlchemyObjectType
10 from .models import Article, Base, Editor, Reporter
11
12 db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
13
14
15 @pytest.yield_fixture(scope='function')
10 from ..utils import sort_argument_for_model, sort_enum_for_model
11 from .models import Article, Base, Editor, Hairkind, Pet, Reporter
12
13 db = create_engine("sqlite:///test_sqlalchemy.sqlite3")
14
15
16 @pytest.yield_fixture(scope="function")
1617 def session():
1718 reset_global_registry()
1819 connection = db.engine.connect()
3233
3334
3435 def setup_fixtures(session):
35 reporter = Reporter(first_name='ABA', last_name='X')
36 pet = Pet(name="Lassie", pet_kind="dog", hair_kind=Hairkind.LONG)
37 session.add(pet)
38 reporter = Reporter(first_name="ABA", last_name="X")
3639 session.add(reporter)
37 reporter2 = Reporter(first_name='ABO', last_name='Y')
40 reporter2 = Reporter(first_name="ABO", last_name="Y")
3841 session.add(reporter2)
39 article = Article(headline='Hi!')
42 article = Article(headline="Hi!")
4043 article.reporter = reporter
4144 session.add(article)
4245 editor = Editor(name="John")
4851 setup_fixtures(session)
4952
5053 class ReporterType(SQLAlchemyObjectType):
51
5254 class Meta:
5355 model = Reporter
5456
6264 def resolve_reporters(self, *args, **kwargs):
6365 return session.query(Reporter)
6466
65 query = '''
67 query = """
6668 query ReporterQuery {
6769 reporter {
6870 firstName,
7375 firstName
7476 }
7577 }
76 '''
78 """
7779 expected = {
78 'reporter': {
79 'firstName': 'ABA',
80 'lastName': 'X',
81 'email': None
82 },
83 'reporters': [{
84 'firstName': 'ABA',
85 }, {
86 'firstName': 'ABO',
87 }]
80 "reporter": {"firstName": "ABA", "lastName": "X", "email": None},
81 "reporters": [{"firstName": "ABA"}, {"firstName": "ABO"}],
8882 }
8983 schema = graphene.Schema(query=Query)
9084 result = schema.execute(query)
9286 assert result.data == expected
9387
9488
89 def test_should_query_enums(session):
90 setup_fixtures(session)
91
92 class PetType(SQLAlchemyObjectType):
93 class Meta:
94 model = Pet
95
96 class Query(graphene.ObjectType):
97 pet = graphene.Field(PetType)
98
99 def resolve_pet(self, *args, **kwargs):
100 return session.query(Pet).first()
101
102 query = """
103 query PetQuery {
104 pet {
105 name,
106 petKind
107 hairKind
108 }
109 }
110 """
111 expected = {"pet": {"name": "Lassie", "petKind": "dog", "hairKind": "LONG"}}
112 schema = graphene.Schema(query=Query)
113 result = schema.execute(query)
114 assert not result.errors
115 assert result.data == expected, result.data
116
117
118 def test_enum_parameter(session):
119 setup_fixtures(session)
120
121 class PetType(SQLAlchemyObjectType):
122 class Meta:
123 model = Pet
124
125 class Query(graphene.ObjectType):
126 pet = graphene.Field(PetType, kind=graphene.Argument(PetType._meta.fields['pet_kind'].type.of_type))
127
128 def resolve_pet(self, info, kind=None, *args, **kwargs):
129 query = session.query(Pet)
130 if kind:
131 query = query.filter(Pet.pet_kind == kind)
132 return query.first()
133
134 query = """
135 query PetQuery($kind: pet_kind) {
136 pet(kind: $kind) {
137 name,
138 petKind
139 hairKind
140 }
141 }
142 """
143 expected = {"pet": {"name": "Lassie", "petKind": "dog", "hairKind": "LONG"}}
144 schema = graphene.Schema(query=Query)
145 result = schema.execute(query, variables={"kind": "cat"})
146 assert not result.errors
147 assert result.data == {"pet": None}
148 result = schema.execute(query, variables={"kind": "dog"})
149 assert not result.errors
150 assert result.data == expected, result.data
151
152
153 def test_py_enum_parameter(session):
154 setup_fixtures(session)
155
156 class PetType(SQLAlchemyObjectType):
157 class Meta:
158 model = Pet
159
160 class Query(graphene.ObjectType):
161 pet = graphene.Field(PetType, kind=graphene.Argument(PetType._meta.fields['hair_kind'].type.of_type))
162
163 def resolve_pet(self, info, kind=None, *args, **kwargs):
164 query = session.query(Pet)
165 if kind:
166 # XXX Why kind passed in as a str instead of a Hairkind instance?
167 query = query.filter(Pet.hair_kind == Hairkind(kind))
168 return query.first()
169
170 query = """
171 query PetQuery($kind: Hairkind) {
172 pet(kind: $kind) {
173 name,
174 petKind
175 hairKind
176 }
177 }
178 """
179 expected = {"pet": {"name": "Lassie", "petKind": "dog", "hairKind": "LONG"}}
180 schema = graphene.Schema(query=Query)
181 result = schema.execute(query, variables={"kind": "SHORT"})
182 assert not result.errors
183 assert result.data == {"pet": None}
184 result = schema.execute(query, variables={"kind": "LONG"})
185 assert not result.errors
186 assert result.data == expected, result.data
187
188
95189 def test_should_node(session):
96190 setup_fixtures(session)
97191
98192 class ReporterNode(SQLAlchemyObjectType):
99
100193 class Meta:
101194 model = Reporter
102 interfaces = (Node, )
195 interfaces = (Node,)
103196
104197 @classmethod
105 def get_node(cls, id, info):
106 return Reporter(id=2, first_name='Cookie Monster')
198 def get_node(cls, info, id):
199 return Reporter(id=2, first_name="Cookie Monster")
107200
108201 class ArticleNode(SQLAlchemyObjectType):
109
110202 class Meta:
111203 model = Article
112 interfaces = (Node, )
204 interfaces = (Node,)
113205
114206 # @classmethod
115207 # def get_node(cls, id, info):
116208 # return Article(id=1, headline='Article node')
117209
210 class ArticleConnection(Connection):
211 class Meta:
212 node = ArticleNode
213
118214 class Query(graphene.ObjectType):
119215 node = Node.Field()
120216 reporter = graphene.Field(ReporterNode)
121217 article = graphene.Field(ArticleNode)
122 all_articles = SQLAlchemyConnectionField(ArticleNode)
218 all_articles = SQLAlchemyConnectionField(ArticleConnection)
123219
124220 def resolve_reporter(self, *args, **kwargs):
125221 return session.query(Reporter).first()
127223 def resolve_article(self, *args, **kwargs):
128224 return session.query(Article).first()
129225
130 query = '''
226 query = """
131227 query ReporterQuery {
132228 reporter {
133229 id,
159255 }
160256 }
161257 }
162 '''
258 """
163259 expected = {
164 'reporter': {
165 'id': 'UmVwb3J0ZXJOb2RlOjE=',
166 'firstName': 'ABA',
167 'lastName': 'X',
168 'email': None,
169 'articles': {
170 'edges': [{
171 'node': {
172 'headline': 'Hi!'
173 }
174 }]
175 },
260 "reporter": {
261 "id": "UmVwb3J0ZXJOb2RlOjE=",
262 "firstName": "ABA",
263 "lastName": "X",
264 "email": None,
265 "articles": {"edges": [{"node": {"headline": "Hi!"}}]},
176266 },
177 'allArticles': {
178 'edges': [{
179 'node': {
180 'headline': 'Hi!'
181 }
182 }]
183 },
184 'myArticle': {
185 'id': 'QXJ0aWNsZU5vZGU6MQ==',
186 'headline': 'Hi!'
187 }
267 "allArticles": {"edges": [{"node": {"headline": "Hi!"}}]},
268 "myArticle": {"id": "QXJ0aWNsZU5vZGU6MQ==", "headline": "Hi!"},
188269 }
189270 schema = graphene.Schema(query=Query)
190 result = schema.execute(query, context_value={'session': session})
271 result = schema.execute(query, context_value={"session": session})
191272 assert not result.errors
192273 assert result.data == expected
193274
196277 setup_fixtures(session)
197278
198279 class EditorNode(SQLAlchemyObjectType):
199
200280 class Meta:
201281 model = Editor
202 interfaces = (Node, )
282 interfaces = (Node,)
283
284 class EditorConnection(Connection):
285 class Meta:
286 node = EditorNode
203287
204288 class Query(graphene.ObjectType):
205289 node = Node.Field()
206 all_editors = SQLAlchemyConnectionField(EditorNode)
207
208 query = '''
290 all_editors = SQLAlchemyConnectionField(EditorConnection)
291
292 query = """
209293 query EditorQuery {
210294 allEditors {
211295 edges {
221305 }
222306 }
223307 }
224 '''
308 """
225309 expected = {
226 'allEditors': {
227 'edges': [{
228 'node': {
229 'id': 'RWRpdG9yTm9kZTox',
230 'name': 'John'
231 }
232 }]
233 },
234 'node': {
235 'name': 'John'
236 }
310 "allEditors": {"edges": [{"node": {"id": "RWRpdG9yTm9kZTox", "name": "John"}}]},
311 "node": {"name": "John"},
237312 }
238313
239314 schema = graphene.Schema(query=Query)
240 result = schema.execute(query, context_value={'session': session})
315 result = schema.execute(query, context_value={"session": session})
241316 assert not result.errors
242317 assert result.data == expected
243318
246321 setup_fixtures(session)
247322
248323 class EditorNode(SQLAlchemyObjectType):
249
250324 class Meta:
251325 model = Editor
252 interfaces = (Node, )
326 interfaces = (Node,)
253327
254328 class ReporterNode(SQLAlchemyObjectType):
255
256329 class Meta:
257330 model = Reporter
258 interfaces = (Node, )
331 interfaces = (Node,)
259332
260333 @classmethod
261334 def get_node(cls, id, info):
262 return Reporter(id=2, first_name='Cookie Monster')
335 return Reporter(id=2, first_name="Cookie Monster")
263336
264337 class ArticleNode(SQLAlchemyObjectType):
265
266338 class Meta:
267339 model = Article
268 interfaces = (Node, )
340 interfaces = (Node,)
269341
270342 class CreateArticle(graphene.Mutation):
271
272343 class Arguments:
273344 headline = graphene.String()
274345 reporter_id = graphene.ID()
277348 article = graphene.Field(ArticleNode)
278349
279350 def mutate(self, info, headline, reporter_id):
280 new_article = Article(
281 headline=headline,
282 reporter_id=reporter_id,
283 )
351 new_article = Article(headline=headline, reporter_id=reporter_id)
284352
285353 session.add(new_article)
286354 session.commit()
294362 class Mutation(graphene.ObjectType):
295363 create_article = CreateArticle.Field()
296364
297 query = '''
365 query = """
298366 mutation ArticleCreator {
299367 createArticle(
300368 headline: "My Article"
310378 }
311379 }
312380 }
313 '''
381 """
314382 expected = {
315 'createArticle': {
316 'ok': True,
317 'article': {
318 'headline': 'My Article',
319 'reporter': {
320 'id': 'UmVwb3J0ZXJOb2RlOjE=',
321 'firstName': 'ABA'
322 }
323 }
324 },
383 "createArticle": {
384 "ok": True,
385 "article": {
386 "headline": "My Article",
387 "reporter": {"id": "UmVwb3J0ZXJOb2RlOjE=", "firstName": "ABA"},
388 },
389 }
325390 }
326391
327392 schema = graphene.Schema(query=Query, mutation=Mutation)
328 result = schema.execute(query, context_value={'session': session})
393 result = schema.execute(query, context_value={"session": session})
329394 assert not result.errors
330395 assert result.data == expected
396
397
398 def sort_setup(session):
399 pets = [
400 Pet(id=2, name="Lassie", pet_kind="dog", hair_kind=Hairkind.LONG),
401 Pet(id=22, name="Alf", pet_kind="cat", hair_kind=Hairkind.LONG),
402 Pet(id=3, name="Barf", pet_kind="dog", hair_kind=Hairkind.LONG),
403 ]
404 session.add_all(pets)
405 session.commit()
406
407
408 def test_sort(session):
409 sort_setup(session)
410
411 class PetNode(SQLAlchemyObjectType):
412 class Meta:
413 model = Pet
414 interfaces = (Node,)
415
416 class PetConnection(Connection):
417 class Meta:
418 node = PetNode
419
420 class Query(graphene.ObjectType):
421 defaultSort = SQLAlchemyConnectionField(PetConnection)
422 nameSort = SQLAlchemyConnectionField(PetConnection)
423 multipleSort = SQLAlchemyConnectionField(PetConnection)
424 descSort = SQLAlchemyConnectionField(PetConnection)
425 singleColumnSort = SQLAlchemyConnectionField(
426 PetConnection, sort=graphene.Argument(sort_enum_for_model(Pet))
427 )
428 noDefaultSort = SQLAlchemyConnectionField(
429 PetConnection, sort=sort_argument_for_model(Pet, False)
430 )
431 noSort = SQLAlchemyConnectionField(PetConnection, sort=None)
432
433 query = """
434 query sortTest {
435 defaultSort{
436 edges{
437 node{
438 id
439 }
440 }
441 }
442 nameSort(sort: name_asc){
443 edges{
444 node{
445 name
446 }
447 }
448 }
449 multipleSort(sort: [pet_kind_asc, name_desc]){
450 edges{
451 node{
452 name
453 petKind
454 }
455 }
456 }
457 descSort(sort: [name_desc]){
458 edges{
459 node{
460 name
461 }
462 }
463 }
464 singleColumnSort(sort: name_desc){
465 edges{
466 node{
467 name
468 }
469 }
470 }
471 noDefaultSort(sort: name_asc){
472 edges{
473 node{
474 name
475 }
476 }
477 }
478 }
479 """
480
481 def makeNodes(nodeList):
482 nodes = [{"node": item} for item in nodeList]
483 return {"edges": nodes}
484
485 expected = {
486 "defaultSort": makeNodes(
487 [{"id": "UGV0Tm9kZToy"}, {"id": "UGV0Tm9kZToz"}, {"id": "UGV0Tm9kZToyMg=="}]
488 ),
489 "nameSort": makeNodes([{"name": "Alf"}, {"name": "Barf"}, {"name": "Lassie"}]),
490 "noDefaultSort": makeNodes(
491 [{"name": "Alf"}, {"name": "Barf"}, {"name": "Lassie"}]
492 ),
493 "multipleSort": makeNodes(
494 [
495 {"name": "Alf", "petKind": "cat"},
496 {"name": "Lassie", "petKind": "dog"},
497 {"name": "Barf", "petKind": "dog"},
498 ]
499 ),
500 "descSort": makeNodes([{"name": "Lassie"}, {"name": "Barf"}, {"name": "Alf"}]),
501 "singleColumnSort": makeNodes(
502 [{"name": "Lassie"}, {"name": "Barf"}, {"name": "Alf"}]
503 ),
504 } # yapf: disable
505
506 schema = graphene.Schema(query=Query)
507 result = schema.execute(query, context_value={"session": session})
508 assert not result.errors
509 assert result.data == expected
510
511 queryError = """
512 query sortTest {
513 singleColumnSort(sort: [pet_kind_asc, name_desc]){
514 edges{
515 node{
516 name
517 }
518 }
519 }
520 }
521 """
522 result = schema.execute(queryError, context_value={"session": session})
523 assert result.errors is not None
524
525 queryNoSort = """
526 query sortTest {
527 noDefaultSort{
528 edges{
529 node{
530 name
531 }
532 }
533 }
534 noSort{
535 edges{
536 node{
537 name
538 }
539 }
540 }
541 }
542 """
543
544 expectedNoSort = {
545 "noDefaultSort": makeNodes(
546 [{"name": "Alf"}, {"name": "Barf"}, {"name": "Lassie"}]
547 ),
548 "noSort": makeNodes([{"name": "Alf"}, {"name": "Barf"}, {"name": "Lassie"}]),
549 } # yapf: disable
550
551 result = schema.execute(queryNoSort, context_value={"session": session})
552 assert not result.errors
553 for key, value in result.data.items():
554 assert set(node["node"]["name"] for node in value["edges"]) == set(
555 node["node"]["name"] for node in expectedNoSort[key]["edges"]
556 )
88
99
1010 class Reflected(SQLAlchemyObjectType):
11
1211 class Meta:
1312 model = ReflectedEditor
1413 registry = registry
1716 def test_objecttype_registered():
1817 assert issubclass(Reflected, ObjectType)
1918 assert Reflected._meta.model == ReflectedEditor
20 assert list(
21 Reflected._meta.fields.keys()) == ['editor_id', 'name']
22
23
19 assert list(Reflected._meta.fields.keys()) == ["editor_id", "name"]
0 import pytest
1
2 from ..registry import Registry
3 from ..types import SQLAlchemyObjectType
4 from .models import Pet
5
6
7 def test_register_incorrect_objecttype():
8 reg = Registry()
9
10 class Spam:
11 pass
12
13 with pytest.raises(AssertionError) as excinfo:
14 reg.register(Spam)
15
16 assert "Only classes of type SQLAlchemyObjectType can be registered" in str(
17 excinfo.value
18 )
19
20
21 def test_register_objecttype():
22 reg = Registry()
23
24 class PetType(SQLAlchemyObjectType):
25 class Meta:
26 model = Pet
27 registry = reg
28
29 try:
30 reg.register(PetType)
31 except AssertionError:
32 pytest.fail("expected no AssertionError")
66
77 def test_should_raise_if_no_model():
88 with raises(Exception) as excinfo:
9
910 class Character1(SQLAlchemyObjectType):
1011 pass
11 assert 'valid SQLAlchemy Model' in str(excinfo.value)
12
13 assert "valid SQLAlchemy Model" in str(excinfo.value)
1214
1315
1416 def test_should_raise_if_model_is_invalid():
1517 with raises(Exception) as excinfo:
18
1619 class Character2(SQLAlchemyObjectType):
17
1820 class Meta:
1921 model = 1
20 assert 'valid SQLAlchemy Model' in str(excinfo.value)
22
23 assert "valid SQLAlchemy Model" in str(excinfo.value)
2124
2225
2326 def test_should_map_fields_correctly():
2427 class ReporterType2(SQLAlchemyObjectType):
25
2628 class Meta:
2729 model = Reporter
2830 registry = Registry()
2931
30 assert list(
31 ReporterType2._meta.fields.keys()) == [
32 'id',
33 'first_name',
34 'last_name',
35 'email',
36 'pets',
37 'articles',
38 'favorite_article']
32 assert list(ReporterType2._meta.fields.keys()) == [
33 "id",
34 "first_name",
35 "last_name",
36 "email",
37 "pets",
38 "articles",
39 "favorite_article",
40 ]
3941
4042
4143 def test_should_map_only_few_fields():
4244 class Reporter2(SQLAlchemyObjectType):
43
4445 class Meta:
4546 model = Reporter
46 only_fields = ('id', 'email')
47 assert list(Reporter2._meta.fields.keys()) == ['id', 'email']
47 only_fields = ("id", "email")
48 assert list(Reporter2._meta.fields.keys()) == ["id", "email"]
0
1 from graphene import Field, Int, Interface, ObjectType
2 from graphene.relay import Node, is_node
3 import six
4
0 from collections import OrderedDict
1
2 import six # noqa F401
3 from promise import Promise
4
5 from graphene import (Connection, Field, Int, Interface, Node, ObjectType,
6 is_node)
7
8 from ..fields import (SQLAlchemyConnectionField,
9 UnsortedSQLAlchemyConnectionField,
10 registerConnectionFieldFactory,
11 unregisterConnectionFieldFactory)
512 from ..registry import Registry
6 from ..types import SQLAlchemyObjectType
13 from ..types import SQLAlchemyObjectType, SQLAlchemyObjectTypeOptions
714 from .models import Article, Reporter
815
916 registry = Registry()
1017
1118
1219 class Character(SQLAlchemyObjectType):
13 '''Character description'''
20 """Character description"""
21
1422 class Meta:
1523 model = Reporter
1624 registry = registry
1725
1826
1927 class Human(SQLAlchemyObjectType):
20 '''Human description'''
28 """Human description"""
2129
2230 pub_date = Int()
2331
2432 class Meta:
2533 model = Article
26 exclude_fields = ('id', )
34 exclude_fields = ("id",)
2735 registry = registry
28 interfaces = (Node, )
36 interfaces = (Node,)
2937
3038
3139 def test_sqlalchemy_interface():
4351 def test_objecttype_registered():
4452 assert issubclass(Character, ObjectType)
4553 assert Character._meta.model == Reporter
46 assert list(
47 Character._meta.fields.keys()) == [
48 'id',
49 'first_name',
50 'last_name',
51 'email',
52 'pets',
53 'articles',
54 'favorite_article']
54 assert list(Character._meta.fields.keys()) == [
55 "id",
56 "first_name",
57 "last_name",
58 "email",
59 "pets",
60 "articles",
61 "favorite_article",
62 ]
5563
5664
5765 # def test_sqlalchemynode_idfield():
6573
6674
6775 def test_node_replacedfield():
68 idfield = Human._meta.fields['pub_date']
76 idfield = Human._meta.fields["pub_date"]
6977 assert isinstance(idfield, Field)
7078 assert idfield.type == Int
7179
7280
7381 def test_object_type():
74
75
7682 class Human(SQLAlchemyObjectType):
77 '''Human description'''
83 """Human description"""
7884
7985 pub_date = Int()
8086
8288 model = Article
8389 # exclude_fields = ('id', )
8490 registry = registry
85 interfaces = (Node, )
91 interfaces = (Node,)
8692
8793 assert issubclass(Human, ObjectType)
88 assert list(Human._meta.fields.keys()) == ['id', 'headline', 'pub_date', 'reporter_id', 'reporter']
94 assert list(Human._meta.fields.keys()) == [
95 "id",
96 "headline",
97 "pub_date",
98 "reporter_id",
99 "reporter",
100 ]
89101 assert is_node(Human)
90
91102
92103
93104 # Test Custom SQLAlchemyObjectType Implementation
97108
98109
99110 class CustomCharacter(CustomSQLAlchemyObjectType):
100 '''Character description'''
111 """Character description"""
112
101113 class Meta:
102114 model = Reporter
103115 registry = registry
106118 def test_custom_objecttype_registered():
107119 assert issubclass(CustomCharacter, ObjectType)
108120 assert CustomCharacter._meta.model == Reporter
109 assert list(
110 CustomCharacter._meta.fields.keys()) == [
111 'id',
112 'first_name',
113 'last_name',
114 'email',
115 'pets',
116 'articles',
117 'favorite_article']
121 assert list(CustomCharacter._meta.fields.keys()) == [
122 "id",
123 "first_name",
124 "last_name",
125 "email",
126 "pets",
127 "articles",
128 "favorite_article",
129 ]
130
131
132 # Test Custom SQLAlchemyObjectType with Custom Options
133 class CustomOptions(SQLAlchemyObjectTypeOptions):
134 custom_option = None
135 custom_fields = None
136
137
138 class SQLAlchemyObjectTypeWithCustomOptions(SQLAlchemyObjectType):
139 class Meta:
140 abstract = True
141
142 @classmethod
143 def __init_subclass_with_meta__(
144 cls, custom_option=None, custom_fields=None, **options
145 ):
146 _meta = CustomOptions(cls)
147 _meta.custom_option = custom_option
148 _meta.fields = custom_fields
149 super(SQLAlchemyObjectTypeWithCustomOptions, cls).__init_subclass_with_meta__(
150 _meta=_meta, **options
151 )
152
153
154 class ReporterWithCustomOptions(SQLAlchemyObjectTypeWithCustomOptions):
155 class Meta:
156 model = Reporter
157 custom_option = "custom_option"
158 custom_fields = OrderedDict([("custom_field", Field(Int()))])
159
160
161 def test_objecttype_with_custom_options():
162 assert issubclass(ReporterWithCustomOptions, ObjectType)
163 assert ReporterWithCustomOptions._meta.model == Reporter
164 assert list(ReporterWithCustomOptions._meta.fields.keys()) == [
165 "custom_field",
166 "id",
167 "first_name",
168 "last_name",
169 "email",
170 "pets",
171 "articles",
172 "favorite_article",
173 ]
174 assert ReporterWithCustomOptions._meta.custom_option == "custom_option"
175 assert isinstance(ReporterWithCustomOptions._meta.fields["custom_field"].type, Int)
176
177
178 def test_promise_connection_resolver():
179 class TestConnection(Connection):
180 class Meta:
181 node = ReporterWithCustomOptions
182
183 def resolver(*args, **kwargs):
184 return Promise.resolve([])
185
186 result = SQLAlchemyConnectionField.connection_resolver(
187 resolver, TestConnection, ReporterWithCustomOptions, None, None
188 )
189 assert result is not None
190
191
192 # Tests for connection_field_factory
193
194 class _TestSQLAlchemyConnectionField(SQLAlchemyConnectionField):
195 pass
196
197
198 def test_default_connection_field_factory():
199 _registry = Registry()
200
201 class ReporterType(SQLAlchemyObjectType):
202 class Meta:
203 model = Reporter
204 registry = _registry
205 interfaces = (Node,)
206
207 class ArticleType(SQLAlchemyObjectType):
208 class Meta:
209 model = Article
210 registry = _registry
211 interfaces = (Node,)
212
213 assert isinstance(ReporterType._meta.fields['articles'].type(), UnsortedSQLAlchemyConnectionField)
214
215
216 def test_register_connection_field_factory():
217 def test_connection_field_factory(relationship, registry):
218 model = relationship.mapper.entity
219 _type = registry.get_type_for_model(model)
220 return _TestSQLAlchemyConnectionField(_type._meta.connection)
221
222 _registry = Registry()
223
224 class ReporterType(SQLAlchemyObjectType):
225 class Meta:
226 model = Reporter
227 registry = _registry
228 interfaces = (Node,)
229 connection_field_factory = test_connection_field_factory
230
231 class ArticleType(SQLAlchemyObjectType):
232 class Meta:
233 model = Article
234 registry = _registry
235 interfaces = (Node,)
236
237 assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField)
238
239
240 def test_deprecated_registerConnectionFieldFactory():
241 registerConnectionFieldFactory(_TestSQLAlchemyConnectionField)
242
243 _registry = Registry()
244
245 class ReporterType(SQLAlchemyObjectType):
246 class Meta:
247 model = Reporter
248 registry = _registry
249 interfaces = (Node,)
250
251 class ArticleType(SQLAlchemyObjectType):
252 class Meta:
253 model = Article
254 registry = _registry
255 interfaces = (Node,)
256
257 assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField)
258
259
260 def test_deprecated_unregisterConnectionFieldFactory():
261 registerConnectionFieldFactory(_TestSQLAlchemyConnectionField)
262 unregisterConnectionFieldFactory()
263
264 _registry = Registry()
265
266 class ReporterType(SQLAlchemyObjectType):
267 class Meta:
268 model = Reporter
269 registry = _registry
270 interfaces = (Node,)
271
272 class ArticleType(SQLAlchemyObjectType):
273 class Meta:
274 model = Article
275 registry = _registry
276 interfaces = (Node,)
277
278 assert not isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField)
0 from graphene import ObjectType, Schema, String
0 import sqlalchemy as sa
11
2 from ..utils import get_session
2 from graphene import Enum, List, ObjectType, Schema, String
3
4 from ..utils import get_session, sort_argument_for_model, sort_enum_for_model
5 from .models import Editor, Pet
36
47
58 def test_get_session():
6 session = 'My SQLAlchemy session'
9 session = "My SQLAlchemy session"
710
811 class Query(ObjectType):
912 x = String()
1114 def resolve_x(self, info):
1215 return get_session(info.context)
1316
14 query = '''
17 query = """
1518 query ReporterQuery {
1619 x
1720 }
18 '''
21 """
1922
2023 schema = Schema(query=Query)
21 result = schema.execute(query, context_value={'session': session})
24 result = schema.execute(query, context_value={"session": session})
2225 assert not result.errors
23 assert result.data['x'] == session
26 assert result.data["x"] == session
27
28
29 def test_sort_enum_for_model():
30 enum = sort_enum_for_model(Pet)
31 assert isinstance(enum, type(Enum))
32 assert str(enum) == "PetSortEnum"
33 for col in sa.inspect(Pet).columns:
34 assert hasattr(enum, col.name + "_asc")
35 assert hasattr(enum, col.name + "_desc")
36
37
38 def test_sort_enum_for_model_custom_naming():
39 enum = sort_enum_for_model(Pet, "Foo", lambda n, d: n.upper() + ("A" if d else "D"))
40 assert str(enum) == "Foo"
41 for col in sa.inspect(Pet).columns:
42 assert hasattr(enum, col.name.upper() + "A")
43 assert hasattr(enum, col.name.upper() + "D")
44
45
46 def test_enum_cache():
47 assert sort_enum_for_model(Editor) is sort_enum_for_model(Editor)
48
49
50 def test_sort_argument_for_model():
51 arg = sort_argument_for_model(Pet)
52
53 assert isinstance(arg.type, List)
54 assert arg.default_value == [Pet.id.name + "_asc"]
55 assert arg.type.of_type == sort_enum_for_model(Pet)
56
57
58 def test_sort_argument_for_model_no_default():
59 arg = sort_argument_for_model(Pet, False)
60
61 assert arg.default_value is None
62
63
64 def test_sort_argument_for_model_multiple_pk():
65 Base = sa.ext.declarative.declarative_base()
66
67 class MultiplePK(Base):
68 foo = sa.Column(sa.Integer, primary_key=True)
69 bar = sa.Column(sa.Integer, primary_key=True)
70 __tablename__ = "MultiplePK"
71
72 arg = sort_argument_for_model(MultiplePK)
73 assert set(arg.default_value) == set(
74 (MultiplePK.foo.name + "_asc", MultiplePK.bar.name + "_asc")
75 )
00 from collections import OrderedDict
11
2 import sqlalchemy
3 from sqlalchemy.ext.hybrid import hybrid_property
24 from sqlalchemy.inspection import inspect as sqlalchemyinspect
3 from sqlalchemy.ext.hybrid import hybrid_property
45 from sqlalchemy.orm.exc import NoResultFound
56
67 from graphene import Field # , annotate, ResolveInfo
1011
1112 from .converter import (convert_sqlalchemy_column,
1213 convert_sqlalchemy_composite,
13 convert_sqlalchemy_relationship,
14 convert_sqlalchemy_hybrid_method)
14 convert_sqlalchemy_hybrid_method,
15 convert_sqlalchemy_relationship)
16 from .fields import default_connection_field_factory
1517 from .registry import Registry, get_global_registry
1618 from .utils import get_query, is_mapped_class, is_mapped_instance
1719
1820
19 def construct_fields(model, registry, only_fields, exclude_fields):
21 def construct_fields(model, registry, only_fields, exclude_fields, connection_field_factory):
2022 inspected_model = sqlalchemyinspect(model)
2123
2224 fields = OrderedDict()
5759 # in there. Or when we exclude this field in exclude_fields
5860 continue
5961
60 converted_hybrid_property = convert_sqlalchemy_hybrid_method(
61 hybrid_item
62 )
62 converted_hybrid_property = convert_sqlalchemy_hybrid_method(hybrid_item)
6363 fields[name] = converted_hybrid_property
6464
6565 # Get all the columns for the relationships on the model
7171 # We skip this field if we specify only_fields and is not
7272 # in there. Or when we exclude this field in exclude_fields
7373 continue
74 converted_relationship = convert_sqlalchemy_relationship(relationship, registry)
74 converted_relationship = convert_sqlalchemy_relationship(relationship, registry, connection_field_factory)
7575 name = relationship.key
7676 fields[name] = converted_relationship
7777
7979
8080
8181 class SQLAlchemyObjectTypeOptions(ObjectTypeOptions):
82 model = None # type: Model
83 registry = None # type: Registry
84 connection = None # type: Type[Connection]
82 model = None # type: sqlalchemy.Model
83 registry = None # type: sqlalchemy.Registry
84 connection = None # type: sqlalchemy.Type[sqlalchemy.Connection]
8585 id = None # type: str
8686
8787
8888 class SQLAlchemyObjectType(ObjectType):
8989 @classmethod
90 def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
91 only_fields=(), exclude_fields=(), connection=None,
92 use_connection=None, interfaces=(), id=None, **options):
90 def __init_subclass_with_meta__(
91 cls,
92 model=None,
93 registry=None,
94 skip_registry=False,
95 only_fields=(),
96 exclude_fields=(),
97 connection=None,
98 connection_class=None,
99 use_connection=None,
100 interfaces=(),
101 id=None,
102 connection_field_factory=default_connection_field_factory,
103 _meta=None,
104 **options
105 ):
93106 assert is_mapped_class(model), (
94 'You need to pass a valid SQLAlchemy Model in '
95 '{}.Meta, received "{}".'
107 "You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
96108 ).format(cls.__name__, model)
97109
98110 if not registry:
99111 registry = get_global_registry()
100112
101113 assert isinstance(registry, Registry), (
102 'The attribute registry in {} needs to be an instance of '
114 "The attribute registry in {} needs to be an instance of "
103115 'Registry, received "{}".'
104116 ).format(cls.__name__, registry)
105117
106118 sqla_fields = yank_fields_from_attrs(
107 construct_fields(model, registry, only_fields, exclude_fields),
108 _as=Field,
119 construct_fields(
120 model=model,
121 registry=registry,
122 only_fields=only_fields,
123 exclude_fields=exclude_fields,
124 connection_field_factory=connection_field_factory
125 ),
126 _as=Field
109127 )
110128
111129 if use_connection is None and interfaces:
112 use_connection = any((issubclass(interface, Node) for interface in interfaces))
130 use_connection = any(
131 (issubclass(interface, Node) for interface in interfaces)
132 )
113133
114134 if use_connection and not connection:
115135 # We create the connection automatically
116 connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls)
136 if not connection_class:
137 connection_class = Connection
138
139 connection = connection_class.create_type(
140 "{}Connection".format(cls.__name__), node=cls
141 )
117142
118143 if connection is not None:
119144 assert issubclass(connection, Connection), (
120145 "The connection must be a Connection. Received {}"
121146 ).format(connection.__name__)
122147
123 _meta = SQLAlchemyObjectTypeOptions(cls)
148 if not _meta:
149 _meta = SQLAlchemyObjectTypeOptions(cls)
150
124151 _meta.model = model
125152 _meta.registry = registry
126 _meta.fields = sqla_fields
153
154 if _meta.fields:
155 _meta.fields.update(sqla_fields)
156 else:
157 _meta.fields = sqla_fields
158
127159 _meta.connection = connection
128 _meta.id = id or 'id'
160 _meta.id = id or "id"
129161
130 super(SQLAlchemyObjectType, cls).__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options)
162 super(SQLAlchemyObjectType, cls).__init_subclass_with_meta__(
163 _meta=_meta, interfaces=interfaces, **options
164 )
131165
132166 if not skip_registry:
133167 registry.register(cls)
137171 if isinstance(root, cls):
138172 return True
139173 if not is_mapped_instance(root):
140 raise Exception((
141 'Received incompatible instance "{}".'
142 ).format(root))
174 raise Exception(('Received incompatible instance "{}".').format(root))
143175 return isinstance(root, cls._meta.model)
144176
145177 @classmethod
00 from sqlalchemy.exc import ArgumentError
1 from sqlalchemy.inspection import inspect
12 from sqlalchemy.orm import class_mapper, object_mapper
23 from sqlalchemy.orm.exc import UnmappedClassError, UnmappedInstanceError
34
5 from graphene import Argument, Enum, List
6
47
58 def get_session(context):
6 return context.get('session')
9 return context.get("session")
710
811
912 def get_query(model, context):
10 query = getattr(model, 'query', None)
13 query = getattr(model, "query", None)
1114 if not query:
1215 session = get_session(context)
1316 if not session:
14 raise Exception('A query in the model Base or a session in the schema is required for querying.\n'
15 'Read more http://graphene-python.org/docs/sqlalchemy/tips/#querying')
17 raise Exception(
18 "A query in the model Base or a session in the schema is required for querying.\n"
19 "Read more http://docs.graphene-python.org/projects/sqlalchemy/en/latest/tips/#querying"
20 )
1621 query = session.query(model)
1722 return query
1823
3338 return False
3439 else:
3540 return True
41
42
43 def _symbol_name(column_name, is_asc):
44 return column_name + ("_asc" if is_asc else "_desc")
45
46
47 class EnumValue(str):
48 """Subclass of str that stores a string and an arbitrary value in the "value" property"""
49
50 def __new__(cls, str_value, value):
51 return super(EnumValue, cls).__new__(cls, str_value)
52
53 def __init__(self, str_value, value):
54 super(EnumValue, self).__init__()
55 self.value = value
56
57
58 # Cache for the generated enums, to avoid name clash
59 _ENUM_CACHE = {}
60
61
62 def _sort_enum_for_model(cls, name=None, symbol_name=_symbol_name):
63 name = name or cls.__name__ + "SortEnum"
64 if name in _ENUM_CACHE:
65 return _ENUM_CACHE[name]
66 items = []
67 default = []
68 for column in inspect(cls).columns.values():
69 asc_name = symbol_name(column.name, True)
70 asc_value = EnumValue(asc_name, column.asc())
71 desc_name = symbol_name(column.name, False)
72 desc_value = EnumValue(desc_name, column.desc())
73 if column.primary_key:
74 default.append(asc_value)
75 items.extend(((asc_name, asc_value), (desc_name, desc_value)))
76 enum = Enum(name, items)
77 _ENUM_CACHE[name] = (enum, default)
78 return enum, default
79
80
81 def sort_enum_for_model(cls, name=None, symbol_name=_symbol_name):
82 """Create Graphene Enum for sorting a SQLAlchemy class query
83
84 Parameters
85 - cls : Sqlalchemy model class
86 Model used to create the sort enumerator
87 - name : str, optional, default None
88 Name to use for the enumerator. If not provided it will be set to `cls.__name__ + 'SortEnum'`
89 - symbol_name : function, optional, default `_symbol_name`
90 Function which takes the column name and a boolean indicating if the sort direction is ascending,
91 and returns the symbol name for the current column and sort direction.
92 The default function will create, for a column named 'foo', the symbols 'foo_asc' and 'foo_desc'
93
94 Returns
95 - Enum
96 The Graphene enumerator
97 """
98 enum, _ = _sort_enum_for_model(cls, name, symbol_name)
99 return enum
100
101
102 def sort_argument_for_model(cls, has_default=True):
103 """Returns a Graphene argument for the sort field that accepts a list of sorting directions for a model.
104 If `has_default` is True (the default) it will sort the result by the primary key(s)
105 """
106 enum, default = _sort_enum_for_model(cls)
107 if not has_default:
108 default = None
109 return Argument(List(enum), default_value=default)
0 [aliases]
1 test=pytest
2
03 [flake8]
14 exclude = setup.py,docs/*,examples/*,tests
25 max-line-length = 120
36
4 [coverage:run]
5 omit = */tests/*
6
77 [isort]
8 known_first_party=graphene,graphene_sqlalchemy
9
10 [tool:pytest]
11 testpaths = graphene_sqlalchemy/
12 addopts =
13 -s
14 ; --cov graphene-sqlalchemy
15 norecursedirs =
16 __pycache__
17 *.egg-info
18 .cache
19 .git
20 .tox
21 appdir
22 docs
23 filterwarnings =
24 error
25 ignore::DeprecationWarning
8 known_graphene=graphene,graphql_relay,flask_graphql,graphql_server,sphinx_graphene_theme
9 known_first_party=graphene_sqlalchemy
10 known_third_party=flask,nameko,promise,py,pytest,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
11 sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER
12 no_lines_before=FIRSTPARTY
2613
2714 [bdist_wheel]
2815 universal=1
0 from setuptools import find_packages, setup
1 import sys
20 import ast
31 import re
2 import sys
43
5 _version_re = re.compile(r'__version__\s+=\s+(.*)')
4 from setuptools import find_packages, setup
65
7 with open('graphene_sqlalchemy/__init__.py', 'rb') as f:
8 version = str(ast.literal_eval(_version_re.search(
9 f.read().decode('utf-8')).group(1)))
6 _version_re = re.compile(r"__version__\s+=\s+(.*)")
107
8 with open("graphene_sqlalchemy/__init__.py", "rb") as f:
9 version = str(
10 ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))
11 )
12
13 requirements = [
14 # To keep things simple, we only support newer versions of Graphene
15 "graphene>=2.1.3,<3",
16 # Tests fail with 1.0.19
17 "SQLAlchemy>=1.1,<2",
18 "six>=1.10.0,<2",
19 "singledispatch>=3.4.0.3,<4",
20 ]
21 try:
22 import enum
23 except ImportError: # Python < 2.7 and Python 3.3
24 requirements.append("enum34 >= 1.1.6")
25
26 tests_require = [
27 "pytest==4.3.1",
28 "mock==2.0.0",
29 "pytest-cov==2.6.1",
30 "sqlalchemy_utils==0.33.9",
31 ]
1132
1233 setup(
13 name='graphene-sqlalchemy',
34 name="graphene-sqlalchemy",
1435 version=version,
15
16 description='Graphene SQLAlchemy integration',
17 long_description=open('README.rst').read(),
18
19 url='https://github.com/graphql-python/graphene-sqlalchemy',
20
21 author='Syrus Akbary',
22 author_email='[email protected]',
23
24 license='MIT',
25
36 description="Graphene SQLAlchemy integration",
37 long_description=open("README.rst").read(),
38 url="https://github.com/graphql-python/graphene-sqlalchemy",
39 author="Syrus Akbary",
40 author_email="[email protected]",
41 license="MIT",
2642 classifiers=[
27 'Development Status :: 3 - Alpha',
28 'Intended Audience :: Developers',
29 'Topic :: Software Development :: Libraries',
30 'Programming Language :: Python :: 2',
31 'Programming Language :: Python :: 2.7',
32 'Programming Language :: Python :: 3',
33 'Programming Language :: Python :: 3.3',
34 'Programming Language :: Python :: 3.4',
35 'Programming Language :: Python :: 3.5',
36 'Programming Language :: Python :: Implementation :: PyPy',
43 "Development Status :: 3 - Alpha",
44 "Intended Audience :: Developers",
45 "Topic :: Software Development :: Libraries",
46 "Programming Language :: Python :: 2",
47 "Programming Language :: Python :: 2.7",
48 "Programming Language :: Python :: 3",
49 "Programming Language :: Python :: 3.3",
50 "Programming Language :: Python :: 3.4",
51 "Programming Language :: Python :: 3.5",
52 "Programming Language :: Python :: 3.6",
53 "Programming Language :: Python :: 3.7",
54 "Programming Language :: Python :: Implementation :: PyPy",
3755 ],
38
39 keywords='api graphql protocol rest relay graphene',
40
41 packages=find_packages(exclude=['tests']),
42
43 install_requires=[
44 'six>=1.10.0',
45 'graphene>=2.0',
46 'SQLAlchemy',
47 'singledispatch>=3.4.0.3',
48 'iso8601',
49 ],
50 tests_require=[
51 'pytest>=2.7.2',
52 'mock',
53 'sqlalchemy_utils',
54 ],
56 keywords="api graphql protocol rest relay graphene",
57 packages=find_packages(exclude=["tests"]),
58 install_requires=requirements,
59 extras_require={
60 "dev": [
61 "tox==3.7.0", # Should be kept in sync with tox.ini
62 "coveralls==1.7.0",
63 "pre-commit==1.14.4",
64 ],
65 "test": tests_require,
66 },
67 tests_require=tests_require,
5568 )
0 [tox]
1 envlist = pre-commit,py{27,34,35,36,37}-sql{11,12,13}
2 skipsdist = true
3 minversion = 3.7.0
4
5 [testenv]
6 deps =
7 .[test]
8 sql11: sqlalchemy>=1.1,<1.2
9 sql12: sqlalchemy>=1.2,<1.3
10 sql13: sqlalchemy>=1.3,<1.4
11 commands =
12 pytest graphene_sqlalchemy --cov=graphene_sqlalchemy {posargs}
13
14 [testenv:pre-commit]
15 basepython=python3.7
16 deps =
17 .[dev]
18 commands =
19 pre-commit {posargs:run --all-files}