New upstream version 1.1.1
Sophie Brun
7 years ago
0 | # Created by https://www.gitignore.io | |
1 | ||
2 | ### Python ### | |
3 | # Byte-compiled / optimized / DLL files | |
4 | __pycache__/ | |
5 | *.py[cod] | |
6 | ||
7 | # C extensions | |
8 | *.so | |
9 | ||
10 | # Distribution / packaging | |
11 | .Python | |
12 | env/ | |
13 | build/ | |
14 | develop-eggs/ | |
15 | dist/ | |
16 | downloads/ | |
17 | eggs/ | |
18 | .eggs/ | |
19 | lib/ | |
20 | lib64/ | |
21 | parts/ | |
22 | sdist/ | |
23 | var/ | |
24 | *.egg-info/ | |
25 | .installed.cfg | |
26 | *.egg | |
27 | ||
28 | # PyInstaller | |
29 | # Usually these files are written by a python script from a template | |
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. | |
31 | *.manifest | |
32 | *.spec | |
33 | ||
34 | # Installer logs | |
35 | pip-log.txt | |
36 | pip-delete-this-directory.txt | |
37 | ||
38 | # Unit test / coverage reports | |
39 | htmlcov/ | |
40 | .tox/ | |
41 | .coverage | |
42 | .coverage.* | |
43 | .cache | |
44 | nosetests.xml | |
45 | coverage.xml | |
46 | *,cover | |
47 | ||
48 | # Translations | |
49 | *.mo | |
50 | *.pot | |
51 | ||
52 | # Django stuff: | |
53 | *.log | |
54 | ||
55 | # Sphinx documentation | |
56 | docs/_build/ | |
57 | ||
58 | # PyBuilder | |
59 | target/ | |
60 | ||
61 | # PyCharm | |
62 | .idea | |
63 | ||
64 | # Databases | |
65 | *.sqlite3 | |
66 | .vscode |
0 | language: python | |
1 | sudo: false | |
2 | python: | |
3 | - 2.7 | |
4 | - 3.4 | |
5 | - 3.5 | |
6 | - pypy | |
7 | before_install: | |
8 | - | | |
9 | if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then | |
10 | export PYENV_ROOT="$HOME/.pyenv" | |
11 | if [ -f "$PYENV_ROOT/bin/pyenv" ]; then | |
12 | cd "$PYENV_ROOT" && git pull | |
13 | else | |
14 | rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" | |
15 | fi | |
16 | export PYPY_VERSION="4.0.1" | |
17 | "$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" | |
18 | virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" | |
19 | source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" | |
20 | fi | |
21 | install: | |
22 | - | | |
23 | if [ "$TEST_TYPE" = build ]; then | |
24 | pip install pytest==3.0.2 pytest-cov pytest-benchmark coveralls six mock sqlalchemy_utils | |
25 | pip install -e . | |
26 | python setup.py develop | |
27 | elif [ "$TEST_TYPE" = lint ]; then | |
28 | pip install flake8 | |
29 | fi | |
30 | script: | |
31 | - | | |
32 | if [ "$TEST_TYPE" = lint ]; then | |
33 | echo "Checking Python code lint." | |
34 | flake8 graphene_sqlalchemy | |
35 | exit | |
36 | elif [ "$TEST_TYPE" = build ]; then | |
37 | py.test --cov=graphene_sqlalchemy graphene_sqlalchemy examples | |
38 | fi | |
39 | after_success: | |
40 | - | | |
41 | if [ "$TEST_TYPE" = build ]; then | |
42 | coveralls | |
43 | fi | |
44 | env: | |
45 | matrix: | |
46 | - TEST_TYPE=build | |
47 | matrix: | |
48 | fast_finish: true | |
49 | include: | |
50 | - python: '2.7' | |
51 | env: TEST_TYPE=lint | |
52 | deploy: | |
53 | provider: pypi | |
54 | user: syrusakbary | |
55 | on: | |
56 | tags: true | |
57 | password: | |
58 | secure: q0ey31cWljGB30l43aEd1KIPuAHRutzmsd2lBb/2zvD79ReBrzvCdFAkH2xcyo4Volk3aazQQTNUIurnTuvBxmtqja0e+gUaO5LdOcokVdOGyLABXh7qhd2kdvbTDWgSwA4EWneLGXn/SjXSe0f3pCcrwc6WDcLAHxtffMvO9gulpYQtUoOqXfMipMOkRD9iDWTJBsSo3trL70X1FHOVr6Yqi0mfkX2Y/imxn6wlTWRz28Ru94xrj27OmUnCv7qcG0taO8LNlUCquNFAr2sZ+l+U/GkQrrM1y+ehPz3pmI0cCCd7SX/7+EG9ViZ07BZ31nk4pgnqjmj3nFwqnCE/4IApGnduqtrMDF63C9TnB1TU8oJmbbUCu4ODwRpBPZMnwzaHsLnrpdrB89/98NtTfujdrh3U5bVB+t33yxrXVh+FjgLYj9PVeDixpFDn6V/Xcnv4BbRMNOhXIQT7a7/5b99RiXBjCk6KRu+Jdu5DZ+3G4Nbr4oim3kZFPUHa555qbzTlwAfkrQxKv3C3OdVJR7eGc9ADsbHyEJbdPNAh/T+xblXTXLS3hPYDvgM+WEGy3CytBDG3JVcXm25ZP96EDWjweJ7MyfylubhuKj/iR1Y1wiHeIsYq9CqRrFQUWL8gFJBfmgjs96xRXXXnvyLtKUKpKw3wFg5cR/6FnLeYZ8k= |
0 | Please read [UPGRADE-v1.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v1.0.md) | |
1 | to learn how to upgrade to Graphene `1.0`. | |
2 | ||
3 | --- | |
4 | ||
5 | # ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-SQLAlchemy [![Build Status](https://travis-ci.org/graphql-python/graphene-sqlalchemy.svg?branch=master)](https://travis-ci.org/graphql-python/graphene-sqlalchemy) [![PyPI version](https://badge.fury.io/py/graphene-sqlalchemy.svg)](https://badge.fury.io/py/graphene-sqlalchemy) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene-sqlalchemy/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene-sqlalchemy?branch=master) | |
6 | ||
7 | ||
8 | A [SQLAlchemy](http://www.sqlalchemy.org/) integration for [Graphene](http://graphene-python.org/). | |
9 | ||
10 | ## Installation | |
11 | ||
12 | For instaling graphene, just run this command in your shell | |
13 | ||
14 | ```bash | |
15 | pip install "graphene-sqlalchemy>=1.0" | |
16 | ``` | |
17 | ||
18 | ## Examples | |
19 | ||
20 | Here is a simple SQLAlchemy model: | |
21 | ||
22 | ```python | |
23 | from sqlalchemy import Column, Integer, String | |
24 | from sqlalchemy.orm import relationship | |
25 | ||
26 | from sqlalchemy.ext.declarative import declarative_base | |
27 | ||
28 | Base = declarative_base() | |
29 | ||
30 | class UserModel(Base): | |
31 | __tablename__ = 'department' | |
32 | id = Column(Integer, primary_key=True) | |
33 | name = Column(String) | |
34 | last_name = Column(String) | |
35 | ``` | |
36 | ||
37 | To create a GraphQL schema for it you simply have to write the following: | |
38 | ||
39 | ```python | |
40 | from graphene_sqlalchemy import SQLAlchemyObjectType | |
41 | ||
42 | class User(SQLAlchemyObjectType): | |
43 | class Meta: | |
44 | model = UserModel | |
45 | ||
46 | class Query(graphene.ObjectType): | |
47 | users = graphene.List(User) | |
48 | ||
49 | def resolve_users(self, args, context, info): | |
50 | query = User.get_query(context) # SQLAlchemy query | |
51 | return query.all() | |
52 | ||
53 | schema = graphene.Schema(query=Query) | |
54 | ``` | |
55 | ||
56 | Then you can simply query the schema: | |
57 | ||
58 | ```python | |
59 | query = ''' | |
60 | query { | |
61 | users { | |
62 | name, | |
63 | lastName | |
64 | } | |
65 | } | |
66 | ''' | |
67 | result = schema.execute(query, context_value={'session': db_session}) | |
68 | ``` | |
69 | ||
70 | To learn more check out the following [examples](examples/): | |
71 | ||
72 | * **Full example**: [Flask SQLAlchemy example](examples/flask_sqlalchemy) | |
73 | ||
74 | ||
75 | ## Contributing | |
76 | ||
77 | After cloning this repo, ensure dependencies are installed by running: | |
78 | ||
79 | ```sh | |
80 | python setup.py install | |
81 | ``` | |
82 | ||
83 | After developing, the full test suite can be evaluated by running: | |
84 | ||
85 | ```sh | |
86 | python setup.py test # Use --pytest-args="-v -s" for verbose mode | |
87 | ``` |
0 | Please read | |
1 | `UPGRADE-v1.0.md <https://github.com/graphql-python/graphene/blob/master/UPGRADE-v1.0.md>`__ | |
2 | to learn how to upgrade to Graphene ``1.0``. | |
3 | ||
4 | -------------- | |
5 | ||
6 | |Graphene Logo| Graphene-SQLAlchemy |Build Status| |PyPI version| |Coverage Status| | |
7 | =================================================================================== | |
8 | ||
9 | A `SQLAlchemy <http://www.sqlalchemy.org/>`__ integration for | |
10 | `Graphene <http://graphene-python.org/>`__. | |
11 | ||
12 | Installation | |
13 | ------------ | |
14 | ||
15 | For instaling graphene, just run this command in your shell | |
16 | ||
17 | .. code:: bash | |
18 | ||
19 | pip install "graphene-sqlalchemy>=1.0" | |
20 | ||
21 | Examples | |
22 | -------- | |
23 | ||
24 | Here is a simple SQLAlchemy model: | |
25 | ||
26 | .. code:: python | |
27 | ||
28 | from sqlalchemy import Column, Integer, String | |
29 | from sqlalchemy.orm import backref, relationship | |
30 | ||
31 | from sqlalchemy.ext.declarative import declarative_base | |
32 | ||
33 | Base = declarative_base() | |
34 | ||
35 | class UserModel(Base): | |
36 | __tablename__ = 'department' | |
37 | id = Column(Integer, primary_key=True) | |
38 | name = Column(String) | |
39 | last_name = Column(String) | |
40 | ||
41 | To create a GraphQL schema for it you simply have to write the | |
42 | following: | |
43 | ||
44 | .. code:: python | |
45 | ||
46 | from graphene_sqlalchemy import SQLAlchemyObjectType | |
47 | ||
48 | class User(SQLAlchemyObjectType): | |
49 | class Meta: | |
50 | model = UserModel | |
51 | ||
52 | class Query(graphene.ObjectType): | |
53 | users = graphene.List(User) | |
54 | ||
55 | def resolve_users(self, args, context, info): | |
56 | query = User.get_query(context) # SQLAlchemy query | |
57 | return query.all() | |
58 | ||
59 | schema = graphene.Schema(query=Query) | |
60 | ||
61 | Then you can simply query the schema: | |
62 | ||
63 | .. code:: python | |
64 | ||
65 | query = ''' | |
66 | query { | |
67 | users { | |
68 | name, | |
69 | lastName | |
70 | } | |
71 | } | |
72 | ''' | |
73 | result = schema.execute(query, context_value={'session': db_session}) | |
74 | ||
75 | To learn more check out the following `examples <examples/>`__: | |
76 | ||
77 | - **Full example**: `Flask SQLAlchemy | |
78 | example <examples/flask_sqlalchemy>`__ | |
79 | ||
80 | Contributing | |
81 | ------------ | |
82 | ||
83 | After cloning this repo, ensure dependencies are installed by running: | |
84 | ||
85 | .. code:: sh | |
86 | ||
87 | python setup.py install | |
88 | ||
89 | After developing, the full test suite can be evaluated by running: | |
90 | ||
91 | .. code:: sh | |
92 | ||
93 | python setup.py test # Use --pytest-args="-v -s" for verbose mode | |
94 | ||
95 | .. |Graphene Logo| image:: http://graphene-python.org/favicon.png | |
96 | .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-sqlalchemy.svg?branch=master | |
97 | :target: https://travis-ci.org/graphql-python/graphene-sqlalchemy | |
98 | .. |PyPI version| image:: https://badge.fury.io/py/graphene-sqlalchemy.svg | |
99 | :target: https://badge.fury.io/py/graphene-sqlalchemy | |
100 | .. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-sqlalchemy/badge.svg?branch=master&service=github | |
101 | :target: https://coveralls.io/github/graphql-python/graphene-sqlalchemy?branch=master |
0 | #!/bin/bash | |
1 | ||
2 | # Install the required scripts with | |
3 | # pip install autoflake autopep8 isort | |
4 | autoflake ./examples/ ./graphene_sqlalchemy/ -r --remove-unused-variables --remove-all-unused-imports --in-place | |
5 | autopep8 ./examples/ ./graphene_sqlalchemy/ -r --in-place --experimental --aggressive --max-line-length 120 | |
6 | isort -rc ./examples/ ./graphene_sqlalchemy/ |
0 | # Makefile for Sphinx documentation | |
1 | # | |
2 | ||
3 | # You can set these variables from the command line. | |
4 | SPHINXOPTS = | |
5 | SPHINXBUILD = sphinx-build | |
6 | PAPER = | |
7 | BUILDDIR = _build | |
8 | ||
9 | # Internal variables. | |
10 | PAPEROPT_a4 = -D latex_paper_size=a4 | |
11 | PAPEROPT_letter = -D latex_paper_size=letter | |
12 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
13 | # the i18n builder cannot share the environment and doctrees with the others | |
14 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
15 | ||
16 | .PHONY: help | |
17 | help: | |
18 | @echo "Please use \`make <target>' where <target> is one of" | |
19 | @echo " html to make standalone HTML files" | |
20 | @echo " dirhtml to make HTML files named index.html in directories" | |
21 | @echo " singlehtml to make a single large HTML file" | |
22 | @echo " pickle to make pickle files" | |
23 | @echo " json to make JSON files" | |
24 | @echo " htmlhelp to make HTML files and a HTML help project" | |
25 | @echo " qthelp to make HTML files and a qthelp project" | |
26 | @echo " applehelp to make an Apple Help Book" | |
27 | @echo " devhelp to make HTML files and a Devhelp project" | |
28 | @echo " epub to make an epub" | |
29 | @echo " epub3 to make an epub3" | |
30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | |
31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" | |
32 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | |
33 | @echo " text to make text files" | |
34 | @echo " man to make manual pages" | |
35 | @echo " texinfo to make Texinfo files" | |
36 | @echo " info to make Texinfo files and run them through makeinfo" | |
37 | @echo " gettext to make PO message catalogs" | |
38 | @echo " changes to make an overview of all changed/added/deprecated items" | |
39 | @echo " xml to make Docutils-native XML files" | |
40 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" | |
41 | @echo " linkcheck to check all external links for integrity" | |
42 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" | |
43 | @echo " coverage to run coverage check of the documentation (if enabled)" | |
44 | @echo " dummy to check syntax errors of document sources" | |
45 | ||
46 | .PHONY: clean | |
47 | clean: | |
48 | rm -rf $(BUILDDIR)/* | |
49 | ||
50 | .PHONY: html | |
51 | html: | |
52 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | |
53 | @echo | |
54 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | |
55 | ||
56 | .PHONY: dirhtml | |
57 | dirhtml: | |
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | |
59 | @echo | |
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | |
61 | ||
62 | .PHONY: singlehtml | |
63 | singlehtml: | |
64 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | |
65 | @echo | |
66 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | |
67 | ||
68 | .PHONY: pickle | |
69 | pickle: | |
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | |
71 | @echo | |
72 | @echo "Build finished; now you can process the pickle files." | |
73 | ||
74 | .PHONY: json | |
75 | json: | |
76 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | |
77 | @echo | |
78 | @echo "Build finished; now you can process the JSON files." | |
79 | ||
80 | .PHONY: htmlhelp | |
81 | htmlhelp: | |
82 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | |
83 | @echo | |
84 | @echo "Build finished; now you can run HTML Help Workshop with the" \ | |
85 | ".hhp project file in $(BUILDDIR)/htmlhelp." | |
86 | ||
87 | .PHONY: qthelp | |
88 | qthelp: | |
89 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | |
90 | @echo | |
91 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ | |
92 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | |
93 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Graphene.qhcp" | |
94 | @echo "To view the help file:" | |
95 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Graphene.qhc" | |
96 | ||
97 | .PHONY: applehelp | |
98 | applehelp: | |
99 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp | |
100 | @echo | |
101 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." | |
102 | @echo "N.B. You won't be able to view it unless you put it in" \ | |
103 | "~/Library/Documentation/Help or install it in your application" \ | |
104 | "bundle." | |
105 | ||
106 | .PHONY: devhelp | |
107 | devhelp: | |
108 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | |
109 | @echo | |
110 | @echo "Build finished." | |
111 | @echo "To view the help file:" | |
112 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Graphene" | |
113 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Graphene" | |
114 | @echo "# devhelp" | |
115 | ||
116 | .PHONY: epub | |
117 | epub: | |
118 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | |
119 | @echo | |
120 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." | |
121 | ||
122 | .PHONY: epub3 | |
123 | epub3: | |
124 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 | |
125 | @echo | |
126 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." | |
127 | ||
128 | .PHONY: latex | |
129 | latex: | |
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
131 | @echo | |
132 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | |
133 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ | |
134 | "(use \`make latexpdf' here to do that automatically)." | |
135 | ||
136 | .PHONY: latexpdf | |
137 | latexpdf: | |
138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
139 | @echo "Running LaTeX files through pdflatex..." | |
140 | $(MAKE) -C $(BUILDDIR)/latex all-pdf | |
141 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
142 | ||
143 | .PHONY: latexpdfja | |
144 | latexpdfja: | |
145 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
146 | @echo "Running LaTeX files through platex and dvipdfmx..." | |
147 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | |
148 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
149 | ||
150 | .PHONY: text | |
151 | text: | |
152 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | |
153 | @echo | |
154 | @echo "Build finished. The text files are in $(BUILDDIR)/text." | |
155 | ||
156 | .PHONY: man | |
157 | man: | |
158 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | |
159 | @echo | |
160 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." | |
161 | ||
162 | .PHONY: texinfo | |
163 | texinfo: | |
164 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
165 | @echo | |
166 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | |
167 | @echo "Run \`make' in that directory to run these through makeinfo" \ | |
168 | "(use \`make info' here to do that automatically)." | |
169 | ||
170 | .PHONY: info | |
171 | info: | |
172 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
173 | @echo "Running Texinfo files through makeinfo..." | |
174 | make -C $(BUILDDIR)/texinfo info | |
175 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | |
176 | ||
177 | .PHONY: gettext | |
178 | gettext: | |
179 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | |
180 | @echo | |
181 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | |
182 | ||
183 | .PHONY: changes | |
184 | changes: | |
185 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | |
186 | @echo | |
187 | @echo "The overview file is in $(BUILDDIR)/changes." | |
188 | ||
189 | .PHONY: linkcheck | |
190 | linkcheck: | |
191 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | |
192 | @echo | |
193 | @echo "Link check complete; look for any errors in the above output " \ | |
194 | "or in $(BUILDDIR)/linkcheck/output.txt." | |
195 | ||
196 | .PHONY: doctest | |
197 | doctest: | |
198 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | |
199 | @echo "Testing of doctests in the sources finished, look at the " \ | |
200 | "results in $(BUILDDIR)/doctest/output.txt." | |
201 | ||
202 | .PHONY: coverage | |
203 | coverage: | |
204 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage | |
205 | @echo "Testing of coverage in the sources finished, look at the " \ | |
206 | "results in $(BUILDDIR)/coverage/python.txt." | |
207 | ||
208 | .PHONY: xml | |
209 | xml: | |
210 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | |
211 | @echo | |
212 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." | |
213 | ||
214 | .PHONY: pseudoxml | |
215 | pseudoxml: | |
216 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | |
217 | @echo | |
218 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." | |
219 | ||
220 | .PHONY: dummy | |
221 | dummy: | |
222 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy | |
223 | @echo | |
224 | @echo "Build finished. Dummy builder generates no files." |
0 | import os | |
1 | ||
2 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' | |
3 | ||
4 | # -*- coding: utf-8 -*- | |
5 | # | |
6 | # Graphene documentation build configuration file, created by | |
7 | # sphinx-quickstart on Sun Sep 11 18:30:51 2016. | |
8 | # | |
9 | # This file is execfile()d with the current directory set to its | |
10 | # containing dir. | |
11 | # | |
12 | # Note that not all possible configuration values are present in this | |
13 | # autogenerated file. | |
14 | # | |
15 | # All configuration values have a default; values that are commented out | |
16 | # serve to show the default. | |
17 | ||
18 | # If extensions (or modules to document with autodoc) are in another directory, | |
19 | # add these directories to sys.path here. If the directory is relative to the | |
20 | # documentation root, use os.path.abspath to make it absolute, like shown here. | |
21 | # | |
22 | # import os | |
23 | # import sys | |
24 | # sys.path.insert(0, os.path.abspath('.')) | |
25 | ||
26 | # -- General configuration ------------------------------------------------ | |
27 | ||
28 | # If your documentation needs a minimal Sphinx version, state it here. | |
29 | # | |
30 | # needs_sphinx = '1.0' | |
31 | ||
32 | # Add any Sphinx extension module names here, as strings. They can be | |
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | |
34 | # ones. | |
35 | extensions = [ | |
36 | 'sphinx.ext.autodoc', | |
37 | 'sphinx.ext.intersphinx', | |
38 | 'sphinx.ext.todo', | |
39 | 'sphinx.ext.coverage', | |
40 | 'sphinx.ext.viewcode', | |
41 | ] | |
42 | if not on_rtd: | |
43 | extensions += [ | |
44 | 'sphinx.ext.githubpages', | |
45 | ] | |
46 | ||
47 | # Add any paths that contain templates here, relative to this directory. | |
48 | templates_path = ['_templates'] | |
49 | ||
50 | # The suffix(es) of source filenames. | |
51 | # You can specify multiple suffix as a list of string: | |
52 | # | |
53 | # source_suffix = ['.rst', '.md'] | |
54 | source_suffix = '.rst' | |
55 | ||
56 | # The encoding of source files. | |
57 | # | |
58 | # source_encoding = 'utf-8-sig' | |
59 | ||
60 | # The master toctree document. | |
61 | master_doc = 'index' | |
62 | ||
63 | # General information about the project. | |
64 | project = u'Graphene Django' | |
65 | copyright = u'Graphene 2016' | |
66 | author = u'Syrus Akbary' | |
67 | ||
68 | # The version info for the project you're documenting, acts as replacement for | |
69 | # |version| and |release|, also used in various other places throughout the | |
70 | # built documents. | |
71 | # | |
72 | # The short X.Y version. | |
73 | version = u'1.0' | |
74 | # The full version, including alpha/beta/rc tags. | |
75 | release = u'1.0.dev' | |
76 | ||
77 | # The language for content autogenerated by Sphinx. Refer to documentation | |
78 | # for a list of supported languages. | |
79 | # | |
80 | # This is also used if you do content translation via gettext catalogs. | |
81 | # Usually you set "language" from the command line for these cases. | |
82 | language = None | |
83 | ||
84 | # There are two options for replacing |today|: either, you set today to some | |
85 | # non-false value, then it is used: | |
86 | # | |
87 | # today = '' | |
88 | # | |
89 | # Else, today_fmt is used as the format for a strftime call. | |
90 | # | |
91 | # today_fmt = '%B %d, %Y' | |
92 | ||
93 | # List of patterns, relative to source directory, that match files and | |
94 | # directories to ignore when looking for source files. | |
95 | # This patterns also effect to html_static_path and html_extra_path | |
96 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] | |
97 | ||
98 | # The reST default role (used for this markup: `text`) to use for all | |
99 | # documents. | |
100 | # | |
101 | # default_role = None | |
102 | ||
103 | # If true, '()' will be appended to :func: etc. cross-reference text. | |
104 | # | |
105 | # add_function_parentheses = True | |
106 | ||
107 | # If true, the current module name will be prepended to all description | |
108 | # unit titles (such as .. function::). | |
109 | # | |
110 | # add_module_names = True | |
111 | ||
112 | # If true, sectionauthor and moduleauthor directives will be shown in the | |
113 | # output. They are ignored by default. | |
114 | # | |
115 | # show_authors = False | |
116 | ||
117 | # The name of the Pygments (syntax highlighting) style to use. | |
118 | pygments_style = 'sphinx' | |
119 | ||
120 | # A list of ignored prefixes for module index sorting. | |
121 | # modindex_common_prefix = [] | |
122 | ||
123 | # If true, keep warnings as "system message" paragraphs in the built documents. | |
124 | # keep_warnings = False | |
125 | ||
126 | # If true, `todo` and `todoList` produce output, else they produce nothing. | |
127 | todo_include_todos = True | |
128 | ||
129 | ||
130 | # -- Options for HTML output ---------------------------------------------- | |
131 | ||
132 | # The theme to use for HTML and HTML Help pages. See the documentation for | |
133 | # a list of builtin themes. | |
134 | # | |
135 | # html_theme = 'alabaster' | |
136 | # if on_rtd: | |
137 | # html_theme = 'sphinx_rtd_theme' | |
138 | import sphinx_graphene_theme | |
139 | ||
140 | html_theme = "sphinx_graphene_theme" | |
141 | ||
142 | html_theme_path = [sphinx_graphene_theme.get_html_theme_path()] | |
143 | ||
144 | ||
145 | # Theme options are theme-specific and customize the look and feel of a theme | |
146 | # further. For a list of options available for each theme, see the | |
147 | # documentation. | |
148 | # | |
149 | # html_theme_options = {} | |
150 | ||
151 | # Add any paths that contain custom themes here, relative to this directory. | |
152 | # html_theme_path = [] | |
153 | ||
154 | # The name for this set of Sphinx documents. | |
155 | # "<project> v<release> documentation" by default. | |
156 | # | |
157 | # html_title = u'Graphene v1.0.dev' | |
158 | ||
159 | # A shorter title for the navigation bar. Default is the same as html_title. | |
160 | # | |
161 | # html_short_title = None | |
162 | ||
163 | # The name of an image file (relative to this directory) to place at the top | |
164 | # of the sidebar. | |
165 | # | |
166 | # html_logo = None | |
167 | ||
168 | # The name of an image file (relative to this directory) to use as a favicon of | |
169 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 | |
170 | # pixels large. | |
171 | # | |
172 | # html_favicon = None | |
173 | ||
174 | # Add any paths that contain custom static files (such as style sheets) here, | |
175 | # relative to this directory. They are copied after the builtin static files, | |
176 | # so a file named "default.css" will overwrite the builtin "default.css". | |
177 | html_static_path = ['_static'] | |
178 | ||
179 | # Add any extra paths that contain custom files (such as robots.txt or | |
180 | # .htaccess) here, relative to this directory. These files are copied | |
181 | # directly to the root of the documentation. | |
182 | # | |
183 | # html_extra_path = [] | |
184 | ||
185 | # If not None, a 'Last updated on:' timestamp is inserted at every page | |
186 | # bottom, using the given strftime format. | |
187 | # The empty string is equivalent to '%b %d, %Y'. | |
188 | # | |
189 | # html_last_updated_fmt = None | |
190 | ||
191 | # If true, SmartyPants will be used to convert quotes and dashes to | |
192 | # typographically correct entities. | |
193 | # | |
194 | # html_use_smartypants = True | |
195 | ||
196 | # Custom sidebar templates, maps document names to template names. | |
197 | # | |
198 | # html_sidebars = {} | |
199 | ||
200 | # Additional templates that should be rendered to pages, maps page names to | |
201 | # template names. | |
202 | # | |
203 | # html_additional_pages = {} | |
204 | ||
205 | # If false, no module index is generated. | |
206 | # | |
207 | # html_domain_indices = True | |
208 | ||
209 | # If false, no index is generated. | |
210 | # | |
211 | # html_use_index = True | |
212 | ||
213 | # If true, the index is split into individual pages for each letter. | |
214 | # | |
215 | # html_split_index = False | |
216 | ||
217 | # If true, links to the reST sources are added to the pages. | |
218 | # | |
219 | # html_show_sourcelink = True | |
220 | ||
221 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | |
222 | # | |
223 | # html_show_sphinx = True | |
224 | ||
225 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | |
226 | # | |
227 | # html_show_copyright = True | |
228 | ||
229 | # If true, an OpenSearch description file will be output, and all pages will | |
230 | # contain a <link> tag referring to it. The value of this option must be the | |
231 | # base URL from which the finished HTML is served. | |
232 | # | |
233 | # html_use_opensearch = '' | |
234 | ||
235 | # This is the file name suffix for HTML files (e.g. ".xhtml"). | |
236 | # html_file_suffix = None | |
237 | ||
238 | # Language to be used for generating the HTML full-text search index. | |
239 | # Sphinx supports the following languages: | |
240 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' | |
241 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' | |
242 | # | |
243 | # html_search_language = 'en' | |
244 | ||
245 | # A dictionary with options for the search language support, empty by default. | |
246 | # 'ja' uses this config value. | |
247 | # 'zh' user can custom change `jieba` dictionary path. | |
248 | # | |
249 | # html_search_options = {'type': 'default'} | |
250 | ||
251 | # The name of a javascript file (relative to the configuration directory) that | |
252 | # implements a search results scorer. If empty, the default will be used. | |
253 | # | |
254 | # html_search_scorer = 'scorer.js' | |
255 | ||
256 | # Output file base name for HTML help builder. | |
257 | htmlhelp_basename = 'Graphenedoc' | |
258 | ||
259 | # -- Options for LaTeX output --------------------------------------------- | |
260 | ||
261 | latex_elements = { | |
262 | # The paper size ('letterpaper' or 'a4paper'). | |
263 | # | |
264 | # 'papersize': 'letterpaper', | |
265 | ||
266 | # The font size ('10pt', '11pt' or '12pt'). | |
267 | # | |
268 | # 'pointsize': '10pt', | |
269 | ||
270 | # Additional stuff for the LaTeX preamble. | |
271 | # | |
272 | # 'preamble': '', | |
273 | ||
274 | # Latex figure (float) alignment | |
275 | # | |
276 | # 'figure_align': 'htbp', | |
277 | } | |
278 | ||
279 | # Grouping the document tree into LaTeX files. List of tuples | |
280 | # (source start file, target name, title, | |
281 | # author, documentclass [howto, manual, or own class]). | |
282 | latex_documents = [ | |
283 | (master_doc, 'Graphene.tex', u'Graphene Documentation', | |
284 | u'Syrus Akbary', 'manual'), | |
285 | ] | |
286 | ||
287 | # The name of an image file (relative to this directory) to place at the top of | |
288 | # the title page. | |
289 | # | |
290 | # latex_logo = None | |
291 | ||
292 | # For "manual" documents, if this is true, then toplevel headings are parts, | |
293 | # not chapters. | |
294 | # | |
295 | # latex_use_parts = False | |
296 | ||
297 | # If true, show page references after internal links. | |
298 | # | |
299 | # latex_show_pagerefs = False | |
300 | ||
301 | # If true, show URL addresses after external links. | |
302 | # | |
303 | # latex_show_urls = False | |
304 | ||
305 | # Documents to append as an appendix to all manuals. | |
306 | # | |
307 | # latex_appendices = [] | |
308 | ||
309 | # It false, will not define \strong, \code, itleref, \crossref ... but only | |
310 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added | |
311 | # packages. | |
312 | # | |
313 | # latex_keep_old_macro_names = True | |
314 | ||
315 | # If false, no module index is generated. | |
316 | # | |
317 | # latex_domain_indices = True | |
318 | ||
319 | ||
320 | # -- Options for manual page output --------------------------------------- | |
321 | ||
322 | # One entry per manual page. List of tuples | |
323 | # (source start file, name, description, authors, manual section). | |
324 | man_pages = [ | |
325 | (master_doc, 'graphene_django', u'Graphene Django Documentation', | |
326 | [author], 1) | |
327 | ] | |
328 | ||
329 | # If true, show URL addresses after external links. | |
330 | # | |
331 | # man_show_urls = False | |
332 | ||
333 | ||
334 | # -- Options for Texinfo output ------------------------------------------- | |
335 | ||
336 | # Grouping the document tree into Texinfo files. List of tuples | |
337 | # (source start file, target name, title, author, | |
338 | # dir menu entry, description, category) | |
339 | texinfo_documents = [ | |
340 | (master_doc, 'Graphene-Django', u'Graphene Django Documentation', | |
341 | author, 'Graphene Django', 'One line description of project.', | |
342 | 'Miscellaneous'), | |
343 | ] | |
344 | ||
345 | # Documents to append as an appendix to all manuals. | |
346 | # | |
347 | # texinfo_appendices = [] | |
348 | ||
349 | # If false, no module index is generated. | |
350 | # | |
351 | # texinfo_domain_indices = True | |
352 | ||
353 | # How to display URL addresses: 'footnote', 'no', or 'inline'. | |
354 | # | |
355 | # texinfo_show_urls = 'footnote' | |
356 | ||
357 | # If true, do not generate a @detailmenu in the "Top" node's menu. | |
358 | # | |
359 | # texinfo_no_detailmenu = False | |
360 | ||
361 | ||
362 | # -- Options for Epub output ---------------------------------------------- | |
363 | ||
364 | # Bibliographic Dublin Core info. | |
365 | epub_title = project | |
366 | epub_author = author | |
367 | epub_publisher = author | |
368 | epub_copyright = copyright | |
369 | ||
370 | # The basename for the epub file. It defaults to the project name. | |
371 | # epub_basename = project | |
372 | ||
373 | # The HTML theme for the epub output. Since the default themes are not | |
374 | # optimized for small screen space, using the same theme for HTML and epub | |
375 | # output is usually not wise. This defaults to 'epub', a theme designed to save | |
376 | # visual space. | |
377 | # | |
378 | # epub_theme = 'epub' | |
379 | ||
380 | # The language of the text. It defaults to the language option | |
381 | # or 'en' if the language is not set. | |
382 | # | |
383 | # epub_language = '' | |
384 | ||
385 | # The scheme of the identifier. Typical schemes are ISBN or URL. | |
386 | # epub_scheme = '' | |
387 | ||
388 | # The unique identifier of the text. This can be a ISBN number | |
389 | # or the project homepage. | |
390 | # | |
391 | # epub_identifier = '' | |
392 | ||
393 | # A unique identification for the text. | |
394 | # | |
395 | # epub_uid = '' | |
396 | ||
397 | # A tuple containing the cover image and cover page html template filenames. | |
398 | # | |
399 | # epub_cover = () | |
400 | ||
401 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. | |
402 | # | |
403 | # epub_guide = () | |
404 | ||
405 | # HTML files that should be inserted before the pages created by sphinx. | |
406 | # The format is a list of tuples containing the path and title. | |
407 | # | |
408 | # epub_pre_files = [] | |
409 | ||
410 | # HTML files that should be inserted after the pages created by sphinx. | |
411 | # The format is a list of tuples containing the path and title. | |
412 | # | |
413 | # epub_post_files = [] | |
414 | ||
415 | # A list of files that should not be packed into the epub file. | |
416 | epub_exclude_files = ['search.html'] | |
417 | ||
418 | # The depth of the table of contents in toc.ncx. | |
419 | # | |
420 | # epub_tocdepth = 3 | |
421 | ||
422 | # Allow duplicate toc entries. | |
423 | # | |
424 | # epub_tocdup = True | |
425 | ||
426 | # Choose between 'default' and 'includehidden'. | |
427 | # | |
428 | # epub_tocscope = 'default' | |
429 | ||
430 | # Fix unsupported image types using the Pillow. | |
431 | # | |
432 | # epub_fix_images = False | |
433 | ||
434 | # Scale large images. | |
435 | # | |
436 | # epub_max_image_width = 0 | |
437 | ||
438 | # How to display URL addresses: 'footnote', 'no', or 'inline'. | |
439 | # | |
440 | # epub_show_urls = 'inline' | |
441 | ||
442 | # If false, no index is generated. | |
443 | # | |
444 | # epub_use_index = True | |
445 | ||
446 | ||
447 | # Example configuration for intersphinx: refer to the Python standard library. | |
448 | intersphinx_mapping = {'https://docs.python.org/': None} |
0 | Graphene-SQLAlchemy | |
1 | =================== | |
2 | ||
3 | Contents: | |
4 | ||
5 | .. toctree:: | |
6 | :maxdepth: 0 | |
7 | ||
8 | tutorial | |
9 | tips |
0 | ==== | |
1 | Tips | |
2 | ==== | |
3 | ||
4 | Tips | |
5 | ==== | |
6 | ||
7 | Querying | |
8 | -------- | |
9 | ||
10 | For make querying to the database work, there are two alternatives: | |
11 | ||
12 | - Set the db session when you do the execution: | |
13 | ||
14 | .. code:: python | |
15 | ||
16 | schema = graphene.Schema() | |
17 | schema.execute(context_value={'session': session}) | |
18 | ||
19 | - Create a query for the models. | |
20 | ||
21 | .. code:: python | |
22 | ||
23 | Base = declarative_base() | |
24 | Base.query = db_session.query_property() | |
25 | ||
26 | class MyModel(Base): | |
27 | # ... | |
28 | ||
29 | If you don't specify any, the following error will be displayed: | |
30 | ||
31 | ``A query in the model Base or a session in the schema is required for querying.`` |
0 | SQLAlchemy + Flask Tutorial | |
1 | =========================== | |
2 | ||
3 | Graphene comes with builtin support to SQLAlchemy, which makes quite | |
4 | easy to operate with your current models. | |
5 | ||
6 | Note: The code in this tutorial is pulled from the `Flask SQLAlchemy | |
7 | example | |
8 | app <https://github.com/graphql-python/graphene-sqlalchemy/tree/master/examples/flask_sqlalchemy>`__. | |
9 | ||
10 | Setup the Project | |
11 | ----------------- | |
12 | ||
13 | We will setup the project, execute the following: | |
14 | ||
15 | .. code:: bash | |
16 | ||
17 | # Create the project directory | |
18 | mkdir flask_sqlalchemy | |
19 | cd flask_sqlalchemy | |
20 | ||
21 | # Create a virtualenv to isolate our package dependencies locally | |
22 | virtualenv env | |
23 | source env/bin/activate # On Windows use `env\Scripts\activate` | |
24 | ||
25 | # SQLAlchemy and Graphene with SQLAlchemy support | |
26 | pip install SQLAlchemy | |
27 | pip install graphene_sqlalchemy | |
28 | ||
29 | # Install Flask and GraphQL Flask for exposing the schema through HTTP | |
30 | pip install Flask | |
31 | pip install Flask-GraphQL | |
32 | ||
33 | Defining our models | |
34 | ------------------- | |
35 | ||
36 | Let's get started with these models: | |
37 | ||
38 | .. code:: python | |
39 | ||
40 | # flask_sqlalchemy/models.py | |
41 | from sqlalchemy import * | |
42 | from sqlalchemy.orm import (scoped_session, sessionmaker, relationship, | |
43 | backref) | |
44 | from sqlalchemy.ext.declarative import declarative_base | |
45 | ||
46 | engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True) | |
47 | db_session = scoped_session(sessionmaker(autocommit=False, | |
48 | autoflush=False, | |
49 | bind=engine)) | |
50 | ||
51 | Base = declarative_base() | |
52 | # We will need this for querying | |
53 | Base.query = db_session.query_property() | |
54 | ||
55 | ||
56 | class Department(Base): | |
57 | __tablename__ = 'department' | |
58 | id = Column(Integer, primary_key=True) | |
59 | name = Column(String) | |
60 | ||
61 | ||
62 | class Employee(Base): | |
63 | __tablename__ = 'employee' | |
64 | id = Column(Integer, primary_key=True) | |
65 | name = Column(String) | |
66 | hired_on = Column(DateTime, default=func.now()) | |
67 | department_id = Column(Integer, ForeignKey('department.id')) | |
68 | department = relationship( | |
69 | Department, | |
70 | backref=backref('employees', | |
71 | uselist=True, | |
72 | cascade='delete,all')) | |
73 | ||
74 | Schema | |
75 | ------ | |
76 | ||
77 | GraphQL presents your objects to the world as a graph structure rather | |
78 | than a more hierarchical structure to which you may be accustomed. In | |
79 | order to create this representation, Graphene needs to know about each | |
80 | *type* of object which will appear in the graph. | |
81 | ||
82 | This graph also has a *root type* through which all access begins. This | |
83 | is the ``Query`` class below. In this example, we provide the ability to | |
84 | list all employees via ``all_employees``, and the ability to obtain a | |
85 | specific node via ``node``. | |
86 | ||
87 | Create ``flask_sqlalchemy/schema.py`` and type the following: | |
88 | ||
89 | .. code:: python | |
90 | ||
91 | # flask_sqlalchemy/schema.py | |
92 | import graphene | |
93 | from graphene import relay | |
94 | from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField | |
95 | from models import db_session, Department as DepartmentModel, Employee as EmployeeModel | |
96 | ||
97 | schema = graphene.Schema() | |
98 | ||
99 | ||
100 | class Department(SQLAlchemyObjectType): | |
101 | class Meta: | |
102 | model = DepartmentModel | |
103 | interfaces = (relay.Node, ) | |
104 | ||
105 | ||
106 | class Employee(SQLAlchemyObjectType): | |
107 | class Meta: | |
108 | model = EmployeeModel | |
109 | interfaces = (relay.Node, ) | |
110 | ||
111 | ||
112 | class Query(graphene.ObjectType): | |
113 | node = relay.Node.Field() | |
114 | all_employees = SQLAlchemyConnectionField(Employee) | |
115 | ||
116 | schema.query = Query | |
117 | ||
118 | Creating GraphQL and GraphiQL views in Flask | |
119 | -------------------------------------------- | |
120 | ||
121 | Unlike a RESTful API, there is only a single URL from which GraphQL is | |
122 | accessed. | |
123 | ||
124 | We are going to use Flask to create a server that expose the GraphQL | |
125 | schema under ``/graphql`` and a interface for querying it easily: | |
126 | GraphiQL (also under ``/graphql`` when accessed by a browser). | |
127 | ||
128 | Fortunately for us, the library ``Flask-GraphQL`` that we previously | |
129 | installed makes this task quite easy. | |
130 | ||
131 | .. code:: python | |
132 | ||
133 | # flask_sqlalchemy/app.py | |
134 | from flask import Flask | |
135 | from flask_graphql import GraphQLView | |
136 | ||
137 | from models import db_session | |
138 | from schema import schema, Department | |
139 | ||
140 | app = Flask(__name__) | |
141 | app.debug = True | |
142 | ||
143 | app.add_url_rule( | |
144 | '/graphql', | |
145 | view_func=GraphQLView.as_view( | |
146 | 'graphql', | |
147 | schema=schema, | |
148 | graphiql=True # for having the GraphiQL interface | |
149 | ) | |
150 | ) | |
151 | ||
152 | @app.teardown_appcontext | |
153 | def shutdown_session(exception=None): | |
154 | db_session.remove() | |
155 | ||
156 | if __name__ == '__main__': | |
157 | app.run() | |
158 | ||
159 | Creating some data | |
160 | ------------------ | |
161 | ||
162 | .. code:: bash | |
163 | ||
164 | $ python | |
165 | ||
166 | >>> from models import engine, db_session, Base, Department, Employee | |
167 | >>> Base.metadata.create_all(bind=engine) | |
168 | ||
169 | >>> # Fill the tables with some data | |
170 | >>> engineering = Department(name='Engineering') | |
171 | >>> db_session.add(engineering) | |
172 | >>> hr = Department(name='Human Resources') | |
173 | >>> db_session.add(hr) | |
174 | ||
175 | >>> peter = Employee(name='Peter', department=engineering) | |
176 | >>> db_session.add(peter) | |
177 | >>> roy = Employee(name='Roy', department=engineering) | |
178 | >>> db_session.add(roy) | |
179 | >>> tracy = Employee(name='Tracy', department=hr) | |
180 | >>> db_session.add(tracy) | |
181 | >>> db_session.commit() | |
182 | ||
183 | Testing our GraphQL schema | |
184 | -------------------------- | |
185 | ||
186 | We're now ready to test the API we've built. Let's fire up the server | |
187 | from the command line. | |
188 | ||
189 | .. code:: bash | |
190 | ||
191 | $ python ./app.py | |
192 | ||
193 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) | |
194 | ||
195 | Go to `localhost:5000/graphql <http://localhost:5000/graphql>`__ and | |
196 | type your first query! | |
197 | ||
198 | .. code:: | |
199 | ||
200 | { | |
201 | allEmployees { | |
202 | edges { | |
203 | node { | |
204 | id | |
205 | name | |
206 | department { | |
207 | name | |
208 | } | |
209 | } | |
210 | } | |
211 | } | |
212 | } |
0 | Example Flask+SQLAlchemy Project | |
1 | ================================ | |
2 | ||
3 | This example project demos integration between Graphene, Flask and SQLAlchemy. | |
4 | The project contains two models, one named `Department` and another | |
5 | named `Employee`. | |
6 | ||
7 | Getting started | |
8 | --------------- | |
9 | ||
10 | First you'll need to get the source of the project. Do this by cloning the | |
11 | whole Graphene repository: | |
12 | ||
13 | ```bash | |
14 | # Get the example project code | |
15 | git clone https://github.com/graphql-python/graphene-sqlalchemy.git | |
16 | cd graphene-sqlalchemy/examples/flask_sqlalchemy | |
17 | ``` | |
18 | ||
19 | It is good idea (but not required) to create a virtual environment | |
20 | for this project. We'll do this using | |
21 | [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) | |
22 | to keep things simple, | |
23 | but you may also find something like | |
24 | [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) | |
25 | to be useful: | |
26 | ||
27 | ```bash | |
28 | # Create a virtualenv in which we can install the dependencies | |
29 | virtualenv env | |
30 | source env/bin/activate | |
31 | ``` | |
32 | ||
33 | Now we can install our dependencies: | |
34 | ||
35 | ```bash | |
36 | pip install -r requirements.txt | |
37 | ``` | |
38 | ||
39 | Now the following command will setup the database, and start the server: | |
40 | ||
41 | ```bash | |
42 | ./app.py | |
43 | ||
44 | ``` | |
45 | ||
46 | ||
47 | Now head on over to | |
48 | [http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql) | |
49 | and run some queries! |
0 | #!/usr/bin/env python | |
1 | ||
2 | from flask import Flask | |
3 | ||
4 | from database import db_session, init_db | |
5 | from flask_graphql import GraphQLView | |
6 | from schema import schema | |
7 | ||
8 | app = Flask(__name__) | |
9 | app.debug = True | |
10 | ||
11 | default_query = ''' | |
12 | { | |
13 | allEmployees { | |
14 | edges { | |
15 | node { | |
16 | id, | |
17 | name, | |
18 | department { | |
19 | id, | |
20 | name | |
21 | }, | |
22 | role { | |
23 | id, | |
24 | name | |
25 | } | |
26 | } | |
27 | } | |
28 | } | |
29 | }'''.strip() | |
30 | ||
31 | ||
32 | app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)) | |
33 | ||
34 | ||
35 | @app.teardown_appcontext | |
36 | def shutdown_session(exception=None): | |
37 | db_session.remove() | |
38 | ||
39 | if __name__ == '__main__': | |
40 | init_db() | |
41 | app.run() |
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 | import graphene | |
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 | |
6 | ||
7 | ||
8 | class Department(SQLAlchemyObjectType): | |
9 | ||
10 | class Meta: | |
11 | model = DepartmentModel | |
12 | interfaces = (relay.Node, ) | |
13 | ||
14 | ||
15 | class Employee(SQLAlchemyObjectType): | |
16 | ||
17 | class Meta: | |
18 | model = EmployeeModel | |
19 | interfaces = (relay.Node, ) | |
20 | ||
21 | ||
22 | class Role(SQLAlchemyObjectType): | |
23 | ||
24 | class Meta: | |
25 | model = RoleModel | |
26 | interfaces = (relay.Node, ) | |
27 | ||
28 | ||
29 | class Query(graphene.ObjectType): | |
30 | node = relay.Node.Field() | |
31 | all_employees = SQLAlchemyConnectionField(Employee) | |
32 | all_roles = SQLAlchemyConnectionField(Role) | |
33 | role = graphene.Field(Role) | |
34 | ||
35 | ||
36 | schema = graphene.Schema(query=Query, types=[Department, Employee, Role]) |
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 | ) | |
10 | ||
11 | __all__ = ['SQLAlchemyObjectType', | |
12 | 'SQLAlchemyConnectionField', | |
13 | 'get_query', | |
14 | 'get_session'] |
0 | from singledispatch import singledispatch | |
1 | from sqlalchemy import types | |
2 | from sqlalchemy.dialects import postgresql | |
3 | from sqlalchemy.orm import interfaces | |
4 | ||
5 | from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, | |
6 | String) | |
7 | from graphene.relay import is_node | |
8 | from graphene.types.json import JSONString | |
9 | ||
10 | from .fields import SQLAlchemyConnectionField | |
11 | ||
12 | try: | |
13 | from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType | |
14 | except ImportError: | |
15 | class ChoiceType(object): | |
16 | pass | |
17 | ||
18 | class ScalarListType(object): | |
19 | pass | |
20 | ||
21 | class JSONType(object): | |
22 | pass | |
23 | ||
24 | ||
25 | def get_column_doc(column): | |
26 | return getattr(column, 'doc', None) | |
27 | ||
28 | ||
29 | def is_column_nullable(column): | |
30 | return bool(getattr(column, 'nullable', False)) | |
31 | ||
32 | ||
33 | def convert_sqlalchemy_relationship(relationship, registry): | |
34 | direction = relationship.direction | |
35 | model = relationship.mapper.entity | |
36 | ||
37 | def dynamic_type(): | |
38 | _type = registry.get_type_for_model(model) | |
39 | if not _type: | |
40 | return None | |
41 | if (direction == interfaces.MANYTOONE or not relationship.uselist): | |
42 | return Field(_type) | |
43 | elif (direction == interfaces.ONETOMANY or | |
44 | direction == interfaces.MANYTOMANY): | |
45 | if is_node(_type): | |
46 | return SQLAlchemyConnectionField(_type) | |
47 | return Field(List(_type)) | |
48 | ||
49 | return Dynamic(dynamic_type) | |
50 | ||
51 | ||
52 | def convert_sqlalchemy_composite(composite, registry): | |
53 | converter = registry.get_converter_for_composite(composite.composite_class) | |
54 | if not converter: | |
55 | try: | |
56 | raise Exception( | |
57 | "Don't know how to convert the composite field %s (%s)" % | |
58 | (composite, composite.composite_class)) | |
59 | except AttributeError: | |
60 | # handle fields that are not attached to a class yet (don't have a parent) | |
61 | raise Exception( | |
62 | "Don't know how to convert the composite field %r (%s)" % | |
63 | (composite, composite.composite_class)) | |
64 | return converter(composite, registry) | |
65 | ||
66 | ||
67 | def _register_composite_class(cls, registry=None): | |
68 | if registry is None: | |
69 | from .registry import get_global_registry | |
70 | registry = get_global_registry() | |
71 | ||
72 | def inner(fn): | |
73 | registry.register_composite_converter(cls, fn) | |
74 | return inner | |
75 | ||
76 | ||
77 | convert_sqlalchemy_composite.register = _register_composite_class | |
78 | ||
79 | ||
80 | def convert_sqlalchemy_column(column, registry=None): | |
81 | return convert_sqlalchemy_type(getattr(column, 'type', None), column, registry) | |
82 | ||
83 | ||
84 | @singledispatch | |
85 | def convert_sqlalchemy_type(type, column, registry=None): | |
86 | raise Exception( | |
87 | "Don't know how to convert the SQLAlchemy field %s (%s)" % (column, column.__class__)) | |
88 | ||
89 | ||
90 | @convert_sqlalchemy_type.register(types.Date) | |
91 | @convert_sqlalchemy_type.register(types.Time) | |
92 | @convert_sqlalchemy_type.register(types.String) | |
93 | @convert_sqlalchemy_type.register(types.Text) | |
94 | @convert_sqlalchemy_type.register(types.Unicode) | |
95 | @convert_sqlalchemy_type.register(types.UnicodeText) | |
96 | @convert_sqlalchemy_type.register(types.Enum) | |
97 | @convert_sqlalchemy_type.register(postgresql.ENUM) | |
98 | @convert_sqlalchemy_type.register(postgresql.UUID) | |
99 | def convert_column_to_string(type, column, registry=None): | |
100 | return String(description=get_column_doc(column), | |
101 | required=not(is_column_nullable(column))) | |
102 | ||
103 | ||
104 | @convert_sqlalchemy_type.register(types.DateTime) | |
105 | def convert_column_to_datetime(type, column, registry=None): | |
106 | from graphene.types.datetime import DateTime | |
107 | return DateTime(description=get_column_doc(column), | |
108 | required=not(is_column_nullable(column))) | |
109 | ||
110 | ||
111 | @convert_sqlalchemy_type.register(types.SmallInteger) | |
112 | @convert_sqlalchemy_type.register(types.Integer) | |
113 | def convert_column_to_int_or_id(type, column, registry=None): | |
114 | if column.primary_key: | |
115 | return ID(description=get_column_doc(column), required=not (is_column_nullable(column))) | |
116 | else: | |
117 | return Int(description=get_column_doc(column), | |
118 | required=not (is_column_nullable(column))) | |
119 | ||
120 | ||
121 | @convert_sqlalchemy_type.register(types.Boolean) | |
122 | def convert_column_to_boolean(type, column, registry=None): | |
123 | return Boolean(description=get_column_doc(column), required=not(is_column_nullable(column))) | |
124 | ||
125 | ||
126 | @convert_sqlalchemy_type.register(types.Float) | |
127 | @convert_sqlalchemy_type.register(types.Numeric) | |
128 | @convert_sqlalchemy_type.register(types.BigInteger) | |
129 | def convert_column_to_float(type, column, registry=None): | |
130 | return Float(description=get_column_doc(column), required=not(is_column_nullable(column))) | |
131 | ||
132 | ||
133 | @convert_sqlalchemy_type.register(ChoiceType) | |
134 | def convert_column_to_enum(type, column, registry=None): | |
135 | name = '{}_{}'.format(column.table.name, column.name).upper() | |
136 | return Enum(name, type.choices, description=get_column_doc(column)) | |
137 | ||
138 | ||
139 | @convert_sqlalchemy_type.register(ScalarListType) | |
140 | def convert_scalar_list_to_list(type, column, registry=None): | |
141 | return List(String, description=get_column_doc(column)) | |
142 | ||
143 | ||
144 | @convert_sqlalchemy_type.register(postgresql.ARRAY) | |
145 | def convert_postgres_array_to_list(_type, column, registry=None): | |
146 | graphene_type = convert_sqlalchemy_type(column.type.item_type, column) | |
147 | inner_type = type(graphene_type) | |
148 | return List(inner_type, description=get_column_doc(column), required=not(is_column_nullable(column))) | |
149 | ||
150 | ||
151 | @convert_sqlalchemy_type.register(postgresql.HSTORE) | |
152 | @convert_sqlalchemy_type.register(postgresql.JSON) | |
153 | @convert_sqlalchemy_type.register(postgresql.JSONB) | |
154 | def convert_json_to_string(type, column, registry=None): | |
155 | return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column))) | |
156 | ||
157 | ||
158 | @convert_sqlalchemy_type.register(JSONType) | |
159 | def convert_json_type_to_string(type, column, registry=None): | |
160 | return JSONString(description=get_column_doc(column), required=not(is_column_nullable(column))) |
0 | from functools import partial | |
1 | ||
2 | from sqlalchemy.orm.query import Query | |
3 | ||
4 | from graphene.relay import ConnectionField | |
5 | from graphene.relay.connection import PageInfo | |
6 | from graphql_relay.connection.arrayconnection import connection_from_list_slice | |
7 | ||
8 | from .utils import get_query | |
9 | ||
10 | ||
11 | class SQLAlchemyConnectionField(ConnectionField): | |
12 | ||
13 | @property | |
14 | def model(self): | |
15 | return self.type._meta.node._meta.model | |
16 | ||
17 | @classmethod | |
18 | def get_query(cls, model, context, info, args): | |
19 | return get_query(model, context) | |
20 | ||
21 | @classmethod | |
22 | def connection_resolver(cls, resolver, connection, model, root, args, context, info): | |
23 | iterable = resolver(root, args, context, info) | |
24 | if iterable is None: | |
25 | iterable = cls.get_query(model, context, info, args) | |
26 | if isinstance(iterable, Query): | |
27 | _len = iterable.count() | |
28 | else: | |
29 | _len = len(iterable) | |
30 | return connection_from_list_slice( | |
31 | iterable, | |
32 | args, | |
33 | slice_start=0, | |
34 | list_length=_len, | |
35 | list_slice_length=_len, | |
36 | connection_type=connection, | |
37 | pageinfo_type=PageInfo, | |
38 | edge_type=connection.Edge, | |
39 | ) | |
40 | ||
41 | def get_resolver(self, parent_resolver): | |
42 | return partial(self.connection_resolver, parent_resolver, self.type, self.model) |
0 | class Registry(object): | |
1 | ||
2 | def __init__(self): | |
3 | self._registry = {} | |
4 | self._registry_models = {} | |
5 | self._registry_composites = {} | |
6 | ||
7 | def register(self, cls): | |
8 | from .types import SQLAlchemyObjectType | |
9 | assert issubclass( | |
10 | cls, SQLAlchemyObjectType), 'Only SQLAlchemyObjectType can be registered, received "{}"'.format( | |
11 | cls.__name__) | |
12 | assert cls._meta.registry == self, 'Registry for a Model have to match.' | |
13 | # assert self.get_type_for_model(cls._meta.model) in [None, cls], ( | |
14 | # 'SQLAlchemy model "{}" already associated with ' | |
15 | # 'another type "{}".' | |
16 | # ).format(cls._meta.model, self._registry[cls._meta.model]) | |
17 | self._registry[cls._meta.model] = cls | |
18 | ||
19 | def get_type_for_model(self, model): | |
20 | return self._registry.get(model) | |
21 | ||
22 | def register_composite_converter(self, composite, converter): | |
23 | self._registry_composites[composite] = converter | |
24 | ||
25 | def get_converter_for_composite(self, composite): | |
26 | return self._registry_composites.get(composite) | |
27 | ||
28 | ||
29 | registry = None | |
30 | ||
31 | ||
32 | def get_global_registry(): | |
33 | global registry | |
34 | if not registry: | |
35 | registry = Registry() | |
36 | return registry | |
37 | ||
38 | ||
39 | def reset_global_registry(): | |
40 | global registry | |
41 | registry = None |
0 | from __future__ import absolute_import | |
1 | ||
2 | from sqlalchemy import Column, Date, ForeignKey, Integer, String, Table | |
3 | from sqlalchemy.ext.declarative import declarative_base | |
4 | from sqlalchemy.orm import relationship | |
5 | ||
6 | Base = declarative_base() | |
7 | ||
8 | association_table = Table('association', Base.metadata, | |
9 | Column('pet_id', Integer, ForeignKey('pets.id')), | |
10 | Column('reporter_id', Integer, ForeignKey('reporters.id'))) | |
11 | ||
12 | ||
13 | class Editor(Base): | |
14 | __tablename__ = 'editors' | |
15 | editor_id = Column(Integer(), primary_key=True) | |
16 | name = Column(String(100)) | |
17 | ||
18 | ||
19 | class Pet(Base): | |
20 | __tablename__ = 'pets' | |
21 | id = Column(Integer(), primary_key=True) | |
22 | name = Column(String(30)) | |
23 | reporter_id = Column(Integer(), ForeignKey('reporters.id')) | |
24 | ||
25 | ||
26 | class Reporter(Base): | |
27 | __tablename__ = 'reporters' | |
28 | id = Column(Integer(), primary_key=True) | |
29 | first_name = Column(String(30)) | |
30 | last_name = Column(String(30)) | |
31 | email = Column(String()) | |
32 | pets = relationship('Pet', secondary=association_table, backref='reporters') | |
33 | articles = relationship('Article', backref='reporter') | |
34 | favorite_article = relationship("Article", uselist=False) | |
35 | ||
36 | ||
37 | class Article(Base): | |
38 | __tablename__ = 'articles' | |
39 | id = Column(Integer(), primary_key=True) | |
40 | headline = Column(String(100)) | |
41 | pub_date = Column(Date()) | |
42 | reporter_id = Column(Integer(), ForeignKey('reporters.id')) |
0 | from py.test import raises | |
1 | from sqlalchemy import Column, Table, case, types | |
2 | from sqlalchemy.dialects import postgresql | |
3 | from sqlalchemy.ext.declarative import declarative_base | |
4 | from sqlalchemy.orm import composite | |
5 | from sqlalchemy.sql.elements import Label | |
6 | from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType | |
7 | ||
8 | import graphene | |
9 | from graphene.relay import Node | |
10 | from graphene.types.datetime import DateTime | |
11 | from graphene.types.json import JSONString | |
12 | ||
13 | from ..converter import (convert_sqlalchemy_column, | |
14 | convert_sqlalchemy_composite, | |
15 | convert_sqlalchemy_relationship) | |
16 | from ..fields import SQLAlchemyConnectionField | |
17 | from ..registry import Registry | |
18 | from ..types import SQLAlchemyObjectType | |
19 | from .models import Article, Pet, Reporter | |
20 | ||
21 | ||
22 | def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs): | |
23 | column = Column(sqlalchemy_type, doc='Custom Help Text', **kwargs) | |
24 | graphene_type = convert_sqlalchemy_column(column) | |
25 | assert isinstance(graphene_type, graphene_field) | |
26 | field = graphene_type.Field() | |
27 | assert field.description == 'Custom Help Text' | |
28 | return field | |
29 | ||
30 | ||
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) | |
35 | graphene_type = convert_sqlalchemy_composite(composite_column, registry) | |
36 | assert isinstance(graphene_type, graphene_field) | |
37 | field = graphene_type.Field() | |
38 | # SQLAlchemy currently does not persist the doc onto the column, even though | |
39 | # the documentation says it does.... | |
40 | # assert field.description == 'Custom Help Text' | |
41 | return field | |
42 | ||
43 | ||
44 | def test_should_unknown_sqlalchemy_field_raise_exception(): | |
45 | with raises(Exception) as excinfo: | |
46 | convert_sqlalchemy_column(None) | |
47 | assert 'Don\'t know how to convert the SQLAlchemy field' in str(excinfo.value) | |
48 | ||
49 | ||
50 | def test_should_date_convert_string(): | |
51 | assert_column_conversion(types.Date(), graphene.String) | |
52 | ||
53 | ||
54 | def test_should_datetime_convert_string(): | |
55 | assert_column_conversion(types.DateTime(), DateTime) | |
56 | ||
57 | ||
58 | def test_should_time_convert_string(): | |
59 | assert_column_conversion(types.Time(), graphene.String) | |
60 | ||
61 | ||
62 | def test_should_string_convert_string(): | |
63 | assert_column_conversion(types.String(), graphene.String) | |
64 | ||
65 | ||
66 | def test_should_text_convert_string(): | |
67 | assert_column_conversion(types.Text(), graphene.String) | |
68 | ||
69 | ||
70 | def test_should_unicode_convert_string(): | |
71 | assert_column_conversion(types.Unicode(), graphene.String) | |
72 | ||
73 | ||
74 | def test_should_unicodetext_convert_string(): | |
75 | assert_column_conversion(types.UnicodeText(), graphene.String) | |
76 | ||
77 | ||
78 | def test_should_enum_convert_string(): | |
79 | assert_column_conversion(types.Enum(), graphene.String) | |
80 | ||
81 | ||
82 | def test_should_small_integer_convert_int(): | |
83 | assert_column_conversion(types.SmallInteger(), graphene.Int) | |
84 | ||
85 | ||
86 | def test_should_big_integer_convert_int(): | |
87 | assert_column_conversion(types.BigInteger(), graphene.Float) | |
88 | ||
89 | ||
90 | def test_should_integer_convert_int(): | |
91 | assert_column_conversion(types.Integer(), graphene.Int) | |
92 | ||
93 | ||
94 | def test_should_integer_convert_id(): | |
95 | assert_column_conversion(types.Integer(), graphene.ID, primary_key=True) | |
96 | ||
97 | ||
98 | def test_should_boolean_convert_boolean(): | |
99 | assert_column_conversion(types.Boolean(), graphene.Boolean) | |
100 | ||
101 | ||
102 | def test_should_float_convert_float(): | |
103 | assert_column_conversion(types.Float(), graphene.Float) | |
104 | ||
105 | ||
106 | def test_should_numeric_convert_float(): | |
107 | assert_column_conversion(types.Numeric(), graphene.Float) | |
108 | ||
109 | ||
110 | def test_should_label_convert_string(): | |
111 | label = Label('label_test', case([], else_="foo"), type_=types.Unicode()) | |
112 | graphene_type = convert_sqlalchemy_column(label) | |
113 | assert isinstance(graphene_type, graphene.String) | |
114 | ||
115 | ||
116 | def test_should_label_convert_int(): | |
117 | label = Label('int_label_test', case([], else_="foo"), type_=types.Integer()) | |
118 | graphene_type = convert_sqlalchemy_column(label) | |
119 | assert isinstance(graphene_type, graphene.Int) | |
120 | ||
121 | 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') | |
127 | Base = declarative_base() | |
128 | ||
129 | Table('translatedmodel', Base.metadata, column) | |
130 | graphene_type = convert_sqlalchemy_column(column) | |
131 | 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' | |
136 | ||
137 | ||
138 | def test_should_scalar_list_convert_list(): | |
139 | assert_column_conversion(ScalarListType(), graphene.List) | |
140 | ||
141 | ||
142 | def test_should_jsontype_convert_jsonstring(): | |
143 | assert_column_conversion(JSONType(), JSONString) | |
144 | ||
145 | ||
146 | def test_should_manytomany_convert_connectionorlist(): | |
147 | registry = Registry() | |
148 | dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, registry) | |
149 | assert isinstance(dynamic_field, graphene.Dynamic) | |
150 | assert not dynamic_field.get_type() | |
151 | ||
152 | ||
153 | def test_should_manytomany_convert_connectionorlist_list(): | |
154 | class A(SQLAlchemyObjectType): | |
155 | ||
156 | class Meta: | |
157 | model = Pet | |
158 | ||
159 | dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry) | |
160 | assert isinstance(dynamic_field, graphene.Dynamic) | |
161 | graphene_type = dynamic_field.get_type() | |
162 | assert isinstance(graphene_type, graphene.Field) | |
163 | assert isinstance(graphene_type.type, graphene.List) | |
164 | assert graphene_type.type.of_type == A | |
165 | ||
166 | ||
167 | def test_should_manytomany_convert_connectionorlist_connection(): | |
168 | class A(SQLAlchemyObjectType): | |
169 | ||
170 | class Meta: | |
171 | model = Pet | |
172 | interfaces = (Node, ) | |
173 | ||
174 | dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry) | |
175 | assert isinstance(dynamic_field, graphene.Dynamic) | |
176 | assert isinstance(dynamic_field.get_type(), SQLAlchemyConnectionField) | |
177 | ||
178 | ||
179 | def test_should_manytoone_convert_connectionorlist(): | |
180 | registry = Registry() | |
181 | dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, registry) | |
182 | assert isinstance(dynamic_field, graphene.Dynamic) | |
183 | assert not dynamic_field.get_type() | |
184 | ||
185 | ||
186 | def test_should_manytoone_convert_connectionorlist_list(): | |
187 | class A(SQLAlchemyObjectType): | |
188 | ||
189 | class Meta: | |
190 | model = Reporter | |
191 | ||
192 | dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, A._meta.registry) | |
193 | assert isinstance(dynamic_field, graphene.Dynamic) | |
194 | graphene_type = dynamic_field.get_type() | |
195 | assert isinstance(graphene_type, graphene.Field) | |
196 | assert graphene_type.type == A | |
197 | ||
198 | ||
199 | def test_should_manytoone_convert_connectionorlist_connection(): | |
200 | class A(SQLAlchemyObjectType): | |
201 | ||
202 | class Meta: | |
203 | model = Reporter | |
204 | interfaces = (Node, ) | |
205 | ||
206 | dynamic_field = convert_sqlalchemy_relationship(Article.reporter.property, A._meta.registry) | |
207 | assert isinstance(dynamic_field, graphene.Dynamic) | |
208 | graphene_type = dynamic_field.get_type() | |
209 | assert isinstance(graphene_type, graphene.Field) | |
210 | assert graphene_type.type == A | |
211 | ||
212 | ||
213 | def test_should_onetoone_convert_field(): | |
214 | class A(SQLAlchemyObjectType): | |
215 | ||
216 | class Meta: | |
217 | model = Article | |
218 | interfaces = (Node, ) | |
219 | ||
220 | dynamic_field = convert_sqlalchemy_relationship(Reporter.favorite_article.property, A._meta.registry) | |
221 | assert isinstance(dynamic_field, graphene.Dynamic) | |
222 | graphene_type = dynamic_field.get_type() | |
223 | assert isinstance(graphene_type, graphene.Field) | |
224 | assert graphene_type.type == A | |
225 | ||
226 | ||
227 | def test_should_postgresql_uuid_convert(): | |
228 | assert_column_conversion(postgresql.UUID(), graphene.String) | |
229 | ||
230 | ||
231 | def test_should_postgresql_enum_convert(): | |
232 | assert_column_conversion(postgresql.ENUM(), graphene.String) | |
233 | ||
234 | ||
235 | def test_should_postgresql_array_convert(): | |
236 | assert_column_conversion(postgresql.ARRAY(types.Integer), graphene.List) | |
237 | ||
238 | ||
239 | def test_should_postgresql_json_convert(): | |
240 | assert_column_conversion(postgresql.JSON(), JSONString) | |
241 | ||
242 | ||
243 | def test_should_postgresql_jsonb_convert(): | |
244 | assert_column_conversion(postgresql.JSONB(), JSONString) | |
245 | ||
246 | ||
247 | def test_should_postgresql_hstore_convert(): | |
248 | assert_column_conversion(postgresql.HSTORE(), JSONString) | |
249 | ||
250 | ||
251 | def test_should_composite_convert(): | |
252 | ||
253 | class CompositeClass(object): | |
254 | ||
255 | def __init__(self, col1, col2): | |
256 | self.col1 = col1 | |
257 | self.col2 = col2 | |
258 | ||
259 | registry = Registry() | |
260 | ||
261 | @convert_sqlalchemy_composite.register(CompositeClass, registry) | |
262 | def convert_composite_class(composite, registry): | |
263 | return graphene.String(description=composite.doc) | |
264 | ||
265 | assert_composite_conversion(CompositeClass, | |
266 | (Column(types.Unicode(50)), | |
267 | Column(types.Unicode(50))), | |
268 | graphene.String, | |
269 | registry) | |
270 | ||
271 | ||
272 | def test_should_unknown_sqlalchemy_composite_raise_exception(): | |
273 | registry = Registry() | |
274 | ||
275 | with raises(Exception) as excinfo: | |
276 | ||
277 | class CompositeClass(object): | |
278 | ||
279 | def __init__(self, col1, col2): | |
280 | self.col1 = col1 | |
281 | self.col2 = col2 | |
282 | ||
283 | assert_composite_conversion(CompositeClass, | |
284 | (Column(types.Unicode(50)), | |
285 | Column(types.Unicode(50))), | |
286 | graphene.String, | |
287 | registry) | |
288 | ||
289 | assert 'Don\'t know how to convert the composite field' in str(excinfo.value) |
0 | import pytest | |
1 | from sqlalchemy import create_engine | |
2 | from sqlalchemy.orm import scoped_session, sessionmaker | |
3 | ||
4 | import graphene | |
5 | from graphene.relay import Node | |
6 | ||
7 | from ..registry import reset_global_registry | |
8 | from ..fields import SQLAlchemyConnectionField | |
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') | |
16 | def session(): | |
17 | reset_global_registry() | |
18 | connection = db.engine.connect() | |
19 | transaction = connection.begin() | |
20 | Base.metadata.create_all(connection) | |
21 | ||
22 | # options = dict(bind=connection, binds={}) | |
23 | session_factory = sessionmaker(bind=connection) | |
24 | session = scoped_session(session_factory) | |
25 | ||
26 | yield session | |
27 | ||
28 | # Finalize test here | |
29 | transaction.rollback() | |
30 | connection.close() | |
31 | session.remove() | |
32 | ||
33 | ||
34 | def setup_fixtures(session): | |
35 | reporter = Reporter(first_name='ABA', last_name='X') | |
36 | session.add(reporter) | |
37 | reporter2 = Reporter(first_name='ABO', last_name='Y') | |
38 | session.add(reporter2) | |
39 | article = Article(headline='Hi!') | |
40 | article.reporter = reporter | |
41 | session.add(article) | |
42 | editor = Editor(name="John") | |
43 | session.add(editor) | |
44 | session.commit() | |
45 | ||
46 | ||
47 | def test_should_query_well(session): | |
48 | setup_fixtures(session) | |
49 | ||
50 | class ReporterType(SQLAlchemyObjectType): | |
51 | ||
52 | class Meta: | |
53 | model = Reporter | |
54 | ||
55 | class Query(graphene.ObjectType): | |
56 | reporter = graphene.Field(ReporterType) | |
57 | reporters = graphene.List(ReporterType) | |
58 | ||
59 | def resolve_reporter(self, *args, **kwargs): | |
60 | return session.query(Reporter).first() | |
61 | ||
62 | def resolve_reporters(self, *args, **kwargs): | |
63 | return session.query(Reporter) | |
64 | ||
65 | query = ''' | |
66 | query ReporterQuery { | |
67 | reporter { | |
68 | firstName, | |
69 | lastName, | |
70 | ||
71 | } | |
72 | reporters { | |
73 | firstName | |
74 | } | |
75 | } | |
76 | ''' | |
77 | expected = { | |
78 | 'reporter': { | |
79 | 'firstName': 'ABA', | |
80 | 'lastName': 'X', | |
81 | 'email': None | |
82 | }, | |
83 | 'reporters': [{ | |
84 | 'firstName': 'ABA', | |
85 | }, { | |
86 | 'firstName': 'ABO', | |
87 | }] | |
88 | } | |
89 | schema = graphene.Schema(query=Query) | |
90 | result = schema.execute(query) | |
91 | assert not result.errors | |
92 | assert result.data == expected | |
93 | ||
94 | ||
95 | def test_should_node(session): | |
96 | setup_fixtures(session) | |
97 | ||
98 | class ReporterNode(SQLAlchemyObjectType): | |
99 | ||
100 | class Meta: | |
101 | model = Reporter | |
102 | interfaces = (Node, ) | |
103 | ||
104 | @classmethod | |
105 | def get_node(cls, id, info): | |
106 | return Reporter(id=2, first_name='Cookie Monster') | |
107 | ||
108 | class ArticleNode(SQLAlchemyObjectType): | |
109 | ||
110 | class Meta: | |
111 | model = Article | |
112 | interfaces = (Node, ) | |
113 | ||
114 | # @classmethod | |
115 | # def get_node(cls, id, info): | |
116 | # return Article(id=1, headline='Article node') | |
117 | ||
118 | class Query(graphene.ObjectType): | |
119 | node = Node.Field() | |
120 | reporter = graphene.Field(ReporterNode) | |
121 | article = graphene.Field(ArticleNode) | |
122 | all_articles = SQLAlchemyConnectionField(ArticleNode) | |
123 | ||
124 | def resolve_reporter(self, *args, **kwargs): | |
125 | return session.query(Reporter).first() | |
126 | ||
127 | def resolve_article(self, *args, **kwargs): | |
128 | return session.query(Article).first() | |
129 | ||
130 | query = ''' | |
131 | query ReporterQuery { | |
132 | reporter { | |
133 | id, | |
134 | firstName, | |
135 | articles { | |
136 | edges { | |
137 | node { | |
138 | headline | |
139 | } | |
140 | } | |
141 | } | |
142 | lastName, | |
143 | ||
144 | } | |
145 | allArticles { | |
146 | edges { | |
147 | node { | |
148 | headline | |
149 | } | |
150 | } | |
151 | } | |
152 | myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { | |
153 | id | |
154 | ... on ReporterNode { | |
155 | firstName | |
156 | } | |
157 | ... on ArticleNode { | |
158 | headline | |
159 | } | |
160 | } | |
161 | } | |
162 | ''' | |
163 | 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 | }, | |
176 | }, | |
177 | 'allArticles': { | |
178 | 'edges': [{ | |
179 | 'node': { | |
180 | 'headline': 'Hi!' | |
181 | } | |
182 | }] | |
183 | }, | |
184 | 'myArticle': { | |
185 | 'id': 'QXJ0aWNsZU5vZGU6MQ==', | |
186 | 'headline': 'Hi!' | |
187 | } | |
188 | } | |
189 | schema = graphene.Schema(query=Query) | |
190 | result = schema.execute(query, context_value={'session': session}) | |
191 | assert not result.errors | |
192 | assert result.data == expected | |
193 | ||
194 | ||
195 | def test_should_custom_identifier(session): | |
196 | setup_fixtures(session) | |
197 | ||
198 | class EditorNode(SQLAlchemyObjectType): | |
199 | ||
200 | class Meta: | |
201 | model = Editor | |
202 | interfaces = (Node, ) | |
203 | ||
204 | class Query(graphene.ObjectType): | |
205 | node = Node.Field() | |
206 | all_editors = SQLAlchemyConnectionField(EditorNode) | |
207 | ||
208 | query = ''' | |
209 | query EditorQuery { | |
210 | allEditors { | |
211 | edges { | |
212 | node { | |
213 | id, | |
214 | name | |
215 | } | |
216 | } | |
217 | }, | |
218 | node(id: "RWRpdG9yTm9kZTox") { | |
219 | ...on EditorNode { | |
220 | name | |
221 | } | |
222 | } | |
223 | } | |
224 | ''' | |
225 | expected = { | |
226 | 'allEditors': { | |
227 | 'edges': [{ | |
228 | 'node': { | |
229 | 'id': 'RWRpdG9yTm9kZTox', | |
230 | 'name': 'John' | |
231 | } | |
232 | }] | |
233 | }, | |
234 | 'node': { | |
235 | 'name': 'John' | |
236 | } | |
237 | } | |
238 | ||
239 | schema = graphene.Schema(query=Query) | |
240 | result = schema.execute(query, context_value={'session': session}) | |
241 | assert not result.errors | |
242 | assert result.data == expected | |
243 | ||
244 | ||
245 | def test_should_mutate_well(session): | |
246 | setup_fixtures(session) | |
247 | ||
248 | class EditorNode(SQLAlchemyObjectType): | |
249 | ||
250 | class Meta: | |
251 | model = Editor | |
252 | interfaces = (Node, ) | |
253 | ||
254 | class ReporterNode(SQLAlchemyObjectType): | |
255 | ||
256 | class Meta: | |
257 | model = Reporter | |
258 | interfaces = (Node, ) | |
259 | ||
260 | @classmethod | |
261 | def get_node(cls, id, info): | |
262 | return Reporter(id=2, first_name='Cookie Monster') | |
263 | ||
264 | class ArticleNode(SQLAlchemyObjectType): | |
265 | ||
266 | class Meta: | |
267 | model = Article | |
268 | interfaces = (Node, ) | |
269 | ||
270 | class CreateArticle(graphene.Mutation): | |
271 | ||
272 | class Input: | |
273 | headline = graphene.String() | |
274 | reporter_id = graphene.ID() | |
275 | ||
276 | ok = graphene.Boolean() | |
277 | article = graphene.Field(ArticleNode) | |
278 | ||
279 | @classmethod | |
280 | def mutate(cls, instance, args, context, info): | |
281 | new_article = Article( | |
282 | headline=args.get('headline'), | |
283 | reporter_id=args.get('reporter_id'), | |
284 | ) | |
285 | ||
286 | session.add(new_article) | |
287 | session.commit() | |
288 | ok = True | |
289 | ||
290 | return CreateArticle(article=new_article, ok=ok) | |
291 | ||
292 | class Query(graphene.ObjectType): | |
293 | node = Node.Field() | |
294 | ||
295 | class Mutation(graphene.ObjectType): | |
296 | create_article = CreateArticle.Field() | |
297 | ||
298 | query = ''' | |
299 | mutation ArticleCreator { | |
300 | createArticle( | |
301 | headline: "My Article" | |
302 | reporterId: "1" | |
303 | ) { | |
304 | ok | |
305 | article { | |
306 | headline | |
307 | reporter { | |
308 | id | |
309 | firstName | |
310 | } | |
311 | } | |
312 | } | |
313 | } | |
314 | ''' | |
315 | expected = { | |
316 | 'createArticle': { | |
317 | 'ok': True, | |
318 | 'article': { | |
319 | 'headline': 'My Article', | |
320 | 'reporter': { | |
321 | 'id': 'UmVwb3J0ZXJOb2RlOjE=', | |
322 | 'firstName': 'ABA' | |
323 | } | |
324 | } | |
325 | }, | |
326 | } | |
327 | ||
328 | schema = graphene.Schema(query=Query, mutation=Mutation) | |
329 | result = schema.execute(query, context_value={'session': session}) | |
330 | assert not result.errors | |
331 | assert result.data == expected |
0 | from py.test import raises | |
1 | ||
2 | from ..registry import Registry | |
3 | from ..types import SQLAlchemyObjectType | |
4 | from .models import Reporter | |
5 | ||
6 | ||
7 | def test_should_raise_if_no_model(): | |
8 | with raises(Exception) as excinfo: | |
9 | class Character1(SQLAlchemyObjectType): | |
10 | pass | |
11 | assert 'valid SQLAlchemy Model' in str(excinfo.value) | |
12 | ||
13 | ||
14 | def test_should_raise_if_model_is_invalid(): | |
15 | with raises(Exception) as excinfo: | |
16 | class Character2(SQLAlchemyObjectType): | |
17 | ||
18 | class Meta: | |
19 | model = 1 | |
20 | assert 'valid SQLAlchemy Model' in str(excinfo.value) | |
21 | ||
22 | ||
23 | def test_should_map_fields_correctly(): | |
24 | class ReporterType2(SQLAlchemyObjectType): | |
25 | ||
26 | class Meta: | |
27 | model = Reporter | |
28 | registry = Registry() | |
29 | ||
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'] | |
39 | ||
40 | ||
41 | def test_should_map_only_few_fields(): | |
42 | class Reporter2(SQLAlchemyObjectType): | |
43 | ||
44 | class Meta: | |
45 | model = Reporter | |
46 | only_fields = ('id', 'email') | |
47 | 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 | ||
4 | from ..registry import Registry | |
5 | from ..types import SQLAlchemyObjectType | |
6 | from .models import Article, Reporter | |
7 | ||
8 | registry = Registry() | |
9 | ||
10 | ||
11 | class Character(SQLAlchemyObjectType): | |
12 | '''Character description''' | |
13 | class Meta: | |
14 | model = Reporter | |
15 | registry = registry | |
16 | ||
17 | ||
18 | class Human(SQLAlchemyObjectType): | |
19 | '''Human description''' | |
20 | ||
21 | pub_date = Int() | |
22 | ||
23 | class Meta: | |
24 | model = Article | |
25 | exclude_fields = ('id', ) | |
26 | registry = registry | |
27 | interfaces = (Node, ) | |
28 | ||
29 | ||
30 | def test_sqlalchemy_interface(): | |
31 | assert issubclass(Node, Interface) | |
32 | assert issubclass(Node, Node) | |
33 | ||
34 | ||
35 | # @patch('graphene.contrib.sqlalchemy.tests.models.Article.filter', return_value=Article(id=1)) | |
36 | # def test_sqlalchemy_get_node(get): | |
37 | # human = Human.get_node(1, None) | |
38 | # get.assert_called_with(id=1) | |
39 | # assert human.id == 1 | |
40 | ||
41 | ||
42 | def test_objecttype_registered(): | |
43 | assert issubclass(Character, ObjectType) | |
44 | assert Character._meta.model == Reporter | |
45 | assert list( | |
46 | Character._meta.fields.keys()) == [ | |
47 | 'id', | |
48 | 'first_name', | |
49 | 'last_name', | |
50 | 'email', | |
51 | 'pets', | |
52 | 'articles', | |
53 | 'favorite_article'] | |
54 | ||
55 | ||
56 | # def test_sqlalchemynode_idfield(): | |
57 | # idfield = Node._meta.fields_map['id'] | |
58 | # assert isinstance(idfield, GlobalIDField) | |
59 | ||
60 | ||
61 | # def test_node_idfield(): | |
62 | # idfield = Human._meta.fields_map['id'] | |
63 | # assert isinstance(idfield, GlobalIDField) | |
64 | ||
65 | ||
66 | def test_node_replacedfield(): | |
67 | idfield = Human._meta.fields['pub_date'] | |
68 | assert isinstance(idfield, Field) | |
69 | assert idfield.type == Int | |
70 | ||
71 | ||
72 | def test_object_type(): | |
73 | assert issubclass(Human, ObjectType) | |
74 | assert list(Human._meta.fields.keys()) == ['id', 'headline', 'reporter_id', 'reporter', 'pub_date'] | |
75 | assert is_node(Human) |
0 | from graphene import ObjectType, Schema, String | |
1 | ||
2 | from ..utils import get_session | |
3 | ||
4 | ||
5 | def test_get_session(): | |
6 | session = 'My SQLAlchemy session' | |
7 | ||
8 | class Query(ObjectType): | |
9 | x = String() | |
10 | ||
11 | def resolve_x(self, args, context, info): | |
12 | return get_session(context) | |
13 | ||
14 | query = ''' | |
15 | query ReporterQuery { | |
16 | x | |
17 | } | |
18 | ''' | |
19 | ||
20 | schema = Schema(query=Query) | |
21 | result = schema.execute(query, context_value={'session': session}) | |
22 | assert not result.errors | |
23 | assert result.data['x'] == session |
0 | from collections import OrderedDict | |
1 | ||
2 | import six | |
3 | from sqlalchemy.inspection import inspect as sqlalchemyinspect | |
4 | from sqlalchemy.orm.exc import NoResultFound | |
5 | ||
6 | from graphene import Field, ObjectType | |
7 | from graphene.relay import is_node | |
8 | from graphene.types.objecttype import ObjectTypeMeta | |
9 | from graphene.types.options import Options | |
10 | from graphene.types.utils import merge, yank_fields_from_attrs | |
11 | from graphene.utils.is_base_type import is_base_type | |
12 | ||
13 | from .converter import (convert_sqlalchemy_column, | |
14 | convert_sqlalchemy_composite, | |
15 | convert_sqlalchemy_relationship) | |
16 | from .registry import Registry, get_global_registry | |
17 | from .utils import get_query, is_mapped | |
18 | ||
19 | ||
20 | def construct_fields(options): | |
21 | only_fields = options.only_fields | |
22 | exclude_fields = options.exclude_fields | |
23 | inspected_model = sqlalchemyinspect(options.model) | |
24 | ||
25 | fields = OrderedDict() | |
26 | ||
27 | for name, column in inspected_model.columns.items(): | |
28 | is_not_in_only = only_fields and name not in only_fields | |
29 | is_already_created = name in options.fields | |
30 | is_excluded = name in exclude_fields or is_already_created | |
31 | if is_not_in_only or is_excluded: | |
32 | # We skip this field if we specify only_fields and is not | |
33 | # in there. Or when we excldue this field in exclude_fields | |
34 | continue | |
35 | converted_column = convert_sqlalchemy_column(column, options.registry) | |
36 | fields[name] = converted_column | |
37 | ||
38 | for name, composite in inspected_model.composites.items(): | |
39 | is_not_in_only = only_fields and name not in only_fields | |
40 | is_already_created = name in options.fields | |
41 | is_excluded = name in exclude_fields or is_already_created | |
42 | if is_not_in_only or is_excluded: | |
43 | # We skip this field if we specify only_fields and is not | |
44 | # in there. Or when we excldue this field in exclude_fields | |
45 | continue | |
46 | converted_composite = convert_sqlalchemy_composite(composite, options.registry) | |
47 | fields[name] = converted_composite | |
48 | ||
49 | # Get all the columns for the relationships on the model | |
50 | for relationship in inspected_model.relationships: | |
51 | is_not_in_only = only_fields and relationship.key not in only_fields | |
52 | is_already_created = relationship.key in options.fields | |
53 | is_excluded = relationship.key in exclude_fields or is_already_created | |
54 | if is_not_in_only or is_excluded: | |
55 | # We skip this field if we specify only_fields and is not | |
56 | # in there. Or when we excldue this field in exclude_fields | |
57 | continue | |
58 | converted_relationship = convert_sqlalchemy_relationship(relationship, options.registry) | |
59 | name = relationship.key | |
60 | fields[name] = converted_relationship | |
61 | ||
62 | return fields | |
63 | ||
64 | ||
65 | class SQLAlchemyObjectTypeMeta(ObjectTypeMeta): | |
66 | ||
67 | @staticmethod | |
68 | def __new__(cls, name, bases, attrs): | |
69 | # Also ensure initialization is only performed for subclasses of Model | |
70 | # (excluding Model class itself). | |
71 | if not is_base_type(bases, SQLAlchemyObjectTypeMeta): | |
72 | return type.__new__(cls, name, bases, attrs) | |
73 | ||
74 | options = Options( | |
75 | attrs.pop('Meta', None), | |
76 | name=name, | |
77 | description=attrs.pop('__doc__', None), | |
78 | model=None, | |
79 | local_fields=None, | |
80 | only_fields=(), | |
81 | exclude_fields=(), | |
82 | id='id', | |
83 | interfaces=(), | |
84 | registry=None | |
85 | ) | |
86 | ||
87 | if not options.registry: | |
88 | options.registry = get_global_registry() | |
89 | assert isinstance(options.registry, Registry), ( | |
90 | 'The attribute registry in {}.Meta needs to be an' | |
91 | ' instance of Registry, received "{}".' | |
92 | ).format(name, options.registry) | |
93 | assert is_mapped(options.model), ( | |
94 | 'You need to pass a valid SQLAlchemy Model in ' | |
95 | '{}.Meta, received "{}".' | |
96 | ).format(name, options.model) | |
97 | ||
98 | cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) | |
99 | ||
100 | options.registry.register(cls) | |
101 | ||
102 | options.sqlalchemy_fields = yank_fields_from_attrs( | |
103 | construct_fields(options), | |
104 | _as=Field, | |
105 | ) | |
106 | options.fields = merge( | |
107 | options.interface_fields, | |
108 | options.sqlalchemy_fields, | |
109 | options.base_fields, | |
110 | options.local_fields | |
111 | ) | |
112 | ||
113 | return cls | |
114 | ||
115 | ||
116 | class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)): | |
117 | ||
118 | @classmethod | |
119 | def is_type_of(cls, root, context, info): | |
120 | if isinstance(root, cls): | |
121 | return True | |
122 | if not is_mapped(type(root)): | |
123 | raise Exception(( | |
124 | 'Received incompatible instance "{}".' | |
125 | ).format(root)) | |
126 | return isinstance(root, cls._meta.model) | |
127 | ||
128 | @classmethod | |
129 | def get_query(cls, context): | |
130 | model = cls._meta.model | |
131 | return get_query(model, context) | |
132 | ||
133 | @classmethod | |
134 | def get_node(cls, id, context, info): | |
135 | try: | |
136 | return cls.get_query(context).get(id) | |
137 | except NoResultFound: | |
138 | return None | |
139 | ||
140 | def resolve_id(self, args, context, info): | |
141 | graphene_type = info.parent_type.graphene_type | |
142 | if is_node(graphene_type): | |
143 | return self.__mapper__.primary_key_from_instance(self)[0] | |
144 | return getattr(self, graphene_type._meta.id, None) |
0 | from sqlalchemy.ext.declarative.api import DeclarativeMeta | |
1 | ||
2 | ||
3 | def get_session(context): | |
4 | return context.get('session') | |
5 | ||
6 | ||
7 | def get_query(model, context): | |
8 | query = getattr(model, 'query', None) | |
9 | if not query: | |
10 | session = get_session(context) | |
11 | if not session: | |
12 | raise Exception('A query in the model Base or a session in the schema is required for querying.\n' | |
13 | 'Read more http://graphene-python.org/docs/sqlalchemy/tips/#querying') | |
14 | query = session.query(model) | |
15 | return query | |
16 | ||
17 | ||
18 | def is_mapped(obj): | |
19 | return isinstance(obj, DeclarativeMeta) |
0 | [flake8] | |
1 | exclude = setup.py,docs/*,examples/*,tests | |
2 | max-line-length = 120 | |
3 | ||
4 | [coverage:run] | |
5 | omit = */tests/* | |
6 | ||
7 | [isort] | |
8 | known_first_party=graphene,graphene_sqlalchemy |
0 | from setuptools import find_packages, setup | |
1 | ||
2 | setup( | |
3 | name='graphene-sqlalchemy', | |
4 | version='1.1.1', | |
5 | ||
6 | description='Graphene SQLAlchemy integration', | |
7 | long_description=open('README.rst').read(), | |
8 | ||
9 | url='https://github.com/graphql-python/graphene-sqlalchemy', | |
10 | ||
11 | author='Syrus Akbary', | |
12 | author_email='[email protected]', | |
13 | ||
14 | license='MIT', | |
15 | ||
16 | classifiers=[ | |
17 | 'Development Status :: 3 - Alpha', | |
18 | 'Intended Audience :: Developers', | |
19 | 'Topic :: Software Development :: Libraries', | |
20 | 'Programming Language :: Python :: 2', | |
21 | 'Programming Language :: Python :: 2.7', | |
22 | 'Programming Language :: Python :: 3', | |
23 | 'Programming Language :: Python :: 3.3', | |
24 | 'Programming Language :: Python :: 3.4', | |
25 | 'Programming Language :: Python :: 3.5', | |
26 | 'Programming Language :: Python :: Implementation :: PyPy', | |
27 | ], | |
28 | ||
29 | keywords='api graphql protocol rest relay graphene', | |
30 | ||
31 | packages=find_packages(exclude=['tests']), | |
32 | ||
33 | install_requires=[ | |
34 | 'six>=1.10.0', | |
35 | 'graphene>=1.0', | |
36 | 'SQLAlchemy', | |
37 | 'singledispatch>=3.4.0.3', | |
38 | 'iso8601', | |
39 | ], | |
40 | tests_require=[ | |
41 | 'pytest>=2.7.2', | |
42 | 'mock', | |
43 | 'sqlalchemy_utils', | |
44 | ], | |
45 | ) |