New upstream version 2.1.2
Sophie Brun
5 years ago
44 | 44 | nosetests.xml |
45 | 45 | coverage.xml |
46 | 46 | *,cover |
47 | .pytest_cache/ | |
47 | 48 | |
48 | 49 | # Translations |
49 | 50 | *.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 |
0 | 0 | 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 | |
34 | 1 | matrix: |
35 | fast_finish: true | |
36 | 2 | 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 | |
39 | 42 | deploy: |
40 | 43 | provider: pypi |
41 | 44 | 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. |
21 | 21 | |
22 | 22 | ```python |
23 | 23 | from sqlalchemy import Column, Integer, String |
24 | from sqlalchemy.orm import relationship | |
25 | 24 | |
26 | 25 | from sqlalchemy.ext.declarative import declarative_base |
27 | 26 | |
28 | 27 | Base = declarative_base() |
29 | 28 | |
30 | 29 | class UserModel(Base): |
31 | __tablename__ = 'department' | |
30 | __tablename__ = 'user' | |
32 | 31 | id = Column(Integer, primary_key=True) |
33 | 32 | name = Column(String) |
34 | 33 | last_name = Column(String) |
37 | 36 | To create a GraphQL schema for it you simply have to write the following: |
38 | 37 | |
39 | 38 | ```python |
39 | import graphene | |
40 | 40 | from graphene_sqlalchemy import SQLAlchemyObjectType |
41 | 41 | |
42 | 42 | class User(SQLAlchemyObjectType): |
43 | 43 | class Meta: |
44 | 44 | model = UserModel |
45 | # only return specified fields | |
46 | only_fields = ("name",) | |
47 | # exclude specified fields | |
48 | exclude_fields = ("last_name",) | |
45 | 49 | |
46 | 50 | class Query(graphene.ObjectType): |
47 | 51 | users = graphene.List(User) |
97 | 101 | schema = graphene.Schema(query=Query) |
98 | 102 | ``` |
99 | 103 | |
104 | ### Full Examples | |
105 | ||
100 | 106 | To learn more check out the following [examples](examples/): |
101 | 107 | |
102 | * **Full example**: [Flask SQLAlchemy example](examples/flask_sqlalchemy) | |
103 | ||
108 | - [Flask SQLAlchemy example](examples/flask_sqlalchemy) | |
109 | - [Nameko SQLAlchemy example](examples/nameko_sqlalchemy) | |
104 | 110 | |
105 | 111 | ## Contributing |
106 | 112 | |
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) |
135 | 135 | # html_theme = 'alabaster' |
136 | 136 | # if on_rtd: |
137 | 137 | # html_theme = 'sphinx_rtd_theme' |
138 | import sphinx_graphene_theme | |
138 | import sphinx_graphene_theme # isort:skip | |
139 | 139 | |
140 | 140 | html_theme = "sphinx_graphene_theme" |
141 | 141 |
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 | } |
0 | 0 | # Docs template |
1 | https://github.com/graphql-python/graphene-python.org/archive/docs.zip | |
1 | http://graphene-python.org/sphinx_graphene_theme.zip |
0 | 0 | ==== |
1 | Tips | |
2 | ==== | |
3 | ||
4 | 1 | Tips |
5 | 2 | ==== |
6 | 3 | |
7 | 4 | Querying |
8 | 5 | -------- |
9 | 6 | |
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: | |
11 | 8 | |
12 | 9 | - Set the db session when you do the execution: |
13 | 10 | |
29 | 26 | If you don't specify any, the following error will be displayed: |
30 | 27 | |
31 | 28 | ``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 |
92 | 92 | import graphene |
93 | 93 | from graphene import relay |
94 | 94 | 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 | |
96 | 96 | |
97 | 97 | |
98 | 98 | class Department(SQLAlchemyObjectType): |
101 | 101 | interfaces = (relay.Node, ) |
102 | 102 | |
103 | 103 | |
104 | class DepartmentConnection(relay.Connection): | |
105 | class Meta: | |
106 | node = Department | |
107 | ||
108 | ||
104 | 109 | class Employee(SQLAlchemyObjectType): |
105 | 110 | class Meta: |
106 | 111 | model = EmployeeModel |
107 | 112 | interfaces = (relay.Node, ) |
108 | 113 | |
109 | 114 | |
115 | class EmployeeConnection(relay.Connection): | |
116 | class Meta: | |
117 | node = Employee | |
118 | ||
119 | ||
110 | 120 | class Query(graphene.ObjectType): |
111 | 121 | 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) | |
113 | 126 | |
114 | 127 | schema = graphene.Schema(query=Query) |
115 | 128 | |
132 | 145 | from flask import Flask |
133 | 146 | from flask_graphql import GraphQLView |
134 | 147 | |
135 | from models import db_session | |
136 | from schema import schema, Department | |
148 | from .models import db_session | |
149 | from .schema import schema, Department | |
137 | 150 | |
138 | 151 | app = Flask(__name__) |
139 | 152 | app.debug = True |
161 | 174 | |
162 | 175 | $ python |
163 | 176 | |
164 | >>> from models import engine, db_session, Base, Department, Employee | |
177 | >>> from .models import engine, db_session, Base, Department, Employee | |
165 | 178 | >>> Base.metadata.create_all(bind=engine) |
166 | 179 | |
167 | 180 | >>> # Fill the tables with some data |
1 | 1 | |
2 | 2 | from flask import Flask |
3 | 3 | |
4 | from database import db_session, init_db | |
5 | 4 | from flask_graphql import GraphQLView |
6 | from schema import schema | |
5 | ||
6 | from .database import db_session, init_db | |
7 | from .schema import schema | |
7 | 8 | |
8 | 9 | app = Flask(__name__) |
9 | 10 | app.debug = True |
13 | 13 | # import all modules here that might define models so that |
14 | 14 | # they will be registered properly on the metadata. Otherwise |
15 | 15 | # 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 | |
17 | 17 | Base.metadata.drop_all(bind=engine) |
18 | 18 | Base.metadata.create_all(bind=engine) |
19 | 19 |
0 | 0 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func |
1 | 1 | from sqlalchemy.orm import backref, relationship |
2 | 2 | |
3 | from database import Base | |
3 | from .database import Base | |
4 | 4 | |
5 | 5 | |
6 | 6 | class Department(Base): |
0 | 0 | graphene[sqlalchemy] |
1 | 1 | SQLAlchemy==1.0.11 |
2 | Flask==0.10.1 | |
2 | Flask==0.12.4 | |
3 | 3 | Flask-GraphQL==1.3.0 |
0 | 0 | import graphene |
1 | 1 | 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 | |
6 | 8 | |
7 | 9 | |
8 | 10 | class Department(SQLAlchemyObjectType): |
9 | ||
10 | 11 | class Meta: |
11 | 12 | model = DepartmentModel |
12 | 13 | interfaces = (relay.Node, ) |
13 | 14 | |
14 | 15 | |
15 | 16 | class Employee(SQLAlchemyObjectType): |
16 | ||
17 | 17 | class Meta: |
18 | 18 | model = EmployeeModel |
19 | 19 | interfaces = (relay.Node, ) |
20 | 20 | |
21 | 21 | |
22 | 22 | class Role(SQLAlchemyObjectType): |
23 | ||
24 | 23 | class Meta: |
25 | 24 | model = RoleModel |
26 | 25 | interfaces = (relay.Node, ) |
27 | 26 | |
28 | 27 | |
28 | SortEnumEmployee = utils.sort_enum_for_model(EmployeeModel, 'SortEnumEmployee', | |
29 | lambda c, d: c.upper() + ('_ASC' if d else '_DESC')) | |
30 | ||
31 | ||
29 | 32 | class Query(graphene.ObjectType): |
30 | 33 | 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 | |
32 | 41 | all_roles = SQLAlchemyConnectionField(Role) |
33 | role = graphene.Field(Role) | |
42 | # Disable sorting over this field | |
43 | all_departments = SQLAlchemyConnectionField(Department, sort=None) | |
34 | 44 | |
35 | 45 | |
36 | 46 | 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 | #!/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 | |
10 | 3 | |
11 | __version__ = '2.0.0' | |
4 | __version__ = "2.1.2" | |
12 | 5 | |
13 | 6 | __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", | |
19 | 12 | ] |
6 | 6 | String) |
7 | 7 | from graphene.types.json import JSONString |
8 | 8 | |
9 | from .fields import createConnectionField | |
10 | ||
11 | 9 | try: |
12 | from sqlalchemy_utils import ( | |
13 | ChoiceType, JSONType, ScalarListType, TSVectorType) | |
10 | from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType | |
14 | 11 | except ImportError: |
15 | 12 | ChoiceType = JSONType = ScalarListType = TSVectorType = object |
16 | 13 | |
17 | 14 | |
18 | 15 | def get_column_doc(column): |
19 | return getattr(column, 'doc', None) | |
16 | return getattr(column, "doc", None) | |
20 | 17 | |
21 | 18 | |
22 | 19 | def is_column_nullable(column): |
23 | return bool(getattr(column, 'nullable', True)) | |
20 | return bool(getattr(column, "nullable", True)) | |
24 | 21 | |
25 | 22 | |
26 | def convert_sqlalchemy_relationship(relationship, registry): | |
23 | def convert_sqlalchemy_relationship(relationship, registry, connection_field_factory): | |
27 | 24 | direction = relationship.direction |
28 | 25 | model = relationship.mapper.entity |
29 | 26 | |
35 | 32 | return Field(_type) |
36 | 33 | elif direction in (interfaces.ONETOMANY, interfaces.MANYTOMANY): |
37 | 34 | if _type._meta.connection: |
38 | return createConnectionField(_type) | |
35 | return connection_field_factory(relationship, registry) | |
39 | 36 | return Field(List(_type)) |
40 | 37 | |
41 | 38 | return Dynamic(dynamic_type) |
42 | 39 | |
43 | 40 | |
44 | 41 | 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) | |
47 | 43 | |
48 | 44 | |
49 | 45 | def convert_sqlalchemy_composite(composite, registry): |
51 | 47 | if not converter: |
52 | 48 | try: |
53 | 49 | 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 | ) | |
56 | 53 | except AttributeError: |
57 | 54 | # handle fields that are not attached to a class yet (don't have a parent) |
58 | 55 | 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 | ) | |
61 | 59 | return converter(composite, registry) |
62 | 60 | |
63 | 61 | |
64 | 62 | def _register_composite_class(cls, registry=None): |
65 | 63 | if registry is None: |
66 | 64 | from .registry import get_global_registry |
65 | ||
67 | 66 | registry = get_global_registry() |
68 | 67 | |
69 | 68 | def inner(fn): |
70 | 69 | registry.register_composite_converter(cls, fn) |
70 | ||
71 | 71 | return inner |
72 | 72 | |
73 | 73 | |
75 | 75 | |
76 | 76 | |
77 | 77 | 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) | |
79 | 79 | |
80 | 80 | |
81 | 81 | @singledispatch |
82 | 82 | def convert_sqlalchemy_type(type, column, registry=None): |
83 | 83 | 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 | ) | |
85 | 87 | |
86 | 88 | |
87 | 89 | @convert_sqlalchemy_type.register(types.Date) |
90 | 92 | @convert_sqlalchemy_type.register(types.Text) |
91 | 93 | @convert_sqlalchemy_type.register(types.Unicode) |
92 | 94 | @convert_sqlalchemy_type.register(types.UnicodeText) |
93 | @convert_sqlalchemy_type.register(types.Enum) | |
94 | @convert_sqlalchemy_type.register(postgresql.ENUM) | |
95 | 95 | @convert_sqlalchemy_type.register(postgresql.UUID) |
96 | @convert_sqlalchemy_type.register(postgresql.INET) | |
97 | @convert_sqlalchemy_type.register(postgresql.CIDR) | |
96 | 98 | @convert_sqlalchemy_type.register(TSVectorType) |
97 | 99 | 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 | ) | |
100 | 103 | |
101 | 104 | |
102 | 105 | @convert_sqlalchemy_type.register(types.DateTime) |
103 | 106 | def convert_column_to_datetime(type, column, registry=None): |
104 | 107 | 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 | ) | |
107 | 112 | |
108 | 113 | |
109 | 114 | @convert_sqlalchemy_type.register(types.SmallInteger) |
110 | 115 | @convert_sqlalchemy_type.register(types.Integer) |
111 | 116 | def convert_column_to_int_or_id(type, column, registry=None): |
112 | 117 | 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 | ) | |
114 | 122 | 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 | ) | |
117 | 127 | |
118 | 128 | |
119 | 129 | @convert_sqlalchemy_type.register(types.Boolean) |
120 | 130 | 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 | ) | |
122 | 134 | |
123 | 135 | |
124 | 136 | @convert_sqlalchemy_type.register(types.Float) |
125 | 137 | @convert_sqlalchemy_type.register(types.Numeric) |
126 | 138 | @convert_sqlalchemy_type.register(types.BigInteger) |
127 | 139 | 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 | ) | |
129 | 158 | |
130 | 159 | |
131 | 160 | @convert_sqlalchemy_type.register(ChoiceType) |
132 | 161 | 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() | |
134 | 163 | return Enum(name, type.choices, description=get_column_doc(column)) |
135 | 164 | |
136 | 165 | |
143 | 172 | def convert_postgres_array_to_list(_type, column, registry=None): |
144 | 173 | graphene_type = convert_sqlalchemy_type(column.type.item_type, column) |
145 | 174 | 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 | ) | |
147 | 180 | |
148 | 181 | |
149 | 182 | @convert_sqlalchemy_type.register(postgresql.HSTORE) |
150 | 183 | @convert_sqlalchemy_type.register(postgresql.JSON) |
151 | 184 | @convert_sqlalchemy_type.register(postgresql.JSONB) |
152 | 185 | 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 | ) | |
154 | 189 | |
155 | 190 | |
156 | 191 | @convert_sqlalchemy_type.register(JSONType) |
157 | 192 | 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 | |
0 | 1 | from functools import partial |
1 | 2 | |
3 | from promise import Promise, is_thenable | |
2 | 4 | from sqlalchemy.orm.query import Query |
3 | 5 | |
4 | from graphene.relay import ConnectionField | |
6 | from graphene.relay import Connection, ConnectionField | |
5 | 7 | from graphene.relay.connection import PageInfo |
6 | 8 | from graphql_relay.connection.arrayconnection import connection_from_list_slice |
7 | 9 | |
8 | from .utils import get_query | |
10 | from .utils import get_query, sort_argument_for_model | |
11 | ||
12 | log = logging.getLogger() | |
9 | 13 | |
10 | 14 | |
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 | |
12 | 30 | |
13 | 31 | @property |
14 | 32 | def model(self): |
15 | 33 | return self.type._meta.node._meta.model |
16 | 34 | |
17 | 35 | @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 | |
30 | 44 | |
31 | 45 | @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() | |
38 | 51 | else: |
39 | _len = len(iterable) | |
52 | _len = len(resolved) | |
40 | 53 | connection = connection_from_list_slice( |
41 | iterable, | |
54 | resolved, | |
42 | 55 | args, |
43 | 56 | slice_start=0, |
44 | 57 | list_length=_len, |
45 | 58 | list_slice_length=_len, |
46 | connection_type=connection, | |
59 | connection_type=connection_type, | |
47 | 60 | pageinfo_type=PageInfo, |
48 | edge_type=connection.Edge, | |
61 | edge_type=connection_type.Edge, | |
49 | 62 | ) |
50 | connection.iterable = iterable | |
63 | connection.iterable = resolved | |
51 | 64 | connection.length = _len |
52 | 65 | 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) | |
53 | 76 | |
54 | 77 | def get_resolver(self, parent_resolver): |
55 | 78 | return partial(self.connection_resolver, parent_resolver, self.type, self.model) |
56 | 79 | |
57 | 80 | |
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 | |
59 | 108 | |
60 | 109 | |
61 | 110 | 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 | ) | |
62 | 115 | return __connectionFactory(_type) |
63 | 116 | |
64 | 117 | |
65 | 118 | 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 | ) | |
66 | 123 | global __connectionFactory |
67 | 124 | __connectionFactory = factoryMethod |
68 | 125 | |
69 | 126 | |
70 | 127 | 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 | ) | |
71 | 132 | global __connectionFactory |
72 | __connectionFactory = SQLAlchemyConnectionField | |
133 | __connectionFactory = UnsortedSQLAlchemyConnectionField |
0 | 0 | class Registry(object): |
1 | ||
2 | 1 | def __init__(self): |
3 | 2 | self._registry = {} |
4 | 3 | self._registry_models = {} |
6 | 5 | |
7 | 6 | def register(self, cls): |
8 | 7 | from .types import SQLAlchemyObjectType |
8 | ||
9 | 9 | assert issubclass(cls, SQLAlchemyObjectType), ( |
10 | 'Only classes of type SQLAlchemyObjectType can be registered, ', | |
10 | "Only classes of type SQLAlchemyObjectType can be registered, " | |
11 | 11 | 'received "{}"' |
12 | 12 | ).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." | |
14 | 14 | # assert self.get_type_for_model(cls._meta.model) in [None, cls], ( |
15 | 15 | # 'SQLAlchemy model "{}" already associated with ' |
16 | 16 | # 'another type "{}".' |
0 | 0 | from __future__ import absolute_import |
1 | 1 | |
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 | |
3 | 5 | from sqlalchemy.ext.declarative import declarative_base |
4 | 6 | from sqlalchemy.orm import mapper, relationship |
5 | 7 | |
8 | ||
9 | class Hairkind(enum.Enum): | |
10 | LONG = 'long' | |
11 | SHORT = 'short' | |
12 | ||
13 | ||
6 | 14 | Base = declarative_base() |
7 | 15 | |
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 | ) | |
11 | 22 | |
12 | 23 | |
13 | 24 | class Editor(Base): |
14 | __tablename__ = 'editors' | |
25 | __tablename__ = "editors" | |
15 | 26 | editor_id = Column(Integer(), primary_key=True) |
16 | 27 | name = Column(String(100)) |
17 | 28 | |
18 | 29 | |
19 | 30 | class Pet(Base): |
20 | __tablename__ = 'pets' | |
31 | __tablename__ = "pets" | |
21 | 32 | id = Column(Integer(), primary_key=True) |
22 | 33 | 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")) | |
24 | 37 | |
25 | 38 | |
26 | 39 | class Reporter(Base): |
27 | __tablename__ = 'reporters' | |
40 | __tablename__ = "reporters" | |
28 | 41 | id = Column(Integer(), primary_key=True) |
29 | 42 | first_name = Column(String(30)) |
30 | 43 | last_name = Column(String(30)) |
31 | 44 | 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") | |
34 | 47 | favorite_article = relationship("Article", uselist=False) |
35 | 48 | |
36 | 49 | # total = column_property( |
41 | 54 | |
42 | 55 | |
43 | 56 | class Article(Base): |
44 | __tablename__ = 'articles' | |
57 | __tablename__ = "articles" | |
45 | 58 | id = Column(Integer(), primary_key=True) |
46 | 59 | headline = Column(String(100)) |
47 | 60 | pub_date = Column(Date()) |
48 | reporter_id = Column(Integer(), ForeignKey('reporters.id')) | |
61 | reporter_id = Column(Integer(), ForeignKey("reporters.id")) | |
49 | 62 | |
50 | 63 | |
51 | 64 | class ReflectedEditor(type): |
52 | 65 | """Same as Editor, but using reflected table.""" |
66 | ||
53 | 67 | @classmethod |
54 | 68 | def __subclasses__(cls): |
55 | 69 | return [] |
56 | 70 | |
57 | editor_table = Table('editors', Base.metadata, autoload=True) | |
71 | ||
72 | editor_table = Table("editors", Base.metadata, autoload=True) | |
58 | 73 | |
59 | 74 | mapper(ReflectedEditor, editor_table) |
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 | ||
0 | 2 | 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 | |
2 | 4 | from sqlalchemy.dialects import postgresql |
3 | 5 | from sqlalchemy.ext.declarative import declarative_base |
4 | from sqlalchemy.orm import composite, column_property | |
6 | from sqlalchemy.orm import column_property, composite | |
5 | 7 | from sqlalchemy.sql.elements import Label |
6 | 8 | from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType |
7 | 9 | |
13 | 15 | from ..converter import (convert_sqlalchemy_column, |
14 | 16 | convert_sqlalchemy_composite, |
15 | 17 | convert_sqlalchemy_relationship) |
16 | from ..fields import SQLAlchemyConnectionField | |
18 | from ..fields import (UnsortedSQLAlchemyConnectionField, | |
19 | default_connection_field_factory) | |
17 | 20 | from ..registry import Registry |
18 | 21 | from ..types import SQLAlchemyObjectType |
19 | 22 | from .models import Article, Pet, Reporter |
20 | 23 | |
21 | 24 | |
22 | 25 | 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) | |
24 | 27 | graphene_type = convert_sqlalchemy_column(column) |
25 | 28 | 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" | |
28 | 35 | return field |
29 | 36 | |
30 | 37 | |
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 | ) | |
35 | 44 | graphene_type = convert_sqlalchemy_composite(composite_column, registry) |
36 | 45 | assert isinstance(graphene_type, graphene_field) |
37 | 46 | field = graphene_type.Field() |
44 | 53 | def test_should_unknown_sqlalchemy_field_raise_exception(): |
45 | 54 | with raises(Exception) as excinfo: |
46 | 55 | 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) | |
48 | 57 | |
49 | 58 | |
50 | 59 | def test_should_date_convert_string(): |
75 | 84 | assert_column_conversion(types.UnicodeText(), graphene.String) |
76 | 85 | |
77 | 86 | |
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") | |
80 | 101 | |
81 | 102 | |
82 | 103 | def test_should_small_integer_convert_int(): |
108 | 129 | |
109 | 130 | |
110 | 131 | 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()) | |
112 | 133 | graphene_type = convert_sqlalchemy_column(label) |
113 | 134 | assert isinstance(graphene_type, graphene.String) |
114 | 135 | |
115 | 136 | |
116 | 137 | 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()) | |
118 | 139 | graphene_type = convert_sqlalchemy_column(label) |
119 | 140 | assert isinstance(graphene_type, graphene.Int) |
120 | 141 | |
142 | ||
121 | 143 | 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") | |
127 | 146 | Base = declarative_base() |
128 | 147 | |
129 | Table('translatedmodel', Base.metadata, column) | |
148 | Table("translatedmodel", Base.metadata, column) | |
130 | 149 | graphene_type = convert_sqlalchemy_column(column) |
131 | 150 | 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" | |
136 | 155 | |
137 | 156 | |
138 | 157 | def test_should_columproperty_convert(): |
140 | 159 | Base = declarative_base() |
141 | 160 | |
142 | 161 | class Test(Base): |
143 | __tablename__ = 'test' | |
162 | __tablename__ = "test" | |
144 | 163 | id = Column(types.Integer, primary_key=True) |
145 | 164 | 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) | |
149 | 166 | ) |
150 | 167 | |
151 | 168 | graphene_type = convert_sqlalchemy_column(Test.column) |
152 | assert graphene_type.kwargs['required'] == False | |
169 | assert not graphene_type.kwargs["required"] | |
153 | 170 | |
154 | 171 | |
155 | 172 | def test_should_scalar_list_convert_list(): |
162 | 179 | |
163 | 180 | def test_should_manytomany_convert_connectionorlist(): |
164 | 181 | 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 | ) | |
166 | 185 | assert isinstance(dynamic_field, graphene.Dynamic) |
167 | 186 | assert not dynamic_field.get_type() |
168 | 187 | |
169 | 188 | |
170 | 189 | def test_should_manytomany_convert_connectionorlist_list(): |
171 | 190 | class A(SQLAlchemyObjectType): |
172 | ||
173 | 191 | class Meta: |
174 | 192 | model = Pet |
175 | 193 | |
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 | ) | |
177 | 197 | assert isinstance(dynamic_field, graphene.Dynamic) |
178 | 198 | graphene_type = dynamic_field.get_type() |
179 | 199 | assert isinstance(graphene_type, graphene.Field) |
183 | 203 | |
184 | 204 | def test_should_manytomany_convert_connectionorlist_connection(): |
185 | 205 | class A(SQLAlchemyObjectType): |
186 | ||
187 | 206 | class Meta: |
188 | 207 | 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) | |
194 | 215 | |
195 | 216 | |
196 | 217 | def test_should_manytoone_convert_connectionorlist(): |
197 | 218 | 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 | ) | |
199 | 222 | assert isinstance(dynamic_field, graphene.Dynamic) |
200 | 223 | assert not dynamic_field.get_type() |
201 | 224 | |
202 | 225 | |
203 | 226 | def test_should_manytoone_convert_connectionorlist_list(): |
204 | 227 | class A(SQLAlchemyObjectType): |
205 | ||
206 | 228 | class Meta: |
207 | 229 | model = Reporter |
208 | 230 | |
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 | ) | |
210 | 234 | assert isinstance(dynamic_field, graphene.Dynamic) |
211 | 235 | graphene_type = dynamic_field.get_type() |
212 | 236 | assert isinstance(graphene_type, graphene.Field) |
215 | 239 | |
216 | 240 | def test_should_manytoone_convert_connectionorlist_connection(): |
217 | 241 | class A(SQLAlchemyObjectType): |
218 | ||
219 | 242 | class Meta: |
220 | 243 | 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 | ) | |
224 | 249 | assert isinstance(dynamic_field, graphene.Dynamic) |
225 | 250 | graphene_type = dynamic_field.get_type() |
226 | 251 | assert isinstance(graphene_type, graphene.Field) |
229 | 254 | |
230 | 255 | def test_should_onetoone_convert_field(): |
231 | 256 | class A(SQLAlchemyObjectType): |
232 | ||
233 | 257 | class Meta: |
234 | 258 | 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 | ) | |
238 | 264 | assert isinstance(dynamic_field, graphene.Dynamic) |
239 | 265 | graphene_type = dynamic_field.get_type() |
240 | 266 | assert isinstance(graphene_type, graphene.Field) |
246 | 272 | |
247 | 273 | |
248 | 274 | 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") | |
250 | 292 | |
251 | 293 | |
252 | 294 | def test_should_postgresql_array_convert(): |
266 | 308 | |
267 | 309 | |
268 | 310 | def test_should_composite_convert(): |
269 | ||
270 | 311 | class CompositeClass(object): |
271 | ||
272 | 312 | def __init__(self, col1, col2): |
273 | 313 | self.col1 = col1 |
274 | 314 | self.col2 = col2 |
279 | 319 | def convert_composite_class(composite, registry): |
280 | 320 | return graphene.String(description=composite.doc) |
281 | 321 | |
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 | ) | |
287 | 328 | |
288 | 329 | |
289 | 330 | def test_should_unknown_sqlalchemy_composite_raise_exception(): |
292 | 333 | with raises(Exception) as excinfo: |
293 | 334 | |
294 | 335 | class CompositeClass(object): |
295 | ||
296 | 336 | def __init__(self, col1, col2): |
297 | 337 | self.col1 = col1 |
298 | 338 | self.col2 = col2 |
299 | 339 | |
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) |
2 | 2 | from sqlalchemy.orm import scoped_session, sessionmaker |
3 | 3 | |
4 | 4 | import graphene |
5 | from graphene.relay import Node | |
6 | ||
5 | from graphene.relay import Connection, Node | |
6 | ||
7 | from ..fields import SQLAlchemyConnectionField | |
7 | 8 | from ..registry import reset_global_registry |
8 | from ..fields import SQLAlchemyConnectionField | |
9 | 9 | 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") | |
16 | 17 | def session(): |
17 | 18 | reset_global_registry() |
18 | 19 | connection = db.engine.connect() |
32 | 33 | |
33 | 34 | |
34 | 35 | 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") | |
36 | 39 | session.add(reporter) |
37 | reporter2 = Reporter(first_name='ABO', last_name='Y') | |
40 | reporter2 = Reporter(first_name="ABO", last_name="Y") | |
38 | 41 | session.add(reporter2) |
39 | article = Article(headline='Hi!') | |
42 | article = Article(headline="Hi!") | |
40 | 43 | article.reporter = reporter |
41 | 44 | session.add(article) |
42 | 45 | editor = Editor(name="John") |
48 | 51 | setup_fixtures(session) |
49 | 52 | |
50 | 53 | class ReporterType(SQLAlchemyObjectType): |
51 | ||
52 | 54 | class Meta: |
53 | 55 | model = Reporter |
54 | 56 | |
62 | 64 | def resolve_reporters(self, *args, **kwargs): |
63 | 65 | return session.query(Reporter) |
64 | 66 | |
65 | query = ''' | |
67 | query = """ | |
66 | 68 | query ReporterQuery { |
67 | 69 | reporter { |
68 | 70 | firstName, |
73 | 75 | firstName |
74 | 76 | } |
75 | 77 | } |
76 | ''' | |
78 | """ | |
77 | 79 | 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"}], | |
88 | 82 | } |
89 | 83 | schema = graphene.Schema(query=Query) |
90 | 84 | result = schema.execute(query) |
92 | 86 | assert result.data == expected |
93 | 87 | |
94 | 88 | |
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 | ||
95 | 189 | def test_should_node(session): |
96 | 190 | setup_fixtures(session) |
97 | 191 | |
98 | 192 | class ReporterNode(SQLAlchemyObjectType): |
99 | ||
100 | 193 | class Meta: |
101 | 194 | model = Reporter |
102 | interfaces = (Node, ) | |
195 | interfaces = (Node,) | |
103 | 196 | |
104 | 197 | @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") | |
107 | 200 | |
108 | 201 | class ArticleNode(SQLAlchemyObjectType): |
109 | ||
110 | 202 | class Meta: |
111 | 203 | model = Article |
112 | interfaces = (Node, ) | |
204 | interfaces = (Node,) | |
113 | 205 | |
114 | 206 | # @classmethod |
115 | 207 | # def get_node(cls, id, info): |
116 | 208 | # return Article(id=1, headline='Article node') |
117 | 209 | |
210 | class ArticleConnection(Connection): | |
211 | class Meta: | |
212 | node = ArticleNode | |
213 | ||
118 | 214 | class Query(graphene.ObjectType): |
119 | 215 | node = Node.Field() |
120 | 216 | reporter = graphene.Field(ReporterNode) |
121 | 217 | article = graphene.Field(ArticleNode) |
122 | all_articles = SQLAlchemyConnectionField(ArticleNode) | |
218 | all_articles = SQLAlchemyConnectionField(ArticleConnection) | |
123 | 219 | |
124 | 220 | def resolve_reporter(self, *args, **kwargs): |
125 | 221 | return session.query(Reporter).first() |
127 | 223 | def resolve_article(self, *args, **kwargs): |
128 | 224 | return session.query(Article).first() |
129 | 225 | |
130 | query = ''' | |
226 | query = """ | |
131 | 227 | query ReporterQuery { |
132 | 228 | reporter { |
133 | 229 | id, |
159 | 255 | } |
160 | 256 | } |
161 | 257 | } |
162 | ''' | |
258 | """ | |
163 | 259 | 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!"}}]}, | |
176 | 266 | }, |
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!"}, | |
188 | 269 | } |
189 | 270 | schema = graphene.Schema(query=Query) |
190 | result = schema.execute(query, context_value={'session': session}) | |
271 | result = schema.execute(query, context_value={"session": session}) | |
191 | 272 | assert not result.errors |
192 | 273 | assert result.data == expected |
193 | 274 | |
196 | 277 | setup_fixtures(session) |
197 | 278 | |
198 | 279 | class EditorNode(SQLAlchemyObjectType): |
199 | ||
200 | 280 | class Meta: |
201 | 281 | model = Editor |
202 | interfaces = (Node, ) | |
282 | interfaces = (Node,) | |
283 | ||
284 | class EditorConnection(Connection): | |
285 | class Meta: | |
286 | node = EditorNode | |
203 | 287 | |
204 | 288 | class Query(graphene.ObjectType): |
205 | 289 | node = Node.Field() |
206 | all_editors = SQLAlchemyConnectionField(EditorNode) | |
207 | ||
208 | query = ''' | |
290 | all_editors = SQLAlchemyConnectionField(EditorConnection) | |
291 | ||
292 | query = """ | |
209 | 293 | query EditorQuery { |
210 | 294 | allEditors { |
211 | 295 | edges { |
221 | 305 | } |
222 | 306 | } |
223 | 307 | } |
224 | ''' | |
308 | """ | |
225 | 309 | 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"}, | |
237 | 312 | } |
238 | 313 | |
239 | 314 | schema = graphene.Schema(query=Query) |
240 | result = schema.execute(query, context_value={'session': session}) | |
315 | result = schema.execute(query, context_value={"session": session}) | |
241 | 316 | assert not result.errors |
242 | 317 | assert result.data == expected |
243 | 318 | |
246 | 321 | setup_fixtures(session) |
247 | 322 | |
248 | 323 | class EditorNode(SQLAlchemyObjectType): |
249 | ||
250 | 324 | class Meta: |
251 | 325 | model = Editor |
252 | interfaces = (Node, ) | |
326 | interfaces = (Node,) | |
253 | 327 | |
254 | 328 | class ReporterNode(SQLAlchemyObjectType): |
255 | ||
256 | 329 | class Meta: |
257 | 330 | model = Reporter |
258 | interfaces = (Node, ) | |
331 | interfaces = (Node,) | |
259 | 332 | |
260 | 333 | @classmethod |
261 | 334 | def get_node(cls, id, info): |
262 | return Reporter(id=2, first_name='Cookie Monster') | |
335 | return Reporter(id=2, first_name="Cookie Monster") | |
263 | 336 | |
264 | 337 | class ArticleNode(SQLAlchemyObjectType): |
265 | ||
266 | 338 | class Meta: |
267 | 339 | model = Article |
268 | interfaces = (Node, ) | |
340 | interfaces = (Node,) | |
269 | 341 | |
270 | 342 | class CreateArticle(graphene.Mutation): |
271 | ||
272 | 343 | class Arguments: |
273 | 344 | headline = graphene.String() |
274 | 345 | reporter_id = graphene.ID() |
277 | 348 | article = graphene.Field(ArticleNode) |
278 | 349 | |
279 | 350 | 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) | |
284 | 352 | |
285 | 353 | session.add(new_article) |
286 | 354 | session.commit() |
294 | 362 | class Mutation(graphene.ObjectType): |
295 | 363 | create_article = CreateArticle.Field() |
296 | 364 | |
297 | query = ''' | |
365 | query = """ | |
298 | 366 | mutation ArticleCreator { |
299 | 367 | createArticle( |
300 | 368 | headline: "My Article" |
310 | 378 | } |
311 | 379 | } |
312 | 380 | } |
313 | ''' | |
381 | """ | |
314 | 382 | 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 | } | |
325 | 390 | } |
326 | 391 | |
327 | 392 | 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}) | |
329 | 394 | assert not result.errors |
330 | 395 | 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 | ) |
8 | 8 | |
9 | 9 | |
10 | 10 | class Reflected(SQLAlchemyObjectType): |
11 | ||
12 | 11 | class Meta: |
13 | 12 | model = ReflectedEditor |
14 | 13 | registry = registry |
17 | 16 | def test_objecttype_registered(): |
18 | 17 | assert issubclass(Reflected, ObjectType) |
19 | 18 | 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") |
6 | 6 | |
7 | 7 | def test_should_raise_if_no_model(): |
8 | 8 | with raises(Exception) as excinfo: |
9 | ||
9 | 10 | class Character1(SQLAlchemyObjectType): |
10 | 11 | pass |
11 | assert 'valid SQLAlchemy Model' in str(excinfo.value) | |
12 | ||
13 | assert "valid SQLAlchemy Model" in str(excinfo.value) | |
12 | 14 | |
13 | 15 | |
14 | 16 | def test_should_raise_if_model_is_invalid(): |
15 | 17 | with raises(Exception) as excinfo: |
18 | ||
16 | 19 | class Character2(SQLAlchemyObjectType): |
17 | ||
18 | 20 | class Meta: |
19 | 21 | model = 1 |
20 | assert 'valid SQLAlchemy Model' in str(excinfo.value) | |
22 | ||
23 | assert "valid SQLAlchemy Model" in str(excinfo.value) | |
21 | 24 | |
22 | 25 | |
23 | 26 | def test_should_map_fields_correctly(): |
24 | 27 | class ReporterType2(SQLAlchemyObjectType): |
25 | ||
26 | 28 | class Meta: |
27 | 29 | model = Reporter |
28 | 30 | registry = Registry() |
29 | 31 | |
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 | ] | |
39 | 41 | |
40 | 42 | |
41 | 43 | def test_should_map_only_few_fields(): |
42 | 44 | class Reporter2(SQLAlchemyObjectType): |
43 | ||
44 | 45 | class Meta: |
45 | 46 | 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) | |
5 | 12 | from ..registry import Registry |
6 | from ..types import SQLAlchemyObjectType | |
13 | from ..types import SQLAlchemyObjectType, SQLAlchemyObjectTypeOptions | |
7 | 14 | from .models import Article, Reporter |
8 | 15 | |
9 | 16 | registry = Registry() |
10 | 17 | |
11 | 18 | |
12 | 19 | class Character(SQLAlchemyObjectType): |
13 | '''Character description''' | |
20 | """Character description""" | |
21 | ||
14 | 22 | class Meta: |
15 | 23 | model = Reporter |
16 | 24 | registry = registry |
17 | 25 | |
18 | 26 | |
19 | 27 | class Human(SQLAlchemyObjectType): |
20 | '''Human description''' | |
28 | """Human description""" | |
21 | 29 | |
22 | 30 | pub_date = Int() |
23 | 31 | |
24 | 32 | class Meta: |
25 | 33 | model = Article |
26 | exclude_fields = ('id', ) | |
34 | exclude_fields = ("id",) | |
27 | 35 | registry = registry |
28 | interfaces = (Node, ) | |
36 | interfaces = (Node,) | |
29 | 37 | |
30 | 38 | |
31 | 39 | def test_sqlalchemy_interface(): |
43 | 51 | def test_objecttype_registered(): |
44 | 52 | assert issubclass(Character, ObjectType) |
45 | 53 | 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 | ] | |
55 | 63 | |
56 | 64 | |
57 | 65 | # def test_sqlalchemynode_idfield(): |
65 | 73 | |
66 | 74 | |
67 | 75 | def test_node_replacedfield(): |
68 | idfield = Human._meta.fields['pub_date'] | |
76 | idfield = Human._meta.fields["pub_date"] | |
69 | 77 | assert isinstance(idfield, Field) |
70 | 78 | assert idfield.type == Int |
71 | 79 | |
72 | 80 | |
73 | 81 | def test_object_type(): |
74 | ||
75 | ||
76 | 82 | class Human(SQLAlchemyObjectType): |
77 | '''Human description''' | |
83 | """Human description""" | |
78 | 84 | |
79 | 85 | pub_date = Int() |
80 | 86 | |
82 | 88 | model = Article |
83 | 89 | # exclude_fields = ('id', ) |
84 | 90 | registry = registry |
85 | interfaces = (Node, ) | |
91 | interfaces = (Node,) | |
86 | 92 | |
87 | 93 | 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 | ] | |
89 | 101 | assert is_node(Human) |
90 | ||
91 | 102 | |
92 | 103 | |
93 | 104 | # Test Custom SQLAlchemyObjectType Implementation |
97 | 108 | |
98 | 109 | |
99 | 110 | class CustomCharacter(CustomSQLAlchemyObjectType): |
100 | '''Character description''' | |
111 | """Character description""" | |
112 | ||
101 | 113 | class Meta: |
102 | 114 | model = Reporter |
103 | 115 | registry = registry |
106 | 118 | def test_custom_objecttype_registered(): |
107 | 119 | assert issubclass(CustomCharacter, ObjectType) |
108 | 120 | 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 | |
1 | 1 | |
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 | |
3 | 6 | |
4 | 7 | |
5 | 8 | def test_get_session(): |
6 | session = 'My SQLAlchemy session' | |
9 | session = "My SQLAlchemy session" | |
7 | 10 | |
8 | 11 | class Query(ObjectType): |
9 | 12 | x = String() |
11 | 14 | def resolve_x(self, info): |
12 | 15 | return get_session(info.context) |
13 | 16 | |
14 | query = ''' | |
17 | query = """ | |
15 | 18 | query ReporterQuery { |
16 | 19 | x |
17 | 20 | } |
18 | ''' | |
21 | """ | |
19 | 22 | |
20 | 23 | schema = Schema(query=Query) |
21 | result = schema.execute(query, context_value={'session': session}) | |
24 | result = schema.execute(query, context_value={"session": session}) | |
22 | 25 | 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 | ) |
0 | 0 | from collections import OrderedDict |
1 | 1 | |
2 | import sqlalchemy | |
3 | from sqlalchemy.ext.hybrid import hybrid_property | |
2 | 4 | from sqlalchemy.inspection import inspect as sqlalchemyinspect |
3 | from sqlalchemy.ext.hybrid import hybrid_property | |
4 | 5 | from sqlalchemy.orm.exc import NoResultFound |
5 | 6 | |
6 | 7 | from graphene import Field # , annotate, ResolveInfo |
10 | 11 | |
11 | 12 | from .converter import (convert_sqlalchemy_column, |
12 | 13 | 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 | |
15 | 17 | from .registry import Registry, get_global_registry |
16 | 18 | from .utils import get_query, is_mapped_class, is_mapped_instance |
17 | 19 | |
18 | 20 | |
19 | def construct_fields(model, registry, only_fields, exclude_fields): | |
21 | def construct_fields(model, registry, only_fields, exclude_fields, connection_field_factory): | |
20 | 22 | inspected_model = sqlalchemyinspect(model) |
21 | 23 | |
22 | 24 | fields = OrderedDict() |
57 | 59 | # in there. Or when we exclude this field in exclude_fields |
58 | 60 | continue |
59 | 61 | |
60 | converted_hybrid_property = convert_sqlalchemy_hybrid_method( | |
61 | hybrid_item | |
62 | ) | |
62 | converted_hybrid_property = convert_sqlalchemy_hybrid_method(hybrid_item) | |
63 | 63 | fields[name] = converted_hybrid_property |
64 | 64 | |
65 | 65 | # Get all the columns for the relationships on the model |
71 | 71 | # We skip this field if we specify only_fields and is not |
72 | 72 | # in there. Or when we exclude this field in exclude_fields |
73 | 73 | continue |
74 | converted_relationship = convert_sqlalchemy_relationship(relationship, registry) | |
74 | converted_relationship = convert_sqlalchemy_relationship(relationship, registry, connection_field_factory) | |
75 | 75 | name = relationship.key |
76 | 76 | fields[name] = converted_relationship |
77 | 77 | |
79 | 79 | |
80 | 80 | |
81 | 81 | 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] | |
85 | 85 | id = None # type: str |
86 | 86 | |
87 | 87 | |
88 | 88 | class SQLAlchemyObjectType(ObjectType): |
89 | 89 | @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 | ): | |
93 | 106 | 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 "{}".' | |
96 | 108 | ).format(cls.__name__, model) |
97 | 109 | |
98 | 110 | if not registry: |
99 | 111 | registry = get_global_registry() |
100 | 112 | |
101 | 113 | 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 " | |
103 | 115 | 'Registry, received "{}".' |
104 | 116 | ).format(cls.__name__, registry) |
105 | 117 | |
106 | 118 | 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 | |
109 | 127 | ) |
110 | 128 | |
111 | 129 | 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 | ) | |
113 | 133 | |
114 | 134 | if use_connection and not connection: |
115 | 135 | # 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 | ) | |
117 | 142 | |
118 | 143 | if connection is not None: |
119 | 144 | assert issubclass(connection, Connection), ( |
120 | 145 | "The connection must be a Connection. Received {}" |
121 | 146 | ).format(connection.__name__) |
122 | 147 | |
123 | _meta = SQLAlchemyObjectTypeOptions(cls) | |
148 | if not _meta: | |
149 | _meta = SQLAlchemyObjectTypeOptions(cls) | |
150 | ||
124 | 151 | _meta.model = model |
125 | 152 | _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 | ||
127 | 159 | _meta.connection = connection |
128 | _meta.id = id or 'id' | |
160 | _meta.id = id or "id" | |
129 | 161 | |
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 | ) | |
131 | 165 | |
132 | 166 | if not skip_registry: |
133 | 167 | registry.register(cls) |
137 | 171 | if isinstance(root, cls): |
138 | 172 | return True |
139 | 173 | if not is_mapped_instance(root): |
140 | raise Exception(( | |
141 | 'Received incompatible instance "{}".' | |
142 | ).format(root)) | |
174 | raise Exception(('Received incompatible instance "{}".').format(root)) | |
143 | 175 | return isinstance(root, cls._meta.model) |
144 | 176 | |
145 | 177 | @classmethod |
0 | 0 | from sqlalchemy.exc import ArgumentError |
1 | from sqlalchemy.inspection import inspect | |
1 | 2 | from sqlalchemy.orm import class_mapper, object_mapper |
2 | 3 | from sqlalchemy.orm.exc import UnmappedClassError, UnmappedInstanceError |
3 | 4 | |
5 | from graphene import Argument, Enum, List | |
6 | ||
4 | 7 | |
5 | 8 | def get_session(context): |
6 | return context.get('session') | |
9 | return context.get("session") | |
7 | 10 | |
8 | 11 | |
9 | 12 | def get_query(model, context): |
10 | query = getattr(model, 'query', None) | |
13 | query = getattr(model, "query", None) | |
11 | 14 | if not query: |
12 | 15 | session = get_session(context) |
13 | 16 | 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 | ) | |
16 | 21 | query = session.query(model) |
17 | 22 | return query |
18 | 23 | |
33 | 38 | return False |
34 | 39 | else: |
35 | 40 | 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 | ||
0 | 3 | [flake8] |
1 | 4 | exclude = setup.py,docs/*,examples/*,tests |
2 | 5 | max-line-length = 120 |
3 | 6 | |
4 | [coverage:run] | |
5 | omit = */tests/* | |
6 | ||
7 | 7 | [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 | |
26 | 13 | |
27 | 14 | [bdist_wheel] |
28 | 15 | universal=1 |
0 | from setuptools import find_packages, setup | |
1 | import sys | |
2 | 0 | import ast |
3 | 1 | import re |
2 | import sys | |
4 | 3 | |
5 | _version_re = re.compile(r'__version__\s+=\s+(.*)') | |
4 | from setuptools import find_packages, setup | |
6 | 5 | |
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+(.*)") | |
10 | 7 | |
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 | ] | |
11 | 32 | |
12 | 33 | setup( |
13 | name='graphene-sqlalchemy', | |
34 | name="graphene-sqlalchemy", | |
14 | 35 | 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", | |
26 | 42 | 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", | |
37 | 55 | ], |
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, | |
55 | 68 | ) |
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} |