├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.rst
├── docs
├── Makefile
├── api.rst
├── conf.py
├── data_layer.rst
├── errors.rst
├── filtering.rst
├── flask-rest-jsonapi.rst
├── img
│ └── schema.png
├── include_related_objects.rst
├── index.rst
├── installation.rst
├── logical_data_abstraction.rst
├── modules.rst
├── oauth.rst
├── pagination.rst
├── permission.rst
├── quickstart.rst
├── resource_manager.rst
├── routing.rst
├── sorting.rst
└── sparse_fieldsets.rst
├── examples
└── api.py
├── flask_rest_jsonapi
├── __init__.py
├── api.py
├── constants.py
├── data_layers
│ ├── __init__.py
│ ├── alchemy.py
│ ├── base.py
│ └── filtering
│ │ ├── __init__.py
│ │ └── alchemy.py
├── decorators.py
├── errors.py
├── exceptions.py
├── pagination.py
├── querystring.py
├── resource.py
└── schema.py
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── conftest.py
└── test_sqlalchemy_data_layer.py
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ fossasia ]
6 | pull_request:
7 | branches: [ fossasia ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up Python 3.7
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: 3.7
20 | - name: Install dependencies
21 | run: |
22 | pip install coveralls coverage pytest
23 | python setup.py install
24 | - name: Test with pytest
25 | run: |
26 | coverage run --source flask_rest_jsonapi -m pytest -v
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | # Pycharm IDE files
92 | .idea/
93 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '3.7'
4 | install:
5 | - pip install -r requirements.txt
6 | - pip install coveralls coverage
7 | - pip install pytest --upgrade
8 | script:
9 | - python setup.py install
10 | - coverage run --source flask_rest_jsonapi -m pytest -v
11 | after_success:
12 | - coveralls
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 python-jsonapi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://badge.fury.io/py/Flask-REST-JSONAPI.svg
2 | :target: https://badge.fury.io/py/Flask-REST-JSONAPI
3 | .. image:: https://travis-ci.org/miLibris/flask-rest-jsonapi.svg
4 | :target: https://travis-ci.org/miLibris/flask-rest-jsonapi
5 | .. image:: https://coveralls.io/repos/github/miLibris/flask-rest-jsonapi/badge.svg
6 | :target: https://coveralls.io/github/miLibris/flask-rest-jsonapi
7 | .. image:: https://readthedocs.org/projects/flask-rest-jsonapi/badge/?version=latest
8 | :target: http://flask-rest-jsonapi.readthedocs.io/en/latest/?badge=latest
9 | :alt: Documentation Status
10 |
11 | Flask-REST-JSONAPI
12 | ##################
13 |
14 | Flask-REST-JSONAPI is a flask extension for building REST APIs. It combines the power of `Flask-Restless `_ and the flexibility of `Flask-RESTful `_ around a strong specification `JSONAPI 1.0 `_. This framework is designed to quickly build REST APIs and fit the complexity of real life projects with legacy data and multiple data storages.
15 |
16 | Install
17 | =======
18 |
19 | pip install Flask-REST-JSONAPI
20 |
21 | A minimal API
22 | =============
23 |
24 | .. code-block:: python
25 |
26 | # -*- coding: utf-8 -*-
27 |
28 | from flask import Flask
29 | from flask_rest_jsonapi import Api, ResourceDetail, ResourceList
30 | from flask_sqlalchemy import SQLAlchemy
31 | from marshmallow_jsonapi.flask import Schema
32 | from marshmallow_jsonapi import fields
33 |
34 | # Create the Flask application and the Flask-SQLAlchemy object.
35 | app = Flask(__name__)
36 | app.config['DEBUG'] = True
37 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
38 | db = SQLAlchemy(app)
39 |
40 | # Create model
41 | class Person(db.Model):
42 | id = db.Column(db.Integer, primary_key=True)
43 | name = db.Column(db.String)
44 |
45 | # Create the database.
46 | db.create_all()
47 |
48 | # Create schema
49 | class PersonSchema(Schema):
50 | class Meta:
51 | type_ = 'person'
52 | self_view = 'person_detail'
53 | self_view_kwargs = {'id': ''}
54 | self_view_many = 'person_list'
55 |
56 | id = fields.Str(dump_only=True)
57 | name = fields.Str()
58 |
59 | # Create resource managers
60 | class PersonList(ResourceList):
61 | schema = PersonSchema
62 | data_layer = {'session': db.session,
63 | 'model': Person}
64 |
65 | class PersonDetail(ResourceDetail):
66 | schema = PersonSchema
67 | data_layer = {'session': db.session,
68 | 'model': Person}
69 |
70 | # Create the API object
71 | api = Api(app)
72 | api.route(PersonList, 'person_list', '/persons')
73 | api.route(PersonDetail, 'person_detail', '/persons/')
74 |
75 | # Start the flask loop
76 | if __name__ == '__main__':
77 | app.run()
78 |
79 | This example provides the following API structure:
80 |
81 | ======================== ====== ============= ===========================
82 | URL method endpoint Usage
83 | ======================== ====== ============= ===========================
84 | /persons GET person_list Get a collection of persons
85 | /persons POST person_list Create a person
86 | /persons/ GET person_detail Get person details
87 | /persons/ PATCH person_detail Update a person
88 | /persons/ DELETE person_detail Delete a person
89 | ======================== ====== ============= ===========================
90 |
91 | Flask-REST-JSONAPI vs `Flask-RESTful `_
92 | ==========================================================================================
93 |
94 | * In contrast to Flask-RESTful, Flask-REST-JSONAPI provides a default implementation of get, post, patch and delete methods around a strong specification JSONAPI 1.0. Thanks to this you can build REST API very quickly.
95 | * Flask-REST-JSONAPI is as flexible as Flask-RESTful. You can rewrite every default method implementation to make custom work like distributing object creation.
96 |
97 | Flask-REST-JSONAPI vs `Flask-Restless `_
98 | ==========================================================================================
99 |
100 | * Flask-REST-JSONAPI is a real implementation of JSONAPI 1.0 specification. So in contrast to Flask-Restless, Flask-REST-JSONAPI forces you to create a real logical abstration over your data models with `Marshmallow `_. So you can create complex resource over your data.
101 | * In contrast to Flask-Restless, Flask-REST-JSONAPI can use any ORM or data storage through the data layer concept, not only `SQLAlchemy `_. A data layer is a CRUD interface between your resource and one or more data storage so you can fetch data from any data storage of your choice or create resource that use multiple data storages.
102 | * Like I said previously, Flask-REST-JSONAPI is a real implementation of JSONAPI 1.0 specification. So in contrast to Flask-Restless you can manage relationships via REST. You can create dedicated URL to create a CRUD API to manage relationships.
103 | * Plus Flask-REST-JSONAPI helps you to design your application with strong separation between resource definition (schemas), resource management (resource class) and route definition to get a great organization of your source code.
104 | * In contrast to Flask-Restless, Flask-REST-JSONAPI is highly customizable. For example you can entirely customize your URLs, define multiple URLs for the same resource manager, control serialization parameters of each method and lots of very useful parameters.
105 | * Finally in contrast to Flask-Restless, Flask-REST-JSONAPI provides a great error handling system according to JSONAPI 1.0. Plus the exception handling system really helps the API developer to quickly find missing resources requirements.
106 |
107 | Documentation
108 | =============
109 |
110 | Documentation available here: http://flask-rest-jsonapi.readthedocs.io/en/latest/
111 |
112 | Thanks
113 | ======
114 |
115 | Flask, marshmallow, marshmallow_jsonapi, sqlalchemy, Flask-RESTful and Flask-Restless are awesome projects. These libraries gave me inspiration to create Flask-REST-JSONAPI, so huge thanks to authors and contributors.
116 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help
18 | help:
19 | @echo "Please use \`make ' where is one of"
20 | @echo " html to make standalone HTML files"
21 | @echo " dirhtml to make HTML files named index.html in directories"
22 | @echo " singlehtml to make a single large HTML file"
23 | @echo " pickle to make pickle files"
24 | @echo " json to make JSON files"
25 | @echo " htmlhelp to make HTML files and a HTML help project"
26 | @echo " qthelp to make HTML files and a qthelp project"
27 | @echo " applehelp to make an Apple Help Book"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " epub3 to make an epub3"
31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
32 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
34 | @echo " text to make text files"
35 | @echo " man to make manual pages"
36 | @echo " texinfo to make Texinfo files"
37 | @echo " info to make Texinfo files and run them through makeinfo"
38 | @echo " gettext to make PO message catalogs"
39 | @echo " changes to make an overview of all changed/added/deprecated items"
40 | @echo " xml to make Docutils-native XML files"
41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
42 | @echo " linkcheck to check all external links for integrity"
43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
44 | @echo " coverage to run coverage check of the documentation (if enabled)"
45 | @echo " dummy to check syntax errors of document sources"
46 |
47 | .PHONY: clean
48 | clean:
49 | rm -rf $(BUILDDIR)/*
50 |
51 | .PHONY: html
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | .PHONY: dirhtml
58 | dirhtml:
59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
60 | @echo
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
62 |
63 | .PHONY: singlehtml
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | .PHONY: pickle
70 | pickle:
71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
72 | @echo
73 | @echo "Build finished; now you can process the pickle files."
74 |
75 | .PHONY: json
76 | json:
77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
78 | @echo
79 | @echo "Build finished; now you can process the JSON files."
80 |
81 | .PHONY: htmlhelp
82 | htmlhelp:
83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
84 | @echo
85 | @echo "Build finished; now you can run HTML Help Workshop with the" \
86 | ".hhp project file in $(BUILDDIR)/htmlhelp."
87 |
88 | .PHONY: qthelp
89 | qthelp:
90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
91 | @echo
92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jsonapi-utils.qhcp"
95 | @echo "To view the help file:"
96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jsonapi-utils.qhc"
97 |
98 | .PHONY: applehelp
99 | applehelp:
100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
101 | @echo
102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
103 | @echo "N.B. You won't be able to view it unless you put it in" \
104 | "~/Library/Documentation/Help or install it in your application" \
105 | "bundle."
106 |
107 | .PHONY: devhelp
108 | devhelp:
109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
110 | @echo
111 | @echo "Build finished."
112 | @echo "To view the help file:"
113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/jsonapi-utils"
114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jsonapi-utils"
115 | @echo "# devhelp"
116 |
117 | .PHONY: epub
118 | epub:
119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
120 | @echo
121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
122 |
123 | .PHONY: epub3
124 | epub3:
125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
126 | @echo
127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
128 |
129 | .PHONY: latex
130 | latex:
131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
132 | @echo
133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
135 | "(use \`make latexpdf' here to do that automatically)."
136 |
137 | .PHONY: latexpdf
138 | latexpdf:
139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
140 | @echo "Running LaTeX files through pdflatex..."
141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
143 |
144 | .PHONY: latexpdfja
145 | latexpdfja:
146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
147 | @echo "Running LaTeX files through platex and dvipdfmx..."
148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
150 |
151 | .PHONY: text
152 | text:
153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
154 | @echo
155 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
156 |
157 | .PHONY: man
158 | man:
159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
160 | @echo
161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
162 |
163 | .PHONY: texinfo
164 | texinfo:
165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
166 | @echo
167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
168 | @echo "Run \`make' in that directory to run these through makeinfo" \
169 | "(use \`make info' here to do that automatically)."
170 |
171 | .PHONY: info
172 | info:
173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
174 | @echo "Running Texinfo files through makeinfo..."
175 | make -C $(BUILDDIR)/texinfo info
176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
177 |
178 | .PHONY: gettext
179 | gettext:
180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
181 | @echo
182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
183 |
184 | .PHONY: changes
185 | changes:
186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
187 | @echo
188 | @echo "The overview file is in $(BUILDDIR)/changes."
189 |
190 | .PHONY: linkcheck
191 | linkcheck:
192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
193 | @echo
194 | @echo "Link check complete; look for any errors in the above output " \
195 | "or in $(BUILDDIR)/linkcheck/output.txt."
196 |
197 | .PHONY: doctest
198 | doctest:
199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
200 | @echo "Testing of doctests in the sources finished, look at the " \
201 | "results in $(BUILDDIR)/doctest/output.txt."
202 |
203 | .PHONY: coverage
204 | coverage:
205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
206 | @echo "Testing of coverage in the sources finished, look at the " \
207 | "results in $(BUILDDIR)/coverage/python.txt."
208 |
209 | .PHONY: xml
210 | xml:
211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
212 | @echo
213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
214 |
215 | .PHONY: pseudoxml
216 | pseudoxml:
217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
218 | @echo
219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
220 |
221 | .PHONY: dummy
222 | dummy:
223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
224 | @echo
225 | @echo "Build finished. Dummy builder generates no files."
226 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | .. _api:
2 |
3 | Api
4 | ===
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | You can provide global decorators as tuple to the Api.
9 |
10 | Example:
11 |
12 | .. code-block:: python
13 |
14 | from flask_rest_jsonapi import Api
15 | from your_project.security import login_required
16 |
17 | api = Api(decorators=(login_required,))
18 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # flask-rest-jsonapi documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Oct 21 14:33:15 2016.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | import os
21 | import sys
22 | sys.path.insert(0, os.path.abspath('..'))
23 |
24 | # -- General configuration ------------------------------------------------
25 |
26 | # If your documentation needs a minimal Sphinx version, state it here.
27 | #
28 | # needs_sphinx = '1.0'
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = [
34 | 'sphinx.ext.autodoc',
35 | ]
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = ['_templates']
39 |
40 | # The suffix(es) of source filenames.
41 | # You can specify multiple suffix as a list of string:
42 | #
43 | # source_suffix = ['.rst', '.md']
44 | source_suffix = '.rst'
45 |
46 | # The encoding of source files.
47 | #
48 | # source_encoding = 'utf-8-sig'
49 |
50 | # The master toctree document.
51 | master_doc = 'index'
52 |
53 | # General information about the project.
54 | project = 'flask-rest-jsonapi'
55 | copyright = '2016, miLibris'
56 | author = 'miLibris'
57 |
58 | # The version info for the project you're documenting, acts as replacement for
59 | # |version| and |release|, also used in various other places throughout the
60 | # built documents.
61 | #
62 | # The short X.Y version.
63 | version = '0.1'
64 | # The full version, including alpha/beta/rc tags.
65 | release = '0.1'
66 |
67 | # The language for content autogenerated by Sphinx. Refer to documentation
68 | # for a list of supported languages.
69 | #
70 | # This is also used if you do content translation via gettext catalogs.
71 | # Usually you set "language" from the command line for these cases.
72 | language = None
73 |
74 | # There are two options for replacing |today|: either, you set today to some
75 | # non-false value, then it is used:
76 | #
77 | # today = ''
78 | #
79 | # Else, today_fmt is used as the format for a strftime call.
80 | #
81 | # today_fmt = '%B %d, %Y'
82 |
83 | # List of patterns, relative to source directory, that match files and
84 | # directories to ignore when looking for source files.
85 | # This patterns also effect to html_static_path and html_extra_path
86 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
87 |
88 | # The reST default role (used for this markup: `text`) to use for all
89 | # documents.
90 | #
91 | # default_role = None
92 |
93 | # If true, '()' will be appended to :func: etc. cross-reference text.
94 | #
95 | # add_function_parentheses = True
96 |
97 | # If true, the current module name will be prepended to all description
98 | # unit titles (such as .. function::).
99 | #
100 | # add_module_names = True
101 |
102 | # If true, sectionauthor and moduleauthor directives will be shown in the
103 | # output. They are ignored by default.
104 | #
105 | # show_authors = False
106 |
107 | # The name of the Pygments (syntax highlighting) style to use.
108 | pygments_style = 'sphinx'
109 |
110 | # A list of ignored prefixes for module index sorting.
111 | # modindex_common_prefix = []
112 |
113 | # If true, keep warnings as "system message" paragraphs in the built documents.
114 | # keep_warnings = False
115 |
116 | # If true, `todo` and `todoList` produce output, else they produce nothing.
117 | todo_include_todos = False
118 |
119 |
120 | # -- Options for HTML output ----------------------------------------------
121 |
122 | # The theme to use for HTML and HTML Help pages. See the documentation for
123 | # a list of builtin themes.
124 | #
125 | html_theme = 'alabaster'
126 |
127 | # Theme options are theme-specific and customize the look and feel of a theme
128 | # further. For a list of options available for each theme, see the
129 | # documentation.
130 | #
131 | html_theme_options = {
132 | 'github_user': 'miLibris',
133 | 'github_repo': 'flask-rest-jsonapi',
134 | 'github_banner': True,
135 | 'travis_button': True,
136 | 'show_related': True,
137 | 'page_width': '1080px',
138 | 'fixed_sidebar': True,
139 | 'code_font_size': '0.8em'
140 | }
141 |
142 | # Add any paths that contain custom themes here, relative to this directory.
143 | # html_theme_path = []
144 |
145 | # The name for this set of Sphinx documents.
146 | # " v documentation" by default.
147 | #
148 | # html_title = 'jsonapi-utils v0.1'
149 |
150 | # A shorter title for the navigation bar. Default is the same as html_title.
151 | #
152 | # html_short_title = None
153 |
154 | # The name of an image file (relative to this directory) to place at the top
155 | # of the sidebar.
156 | #
157 | # html_logo = None
158 |
159 | # The name of an image file (relative to this directory) to use as a favicon of
160 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
161 | # pixels large.
162 | #
163 | # html_favicon = None
164 |
165 | # Add any paths that contain custom static files (such as style sheets) here,
166 | # relative to this directory. They are copied after the builtin static files,
167 | # so a file named "default.css" will overwrite the builtin "default.css".
168 | html_static_path = ['_static']
169 |
170 | # Add any extra paths that contain custom files (such as robots.txt or
171 | # .htaccess) here, relative to this directory. These files are copied
172 | # directly to the root of the documentation.
173 | #
174 | # html_extra_path = []
175 |
176 | # If not None, a 'Last updated on:' timestamp is inserted at every page
177 | # bottom, using the given strftime format.
178 | # The empty string is equivalent to '%b %d, %Y'.
179 | #
180 | # html_last_updated_fmt = None
181 |
182 | # If true, SmartyPants will be used to convert quotes and dashes to
183 | # typographically correct entities.
184 | #
185 | # html_use_smartypants = True
186 |
187 | # Custom sidebar templates, maps document names to template names.
188 | #
189 | # html_sidebars = {}
190 |
191 | # Additional templates that should be rendered to pages, maps page names to
192 | # template names.
193 | #
194 | # html_additional_pages = {}
195 |
196 | # If false, no module index is generated.
197 | #
198 | # html_domain_indices = True
199 |
200 | # If false, no index is generated.
201 | #
202 | # html_use_index = True
203 |
204 | # If true, the index is split into individual pages for each letter.
205 | #
206 | # html_split_index = False
207 |
208 | # If true, links to the reST sources are added to the pages.
209 | #
210 | # html_show_sourcelink = True
211 |
212 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
213 | #
214 | # html_show_sphinx = True
215 |
216 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
217 | #
218 | # html_show_copyright = True
219 |
220 | # If true, an OpenSearch description file will be output, and all pages will
221 | # contain a tag referring to it. The value of this option must be the
222 | # base URL from which the finished HTML is served.
223 | #
224 | # html_use_opensearch = ''
225 |
226 | # This is the file name suffix for HTML files (e.g. ".xhtml").
227 | # html_file_suffix = None
228 |
229 | # Language to be used for generating the HTML full-text search index.
230 | # Sphinx supports the following languages:
231 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
232 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
233 | #
234 | # html_search_language = 'en'
235 |
236 | # A dictionary with options for the search language support, empty by default.
237 | # 'ja' uses this config value.
238 | # 'zh' user can custom change `jieba` dictionary path.
239 | #
240 | # html_search_options = {'type': 'default'}
241 |
242 | # The name of a javascript file (relative to the configuration directory) that
243 | # implements a search results scorer. If empty, the default will be used.
244 | #
245 | # html_search_scorer = 'scorer.js'
246 |
247 | # Output file base name for HTML help builder.
248 | htmlhelp_basename = 'flask-rest-jsonapidoc'
249 |
250 | # -- Options for LaTeX output ---------------------------------------------
251 |
252 | latex_elements = {
253 | # The paper size ('letterpaper' or 'a4paper').
254 | #
255 | # 'papersize': 'letterpaper',
256 |
257 | # The font size ('10pt', '11pt' or '12pt').
258 | #
259 | # 'pointsize': '10pt',
260 |
261 | # Additional stuff for the LaTeX preamble.
262 | #
263 | # 'preamble': '',
264 |
265 | # Latex figure (float) alignment
266 | #
267 | # 'figure_align': 'htbp',
268 | }
269 |
270 | # Grouping the document tree into LaTeX files. List of tuples
271 | # (source start file, target name, title,
272 | # author, documentclass [howto, manual, or own class]).
273 | latex_documents = [
274 | (master_doc, 'flask-rest-jsonapi.tex', 'flask-rest-jsonapi Documentation',
275 | 'miLibris', 'manual'),
276 | ]
277 |
278 | # The name of an image file (relative to this directory) to place at the top of
279 | # the title page.
280 | #
281 | # latex_logo = None
282 |
283 | # For "manual" documents, if this is true, then toplevel headings are parts,
284 | # not chapters.
285 | #
286 | # latex_use_parts = False
287 |
288 | # If true, show page references after internal links.
289 | #
290 | # latex_show_pagerefs = False
291 |
292 | # If true, show URL addresses after external links.
293 | #
294 | # latex_show_urls = False
295 |
296 | # Documents to append as an appendix to all manuals.
297 | #
298 | # latex_appendices = []
299 |
300 | # It false, will not define \strong, \code, itleref, \crossref ... but only
301 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
302 | # packages.
303 | #
304 | # latex_keep_old_macro_names = True
305 |
306 | # If false, no module index is generated.
307 | #
308 | # latex_domain_indices = True
309 |
310 |
311 | # -- Options for manual page output ---------------------------------------
312 |
313 | # One entry per manual page. List of tuples
314 | # (source start file, name, description, authors, manual section).
315 | man_pages = [
316 | (master_doc, 'flask-rest-jsonapi', 'flask-rest-jsonapi Documentation',
317 | [author], 1)
318 | ]
319 |
320 | # If true, show URL addresses after external links.
321 | #
322 | # man_show_urls = False
323 |
324 |
325 | # -- Options for Texinfo output -------------------------------------------
326 |
327 | # Grouping the document tree into Texinfo files. List of tuples
328 | # (source start file, target name, title, author,
329 | # dir menu entry, description, category)
330 | texinfo_documents = [
331 | (master_doc, 'flask-rest-jsonapi', 'flask-rest-jsonapi Documentation',
332 | author, 'flask-rest-jsonapi', 'One line description of project.',
333 | 'Miscellaneous'),
334 | ]
335 |
336 | # Documents to append as an appendix to all manuals.
337 | #
338 | # texinfo_appendices = []
339 |
340 | # If false, no module index is generated.
341 | #
342 | # texinfo_domain_indices = True
343 |
344 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
345 | #
346 | # texinfo_show_urls = 'footnote'
347 |
348 | # If true, do not generate a @detailmenu in the "Top" node's menu.
349 | #
350 | # texinfo_no_detailmenu = False
351 |
352 | RTD_NEW_THEME = True
353 |
--------------------------------------------------------------------------------
/docs/data_layer.rst:
--------------------------------------------------------------------------------
1 | .. _data_layer:
2 |
3 | Data layer
4 | ==========
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | | The data layer is a CRUD interface between resource manager and data. It is a very flexible system to use any ORM or data storage. You can even create a data layer that use multiple ORMs and data storage to manage your own objects. The data layer implements a CRUD interface for objects and relationships. It also manage pagination, filtering and sorting.
9 | |
10 | | Flask-REST-JSONAPI got a full featured data layer that use the popular ORM `SQLAlchemy `_.
11 |
12 | .. note::
13 |
14 | The default data layer used by a resource manager is the SQLAlchemy one. So if you want to use it, you don't have to specify the class of the data layer in the resource manager
15 |
16 | To configure the data layer you have to set his required parameters in the resource manager.
17 |
18 | Example:
19 |
20 | .. code-block:: python
21 |
22 | from flask_rest_jsonapi import ResourceList
23 | from your_project.schemas import PersonSchema
24 | from your_project.models import Person
25 |
26 | class PersonList(ResourceList):
27 | schema = PersonSchema
28 | data_layer = {'session': db.session,
29 | 'model': Person}
30 |
31 | You can also plug additional methods to your data layer in the resource manager. There is 2 kind of additional methods:
32 |
33 | * query: the "query" additional method takes view_kwargs as parameter and return an alternative query to retrieve the collection of objects in the get method of the ResourceList manager.
34 |
35 | * pre / post process methods: all CRUD and relationship(s) operations have a pre / post process methods. Thanks to it you can make additional work before and after each operations of the data layer. Parameters of each pre / post process methods are available in the `flask_rest_jsonapi.data_layers.base.Base `_ base class.
36 |
37 | Example:
38 |
39 | .. code-block:: python
40 |
41 | from sqlalchemy.orm.exc import NoResultFound
42 | from flask_rest_jsonapi import ResourceList
43 | from flask_rest_jsonapi.exceptions import ObjectNotFound
44 | from your_project.models import Computer, Person
45 |
46 | class ComputerList(ResourceList):
47 | def query(self, view_kwargs):
48 | query_ = self.session.query(Computer)
49 | if view_kwargs.get('id') is not None:
50 | try:
51 | self.session.query(Person).filter_by(id=view_kwargs['id']).one()
52 | except NoResultFound:
53 | raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
54 | else:
55 | query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
56 | return query_
57 |
58 | def before_create_object(self, data, view_kwargs):
59 | if view_kwargs.get('id') is not None:
60 | person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
61 | data['person_id'] = person.id
62 |
63 | schema = ComputerSchema
64 | data_layer = {'session': db.session,
65 | 'model': Computer,
66 | 'methods': {'query': query,
67 | 'before_create_object': before_create_object}}
68 |
69 | .. note::
70 |
71 | You don't have to declare additonals data layer methods in the resource manager. You can declare them in a dedicated module or in the declaration of the model.
72 |
73 | Example:
74 |
75 | .. code-block:: python
76 |
77 | from sqlalchemy.orm.exc import NoResultFound
78 | from flask_rest_jsonapi import ResourceList
79 | from flask_rest_jsonapi.exceptions import ObjectNotFound
80 | from your_project.models import Computer, Person
81 | from your_project.additional_methods.computer import before_create_object
82 |
83 | class ComputerList(ResourceList):
84 | schema = ComputerSchema
85 | data_layer = {'session': db.session,
86 | 'model': Computer,
87 | 'methods': {'query': Computer.query,
88 | 'before_create_object': before_create_object}}
89 |
90 | SQLAlchemy
91 | ----------
92 |
93 | Required parameters:
94 |
95 | :session: the session used by the data layer
96 | :model: the model used by the data layer
97 |
98 | Optional parameters:
99 |
100 | :id_field: the field used as identifier field instead of the primary key of the model
101 | :url_field: the name of the parameter in the route to get value to filter with. Instead "id" is used.
102 |
103 | Custom data layer
104 | -----------------
105 |
106 | Like I said previously you can create and use your own data layer. A custom data layer must inherit from `flask_rest_jsonapi.data_layers.base.Base `_. You can see the full scope of possibilities of a data layer in this base class.
107 |
108 | Usage example:
109 |
110 | .. code-block:: python
111 |
112 | from flask_rest_jsonapi import ResourceList
113 | from your_project.schemas import PersonSchema
114 | from your_project.data_layers import MyCustomDataLayer
115 |
116 | class PersonList(ResourceList):
117 | schema = PersonSchema
118 | data_layer = {'class': MyCustomDataLayer,
119 | 'param_1': value_1,
120 | 'param_2': value_2}
121 |
122 | .. note::
123 |
124 | All items except "class" in the data_layer dict of the resource manager will be plugged as instance attributes of the data layer. It is easier to use in the data layer.
125 |
--------------------------------------------------------------------------------
/docs/errors.rst:
--------------------------------------------------------------------------------
1 | .. _errors:
2 |
3 | Errors
4 | ======
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | JSONAPI 1.0 specification recommand to return errors like that:
9 |
10 | .. sourcecode:: http
11 |
12 | HTTP/1.1 422 Unprocessable Entity
13 | Content-Type: application/vnd.api+json
14 |
15 | {
16 | "errors": [
17 | {
18 | "status": "422",
19 | "source": {
20 | "pointer":"/data/attributes/first-name"
21 | },
22 | "title": "Invalid Attribute",
23 | "detail": "First name must contain at least three characters."
24 | }
25 | ],
26 | "jsonapi": {
27 | "version": "1.0"
28 | }
29 | }
30 |
31 | The "source" field gives information about the error if it is located in data provided or in querystring parameter.
32 |
33 | The previous example displays error located in data provided instead of this next example displays error located in querystring parameter "include":
34 |
35 | .. sourcecode:: http
36 |
37 | HTTP/1.1 400 Bad Request
38 | Content-Type: application/vnd.api+json
39 |
40 | {
41 | "errors": [
42 | {
43 | "status": "400",
44 | "source": {
45 | "parameter": "include"
46 | },
47 | "title": "BadRequest",
48 | "detail": "Include parameter is invalid"
49 | }
50 | ],
51 | "jsonapi": {
52 | "version": "1.0"
53 | }
54 | }
55 |
56 | Flask-REST-JSONAPI provides two kind of helpers to achieve error displaying:
57 |
58 | | * **the errors module**: you can import jsonapi_errors from the `errors module `_ to create the structure of a list of errors according to JSONAPI 1.0 specification
59 | |
60 | | * **the exceptions module**: you can import lot of exceptions from this `module `_ that helps you to raise exceptions that will be well formatted according to JSONAPI 1.0 specification
61 |
62 | When you create custom code for your api I recommand to use exceptions from exceptions module of Flask-REST-JSONAPI to raise errors because JsonApiException based exceptions are catched and rendered according to JSONAPI 1.0 specification.
63 |
64 | Example:
65 |
66 | .. code-block:: python
67 |
68 | # all required imports are not displayed in this example
69 | from flask_rest_jsonapi.exceptions import ObjectNotFound
70 |
71 | class ComputerList(ResourceList):
72 | def query(self, view_kwargs):
73 | query_ = self.session.query(Computer)
74 | if view_kwargs.get('id') is not None:
75 | try:
76 | self.session.query(Person).filter_by(id=view_kwargs['id']).one()
77 | except NoResultFound:
78 | raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
79 | else:
80 | query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
81 | return query_
82 |
--------------------------------------------------------------------------------
/docs/filtering.rst:
--------------------------------------------------------------------------------
1 | .. _filtering:
2 |
3 | Filtering
4 | =========
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | Flask-REST-JSONAPI as a very flexible filtering system. The filtering system is completely related to the data layer used by the ResourceList manager. I will explain the filtering interface for SQLAlchemy data layer but you can use the same interface to your filtering implementation of your custom data layer. The only requirement is that you have to use the "filter" querystring parameter to make filtering according to JSONAPI 1.0 specification.
9 |
10 | .. note::
11 |
12 | Examples are not urlencoded for a better readability
13 |
14 | SQLAlchemy
15 | ----------
16 |
17 | The filtering system of SQLAlchemy data layer has exactly the same interface as the filtering system of `Flask-Restless `_.
18 |
19 | So this is a first example:
20 |
21 | .. sourcecode:: http
22 |
23 | GET /persons?filter=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
24 | Accept: application/vnd.api+json
25 |
26 | In this example we want to retrieve persons which name is John. So we can see that the filtering interface completely fit the filtering interface of SQLAlchemy: a list a filter information.
27 |
28 | :name: the name of the field you want to filter on
29 | :op: the operation you want to use (all sqlalchemy operations are available)
30 | :val: the value that you want to compare. You can replace this by "field" if you want to compare against the value of an other field
31 |
32 | Example with field:
33 |
34 | .. sourcecode:: http
35 |
36 | GET /persons?filter=[{"name":"name","op":"eq","field":"birth_date"}] HTTP/1.1
37 | Accept: application/vnd.api+json
38 |
39 | In this example, we want to retrieve persons that name is equal to his birth_date. I know, this example is absurd but it is just to explain the syntax of this kind of filter.
40 |
41 | If you want to filter through relationships you can do that:
42 |
43 | .. sourcecode:: http
44 |
45 | GET /persons?filter=[
46 | {
47 | "name": "computers",
48 | "op": "any",
49 | "val": {
50 | "name": "serial",
51 | "op": "ilike",
52 | "val": "%Amstrad%"
53 | }
54 | }
55 | ] HTTP/1.1
56 | Accept: application/vnd.api+json
57 |
58 | .. note ::
59 |
60 | When you filter on relationships use "any" operator for "to many" relationships and "has" operator for "to one" relationships.
61 |
62 | There is a shortcut to achieve the same filter:
63 |
64 | .. sourcecode:: http
65 |
66 | GET /persons?filter=[{"name":"computers__serial","op":"ilike","val":"%Amstrad%"}] HTTP/1.1
67 | Accept: application/vnd.api+json
68 |
69 | You can also use boolean combinaison of operations:
70 |
71 | .. sourcecode:: http
72 |
73 | GET /persons?filter=[
74 | {
75 | "name":"computers__serial",
76 | "op":"ilike",
77 | "val":"%Amstrad%"
78 | },
79 | {
80 | "or": {
81 | [
82 | {
83 | "not": {
84 | "name": "name",
85 | "op": "eq",
86 | "val":"John"
87 | }
88 | },
89 | {
90 | "and": [
91 | {
92 | "name": "name",
93 | "op": "like",
94 | "val": "%Jim%"
95 | },
96 | {
97 | "name": "birth_date",
98 | "op": "gt",
99 | "val": "1990-01-01"
100 | }
101 | ]
102 | }
103 | ]
104 | }
105 | }
106 | ] HTTP/1.1
107 | Accept: application/vnd.api+json
108 |
109 | Common available operators:
110 |
111 | * any: used to filter on to many relationships
112 | * between: used to filter a field between two values
113 | * endswith: check if field ends with a string
114 | * eq: check if field is equal to something
115 | * ge: check if field is greater than or equal to something
116 | * gt: check if field is greater than to something
117 | * has: used to filter on to one relationships
118 | * ilike: check if field contains a string (case insensitive)
119 | * in_: check if field is in a list of values
120 | * is_: check if field is a value
121 | * isnot: check if field is not a value
122 | * like: check if field contains a string
123 | * le: check if field is less than or equal to something
124 | * lt: check if field is less than to something
125 | * match: check if field match against a string or pattern
126 | * ne: check if field is not equal to something
127 | * notilike: check if field does not contains a string (case insensitive)
128 | * notin_: check if field is not in a list of values
129 | * notlike: check if field does not contains a string
130 | * startswith: check if field starts with a string
131 |
132 | .. note::
133 |
134 | Availables operators depend on field type in your model
--------------------------------------------------------------------------------
/docs/flask-rest-jsonapi.rst:
--------------------------------------------------------------------------------
1 | flask_rest_jsonapi package
2 | ==========================
3 |
4 | flask_rest_jsonapi.data_layers.filtering.alchemy module
5 | -------------------------------------------------------
6 |
7 | .. automodule:: flask_rest_jsonapi.data_layers.filtering.alchemy
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | flask_rest_jsonapi.data_layers.base module
13 | ------------------------------------------
14 |
15 | .. automodule:: flask_rest_jsonapi.data_layers.filtering.alchemy
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | flask_rest_jsonapi.data_layers.alchemy module
21 | ---------------------------------------------
22 |
23 | .. automodule:: flask_rest_jsonapi.data_layers.alchemy
24 | :members:
25 | :undoc-members:
26 | :show-inheritance:
27 |
28 | flask_rest_jsonapi.api module
29 | -----------------------------
30 |
31 | .. automodule:: flask_rest_jsonapi.api
32 | :members:
33 | :undoc-members:
34 | :show-inheritance:
35 |
36 | flask_rest_jsonapi.constants module
37 | -----------------------------------
38 |
39 | .. automodule:: flask_rest_jsonapi.constants
40 | :members:
41 | :undoc-members:
42 | :show-inheritance:
43 |
44 | flask_rest_jsonapi.decorators module
45 | ------------------------------------
46 |
47 | .. automodule:: flask_rest_jsonapi.decorators
48 | :members:
49 | :undoc-members:
50 | :show-inheritance:
51 |
52 | flask_rest_jsonapi.errors module
53 | --------------------------------
54 |
55 | .. automodule:: flask_rest_jsonapi.errors
56 | :members:
57 | :undoc-members:
58 | :show-inheritance:
59 |
60 | flask_rest_jsonapi.exceptions module
61 | ------------------------------------
62 |
63 | .. automodule:: flask_rest_jsonapi.exceptions
64 | :members:
65 | :undoc-members:
66 | :show-inheritance:
67 |
68 | flask_rest_jsonapi.pagination module
69 | ------------------------------------
70 |
71 | .. automodule:: flask_rest_jsonapi.pagination
72 | :members:
73 | :undoc-members:
74 | :show-inheritance:
75 |
76 | flask_rest_jsonapi.querystring module
77 | -------------------------------------
78 |
79 | .. automodule:: flask_rest_jsonapi.querystring
80 | :members:
81 | :undoc-members:
82 | :show-inheritance:
83 |
84 | flask_rest_jsonapi.resource module
85 | ----------------------------------
86 |
87 | .. automodule:: flask_rest_jsonapi.resource
88 | :members:
89 | :undoc-members:
90 | :show-inheritance:
91 |
92 | flask_rest_jsonapi.schema module
93 | --------------------------------
94 |
95 | .. automodule:: flask_rest_jsonapi.schema
96 | :members:
97 | :undoc-members:
98 | :show-inheritance:
99 |
--------------------------------------------------------------------------------
/docs/img/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossasia/flask-rest-jsonapi/a03408bffd5ef96bf3b8abe3a30d147db46fbe47/docs/img/schema.png
--------------------------------------------------------------------------------
/docs/include_related_objects.rst:
--------------------------------------------------------------------------------
1 | .. _include_related_objects:
2 |
3 | Include related objects
4 | =======================
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | You can include related object(s) details to responses with the querystring parameter named "include". You can use "include" parameter on any kind of route (classical CRUD route or relationships route) and any kind of http methods as long as method return data.
9 |
10 | This features will add an additional key in result named "included"
11 |
12 | Example:
13 |
14 | Request:
15 |
16 | .. sourcecode:: http
17 |
18 | GET /persons/1?include=computers HTTP/1.1
19 | Accept: application/vnd.api+json
20 |
21 | Response:
22 |
23 | .. sourcecode:: http
24 |
25 | HTTP/1.1 200 OK
26 | Content-Type: application/vnd.api+json
27 |
28 | {
29 | "data": {
30 | "type": "person",
31 | "id": "1",
32 | "attributes": {
33 | "display_name": "JEAN ",
34 | "birth_date": "1990-10-10"
35 | },
36 | "relationships": {
37 | "computers": {
38 | "data": [
39 | {
40 | "type": "computer",
41 | "id": "1"
42 | }
43 | ],
44 | "links": {
45 | "related": "/persons/1/computers",
46 | "self": "/persons/1/relationships/computers"
47 | }
48 | }
49 | },
50 | "links": {
51 | "self": "/persons/1"
52 | }
53 | },
54 | "included": [
55 | {
56 | "type": "computer",
57 | "id": "1",
58 | "attributes": {
59 | "serial": "Amstrad"
60 | },
61 | "relationships": {
62 | "owner": {
63 | "links": {
64 | "related": "/computers/1/owner",
65 | "self": "/computers/1/relationships/owner"
66 | }
67 | }
68 | },
69 | "links": {
70 | "self": "/computers/1"
71 | }
72 | }
73 | ],
74 | "links": {
75 | "self": "/persons/1"
76 | },
77 | "jsonapi": {
78 | "version": "1.0"
79 | }
80 | }
81 |
82 | You can even follow relationships with include
83 |
84 | Example:
85 |
86 | Request:
87 |
88 | .. sourcecode:: http
89 |
90 | GET /persons/1?include=computers.owner HTTP/1.1
91 | Accept: application/vnd.api+json
92 |
93 | Response:
94 |
95 | .. sourcecode:: http
96 |
97 | HTTP/1.1 200 OK
98 | Content-Type: application/vnd.api+json
99 |
100 | {
101 | "data": {
102 | "type": "person",
103 | "id": "1",
104 | "attributes": {
105 | "display_name": "JEAN ",
106 | "birth_date": "1990-10-10"
107 | },
108 | "relationships": {
109 | "computers": {
110 | "data": [
111 | {
112 | "type": "computer",
113 | "id": "1"
114 | }
115 | ],
116 | "links": {
117 | "related": "/persons/1/computers",
118 | "self": "/persons/1/relationships/computers"
119 | }
120 | }
121 | },
122 | "links": {
123 | "self": "/persons/1"
124 | }
125 | },
126 | "included": [
127 | {
128 | "type": "computer",
129 | "id": "1",
130 | "attributes": {
131 | "serial": "Amstrad"
132 | },
133 | "relationships": {
134 | "owner": {
135 | "data": {
136 | "type": "person",
137 | "id": "1"
138 | },
139 | "links": {
140 | "related": "/computers/1/owner",
141 | "self": "/computers/1/relationships/owner"
142 | }
143 | }
144 | },
145 | "links": {
146 | "self": "/computers/1"
147 | }
148 | },
149 | {
150 | "type": "person",
151 | "id": "1",
152 | "attributes": {
153 | "display_name": "JEAN ",
154 | "birth_date": "1990-10-10"
155 | },
156 | "relationships": {
157 | "computers": {
158 | "links": {
159 | "related": "/persons/1/computers",
160 | "self": "/persons/1/relationships/computers"
161 | }
162 | }
163 | },
164 | "links": {
165 | "self": "/persons/1"
166 | }
167 | }
168 | ],
169 | "links": {
170 | "self": "/persons/1"
171 | },
172 | "jsonapi": {
173 | "version": "1.0"
174 | }
175 | }
176 |
177 | I know it is an absurd example because it will include details of related person computers and details of the person that is already in reponse. But it is just for example.
178 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Flask-REST-JSONAPI
2 | ==================
3 |
4 | .. module:: flask_rest_jsonapi
5 |
6 | **Flask-REST-JSONAPI** is an extension for Flask that adds support for quickly building REST APIs with huge flexibility around the JSONAPI 1.0 specification. It is designed to fit the complexity of real life environments so Flask-REST-JSONAPI helps you to create a logical abstraction of your data called "resource" and can interface any kind of ORMs or data storage through data layer concept.
7 |
8 | Main concepts
9 | -------------
10 |
11 | .. image:: img/schema.png
12 | :width: 900px
13 | :alt: Architecture
14 |
15 | | * `JSON API 1.0 specification `_: it is a very popular specification about client server interactions for REST JSON API. It helps you work in a team because it is very precise and sharable. Thanks to this specification your API offers lots of features like a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting etc.
16 | |
17 | | * **Logical data abstration**: you usually need to expose resources to clients that don't fit your data table architecture. For example sometimes you don't want to expose all attributes of a table, compute additional attributes or create a resource that uses data from multiple data storages. Flask-REST-JSONAPI helps you create a logical abstraction of your data with `Marshmallow `_ / `marshmallow-jsonapi `_ so you can expose your data through a very flexible way.
18 | |
19 | | * **Data layer**: the data layer is a CRUD interface between your resource manager and your data. Thanks to it you can use any data storage or ORMs. There is an already full featured data layer that uses the SQLAlchemy ORM but you can create and use your own custom data layer to use data from your data storage(s). You can even create a data layer that uses multiple data storages and ORMs, send notifications or make any custom work during CRUD operations.
20 |
21 | Features
22 | --------
23 |
24 | Flask-REST-JSONAPI has lot of features:
25 |
26 | * Relationship management
27 | * Powerful filtering
28 | * Include related objects
29 | * Sparse fieldsets
30 | * Pagination
31 | * Sorting
32 | * Permission management
33 | * OAuth support
34 |
35 |
36 | User's Guide
37 | ------------
38 |
39 | This part of the documentation will show you how to get started in using
40 | Flask-REST-JSONAPI with Flask.
41 |
42 | .. toctree::
43 | :maxdepth: 3
44 |
45 | installation
46 | quickstart
47 | logical_data_abstraction
48 | resource_manager
49 | data_layer
50 | routing
51 | filtering
52 | include_related_objects
53 | sparse_fieldsets
54 | pagination
55 | sorting
56 | errors
57 | api
58 | permission
59 | oauth
60 |
61 | API Reference
62 | -------------
63 |
64 | If you are looking for information on a specific function, class or
65 | method, this part of the documentation is for you.
66 |
67 | * :ref:`genindex`
68 | * :ref:`modindex`
69 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | Installation
4 | ============
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | Install Flask-REST-JSONAPI with ``pip`` ::
9 |
10 | pip install flask-rest-jsonapi
11 |
12 |
13 | The development version can be downloaded from `its page at GitHub
14 | `_. ::
15 |
16 | git clone https://github.com/miLibris/flask-rest-jsonapi.git
17 | cd flask-rest-jsonapi
18 | mkvirtualenv venv
19 | python setup.py install
20 |
21 | .. note::
22 |
23 | If you don't have virtualenv please install it before
24 |
25 | $ pip install virtualenv
26 |
27 |
28 | Flask-RESTful requires Python version 2.7, 3.4 or 3.5.
--------------------------------------------------------------------------------
/docs/logical_data_abstraction.rst:
--------------------------------------------------------------------------------
1 | .. _logical_data_abstraction:
2 |
3 | Logical data abstraction
4 | ========================
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | The first thing to do in Flask-REST-JSONAPI is to create a logical data abstraction. This part of the api discribes schemas of resources exposed by the api that is not the exact mapping of data architecture. The declaration of schemas is made my `Marshmallow `_ / `marshmallow-jsonapi `_. Marshmallow is a very popular serialization / deserialization library that offer lot a features to abstract your data architecture. Moreover there is an other library called marshmallow-jsonapi that fit the JSONAPI 1.0 specification and provides Flask integration.
9 |
10 | Example:
11 |
12 | In this example, let's assume that we have 2 legacy models Person and Computer and we want to create an abstraction over them.
13 |
14 | .. code-block:: python
15 |
16 | from flask_sqlalchemy import SQLAlchemy
17 |
18 | db = SQLAlchemy()
19 |
20 | class Person(db.Model):
21 | id = db.Column(db.Integer, primary_key=True)
22 | name = db.Column(db.String)
23 | email = db.Column(db.String)
24 | birth_date = db.Column(db.String)
25 | password = db.Column(db.String)
26 |
27 |
28 | class Computer(db.Model):
29 | computer_id = db.Column(db.Integer, primary_key=True)
30 | serial = db.Column(db.String)
31 | person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
32 | person = db.relationship('Person', backref=db.backref('computers'))
33 |
34 | Now let's create the logical abstraction to illustrate this concept.
35 |
36 | .. code-block:: python
37 |
38 | from marshmallow_jsonapi.flask import Schema, Relationship
39 | from marshmallow_jsonapi import fields
40 |
41 | class PersonSchema(Schema):
42 | class Meta:
43 | type_ = 'person'
44 | self_view = 'person_detail'
45 | self_view_kwargs = {'id': ''}
46 | self_view_many = 'person_list'
47 |
48 | id = fields.Str(dump_only=True)
49 | name = fields.Str(required=True, load_only=True)
50 | email = fields.Email(load_only=True)
51 | birth_date = fields.Date()
52 | display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
53 | computers = Relationship(self_view='person_computers',
54 | self_view_kwargs={'id': ''},
55 | related_view='computer_list',
56 | related_view_kwargs={'id': ''},
57 | many=True,
58 | schema='ComputerSchema',
59 | type_='computer')
60 |
61 |
62 | class ComputerSchema(Schema):
63 | class Meta:
64 | type_ = 'computer'
65 | self_view = 'computer_detail'
66 | self_view_kwargs = {'id': ''}
67 |
68 | id = fields.Str(dump_only=True, attribute='computer_id')
69 | serial = fields.Str(required=True)
70 | owner = Relationship(attribute='person',
71 | self_view='computer_person',
72 | self_view_kwargs={'id': ''},
73 | related_view='person_detail',
74 | related_view_kwargs={'computer_id': ''},
75 | schema='PersonSchema',
76 | type_='person')
77 |
78 | You can see several differences between models and schemas exposed by the api.
79 |
80 | First, take a look of Person compared to PersonSchema:
81 |
82 | * we can see that Person has an attribute named "password" and we don't want to expose it through the api so it is not set in PersonSchema
83 | * PersonSchema has an attribute named "display_name" that is the result of concatenation of name and email
84 |
85 | Second, take a look of Computer compared to ComputerSchema:
86 |
87 | * we can see that attribute computer_id is exposed as id for concictency of the api
88 | * we can see that person relationship between Computer and Person is exposed in ComputerSchema as owner because it is more explicit
89 |
90 | As a result you can see that you can expose your data through a very flexible way to create the api of your choice over your data architecture.
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | flask-rest-jsonapi
2 | ==================
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | flask-rest-jsonapi
8 |
--------------------------------------------------------------------------------
/docs/oauth.rst:
--------------------------------------------------------------------------------
1 | .. _oauth:
2 |
3 | OAuth
4 | =====
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | Flask-REST-JSONAPI support OAuth via `Flask-OAuthlib `_
9 |
10 | Example:
11 |
12 | .. code-block:: python
13 |
14 | from flask import Flask
15 | from flask_rest_jsonapi import Api
16 | from flask_oauthlib.provider import OAuth2Provider
17 |
18 | app = Flask(__name__)
19 | oauth2 = OAuth2Provider()
20 |
21 | api = Api()
22 | api.init_app(app)
23 | api.oauth_manager(oauth2)
24 |
25 |
26 | In this example Flask-REST-JSONAPI will protect all your resource methods with this decorator ::
27 |
28 | oauth2.require_oauth()
29 |
30 | The pattern of the scope is like that ::
31 |
32 | _
33 |
34 | Where action is:
35 |
36 | * list: for the get method of a ResourceList
37 | * create: for the post method of a ResourceList
38 | * get: for the get method of a ResourceDetail
39 | * update: for the patch method of a ResourceDetail
40 | * delete: for the delete method of a ResourceDetail
41 |
42 | Example ::
43 |
44 | list_person
45 |
46 | If you want to customize the scope you can provide a function that computes your custom scope. The function have to looks like that:
47 |
48 | .. code-block:: python
49 |
50 | def get_scope(resource, method):
51 | """Compute the name of the scope for oauth
52 |
53 | :param Resource resource: the resource manager
54 | :param str method: an http method
55 | :return str: the name of the scope
56 | """
57 | return 'custom_scope'
58 |
59 | Usage example:
60 |
61 | .. code-block:: python
62 |
63 | from flask import Flask
64 | from flask_rest_jsonapi import Api
65 | from flask_oauthlib.provider import OAuth2Provider
66 |
67 | app = Flask(__name__)
68 | oauth2 = OAuth2Provider()
69 |
70 | api = Api()
71 | api.init_app(app)
72 | api.oauth_manager(oauth2)
73 | api.scope_setter(get_scope)
74 |
75 | .. note::
76 |
77 | You can name the custom scope computation method as you want but you have to set the 2 required parameters: resource and method like in this previous example.
78 |
79 | If you want to disable OAuth or make custom methods protection for a resource you can add this option to the resource manager.
80 |
81 | Example:
82 |
83 | .. code-block:: python
84 |
85 | from flask_rest_jsonapi import ResourceList
86 | from your_project.extensions import oauth2
87 |
88 | class PersonList(ResourceList):
89 | disable_oauth = True
90 |
91 | @oauth2.require_oauth('custom_scope')
92 | def get(*args, **kwargs):
93 | return 'Hello world !'
94 |
--------------------------------------------------------------------------------
/docs/pagination.rst:
--------------------------------------------------------------------------------
1 | .. _pagination:
2 |
3 | Pagination
4 | ==========
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | When you use the default implementation of get method on a ResourceList your results will be paginated by default. Default pagination size is 20 but you can manage it from querystring parameter named "page".
9 |
10 | .. note::
11 |
12 | Examples are not urlencoded for a better readability
13 |
14 | Size
15 | ----
16 |
17 | You can control page size like that:
18 |
19 | .. sourcecode:: http
20 |
21 | GET /persons?page[size]=10 HTTP/1.1
22 | Accept: application/vnd.api+json
23 |
24 | Number
25 | ------
26 |
27 | You can control page number like that:
28 |
29 | .. sourcecode:: http
30 |
31 | GET /persons?page[number]=2 HTTP/1.1
32 | Accept: application/vnd.api+json
33 |
34 | Size + Number
35 | -------------
36 |
37 | Of course, you can control both like that:
38 |
39 | .. sourcecode:: http
40 |
41 | GET /persons?page[size]=10&page[number]=2 HTTP/1.1
42 | Accept: application/vnd.api+json
43 |
44 | Disable pagination
45 | ------------------
46 |
47 | You can disable pagination with size = 0
48 |
49 | .. sourcecode:: http
50 |
51 | GET /persons?page[size]=0 HTTP/1.1
52 | Accept: application/vnd.api+json
53 |
--------------------------------------------------------------------------------
/docs/permission.rst:
--------------------------------------------------------------------------------
1 | .. _permission:
2 |
3 | Permission
4 | ==========
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | Flask-REST-JSONAPI provides a very agnostic permission system.
9 |
10 | Example:
11 |
12 | .. code-block:: python
13 |
14 | from flask import Flask
15 | from flask_rest_jsonapi import Api
16 | from your_project.permission import permission_manager
17 |
18 | app = Flask(__name__)
19 |
20 | api = Api()
21 | api.init_app(app)
22 | api.permission_manager(permission_manager)
23 |
24 | In this previous example, the API will check permission before each method call with the permission_manager function.
25 |
26 | The permission manager must be a function that looks like this:
27 |
28 | .. code-block:: python
29 |
30 | def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
31 | """The function use to check permissions
32 |
33 | :param callable view: the view
34 | :param list view_args: view args
35 | :param dict view_kwargs: view kwargs
36 | :param list args: decorator args
37 | :param dict kwargs: decorator kwargs
38 | """
39 |
40 | .. note::
41 |
42 | Flask-REST-JSONAPI use a decorator to check permission for each method named has_permission. You can provide args and kwargs to this decorators so you can retrieve this args and kwargs in the permission_manager. The default usage of the permission system does not provides any args or kwargs to the decorator.
43 |
44 | If permission is denied I recommand to raise exception like that:
45 |
46 | .. code-block:: python
47 |
48 | raise JsonApiException(,
49 | ,
50 | title='Permission denied',
51 | status='403')
52 |
53 | You can disable the permission system or make custom permission checking management of a resource like that:
54 |
55 | .. code-block:: python
56 |
57 | from flask_rest_jsonapi import ResourceList
58 | from your_project.extensions import api
59 |
60 | class PersonList(ResourceList):
61 | disable_permission = True
62 |
63 | @api.has_permission('custom_arg', custom_kwargs='custom_kwargs')
64 | def get(*args, **kwargs):
65 | return 'Hello world !'
66 |
67 | .. warning::
68 |
69 | If you want to use both permission system and oauth support to retrieve information like user from oauth (request.oauth.user) in the permission system you have to initialize permission system before to initialize oauth support because of decorators cascading.
70 |
71 | Example:
72 |
73 | .. code-block:: python
74 |
75 | from flask import Flask
76 | from flask_rest_jsonapi import Api
77 | from flask_oauthlib.provider import OAuth2Provider
78 | from your_project.permission import permission_manager
79 |
80 | app = Flask(__name__)
81 | oauth2 = OAuth2Provider()
82 |
83 | api = Api()
84 | api.init_app(app)
85 | api.permission_manager(permission_manager) # initialize permission system first
86 | api.oauth_manager(oauth2) # initialize oauth support second
--------------------------------------------------------------------------------
/docs/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. _quickstart:
2 |
3 | Quickstart
4 | ==========
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | It's time to write your first REST API. This guide assumes you have a working understanding of `Flask `_, and that you have already installed both Flask and Flask-REST-JSONAPI. If not, then follow the steps in the :ref:`installation` section.
9 |
10 | In this section you will learn basic usage of Flask-REST-JSONAPI around a small tutorial that use the SQLAlchemy data layer. This tutorial show you an example of a person and his computers.
11 |
12 | First example
13 | -------------
14 |
15 | An example of Flask-REST-JSONAPI API looks like this:
16 |
17 | .. code-block:: python
18 |
19 | # -*- coding: utf-8 -*-
20 |
21 | from flask import Flask
22 | from flask_rest_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
23 | from flask_rest_jsonapi.exceptions import ObjectNotFound
24 | from flask_sqlalchemy import SQLAlchemy
25 | from sqlalchemy.orm.exc import NoResultFound
26 | from marshmallow_jsonapi.flask import Schema, Relationship
27 | from marshmallow_jsonapi import fields
28 |
29 | # Create the Flask application
30 | app = Flask(__name__)
31 | app.config['DEBUG'] = True
32 |
33 |
34 | # Initialize SQLAlchemy
35 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
36 | db = SQLAlchemy(app)
37 |
38 |
39 | # Create data storage
40 | class Person(db.Model):
41 | id = db.Column(db.Integer, primary_key=True)
42 | name = db.Column(db.String)
43 | email = db.Column(db.String)
44 | birth_date = db.Column(db.Date)
45 | password = db.Column(db.String)
46 |
47 |
48 | class Computer(db.Model):
49 | id = db.Column(db.Integer, primary_key=True)
50 | serial = db.Column(db.String)
51 | person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
52 | person = db.relationship('Person', backref=db.backref('computers'))
53 |
54 | db.create_all()
55 |
56 |
57 | # Create logical data abstraction (same as data storage for this first example)
58 | class PersonSchema(Schema):
59 | class Meta:
60 | type_ = 'person'
61 | self_view = 'person_detail'
62 | self_view_kwargs = {'id': ''}
63 | self_view_many = 'person_list'
64 |
65 | id = fields.Str(dump_only=True)
66 | name = fields.Str(requried=True, load_only=True)
67 | email = fields.Email(load_only=True)
68 | birth_date = fields.Date()
69 | display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
70 | computers = Relationship(self_view='person_computers',
71 | self_view_kwargs={'id': ''},
72 | related_view='computer_list',
73 | related_view_kwargs={'id': ''},
74 | many=True,
75 | schema='ComputerSchema',
76 | type_='computer')
77 |
78 |
79 | class ComputerSchema(Schema):
80 | class Meta:
81 | type_ = 'computer'
82 | self_view = 'computer_detail'
83 | self_view_kwargs = {'id': ''}
84 |
85 | id = fields.Str(dump_only=True)
86 | serial = fields.Str(requried=True)
87 | owner = Relationship(attribute='person',
88 | self_view='computer_person',
89 | self_view_kwargs={'id': ''},
90 | related_view='person_detail',
91 | related_view_kwargs={'computer_id': ''},
92 | schema='PersonSchema',
93 | type_='person')
94 |
95 |
96 | # Create resource managers
97 | class PersonList(ResourceList):
98 | schema = PersonSchema
99 | data_layer = {'session': db.session,
100 | 'model': Person}
101 |
102 |
103 | class PersonDetail(ResourceDetail):
104 | def before_get_object(self, view_kwargs):
105 | if view_kwargs.get('computer_id') is not None:
106 | try:
107 | computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
108 | except NoResultFound:
109 | raise ObjectNotFound({'parameter': 'computer_id'},
110 | "Computer: {} not found".format(view_kwargs['computer_id']))
111 | else:
112 | if computer.person is not None:
113 | view_kwargs['id'] = computer.person.id
114 | else:
115 | view_kwargs['id'] = None
116 |
117 | schema = PersonSchema
118 | data_layer = {'session': db.session,
119 | 'model': Person,
120 | 'methods': {'before_get_object': before_get_object}}
121 |
122 |
123 | class PersonRelationship(ResourceRelationship):
124 | schema = PersonSchema
125 | data_layer = {'session': db.session,
126 | 'model': Person}
127 |
128 |
129 | class ComputerList(ResourceList):
130 | def query(self, view_kwargs):
131 | query_ = self.session.query(Computer)
132 | if view_kwargs.get('id') is not None:
133 | try:
134 | self.session.query(Person).filter_by(id=view_kwargs['id']).one()
135 | except NoResultFound:
136 | raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
137 | else:
138 | query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
139 | return query_
140 |
141 | def before_create_object(self, data, view_kwargs):
142 | if view_kwargs.get('id') is not None:
143 | person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
144 | data['person_id'] = person.id
145 |
146 | schema = ComputerSchema
147 | data_layer = {'session': db.session,
148 | 'model': Computer,
149 | 'methods': {'query': query,
150 | 'before_create_object': before_create_object}}
151 |
152 |
153 | class ComputerDetail(ResourceDetail):
154 | schema = ComputerSchema
155 | data_layer = {'session': db.session,
156 | 'model': Computer}
157 |
158 |
159 | class ComputerRelationship(ResourceRelationship):
160 | schema = ComputerSchema
161 | data_layer = {'session': db.session,
162 | 'model': Computer}
163 |
164 |
165 | # Create endpoints
166 | api = Api(app)
167 | api.route(PersonList, 'person_list', '/persons')
168 | api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
169 | api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
170 | api.route(ComputerList, 'computer_list', '/computers', '/persons//computers')
171 | api.route(ComputerDetail, 'computer_detail', '/computers/')
172 | api.route(ComputerRelationship, 'computer_person', '/computers//relationships/owner')
173 |
174 | if __name__ == '__main__':
175 | # Start application
176 | app.run(debug=True)
177 |
178 | This example provides this api:
179 |
180 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
181 | | url | method | endpoint | action |
182 | +==========================================+========+==================+=======================================================+
183 | | /persons | GET | person_list | Retrieve a collection of persons |
184 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
185 | | /persons | POST | person_list | Create a person |
186 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
187 | | /persons/ | GET | person_detail | Retrieve details of a person |
188 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
189 | | /persons/ | PATCH | person_detail | Update a person |
190 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
191 | | /persons/ | DELETE | person_detail | Delete a person |
192 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
193 | | /persons//computers | GET | computer_list | Retrieve a collection computers related to a person |
194 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
195 | | /persons//computers | POST | computer_list | Create a computer related to a person |
196 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
197 | | /persons//relationship/computers | GET | person_computers | Retrieve relationships between a person and computers |
198 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
199 | | /persons//relationship/computers | POST | person_computers | Create relationships between a person and computers |
200 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
201 | | /persons//relationship/computers | PATCH | person_computers | Update relationships between a person and computers |
202 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
203 | | /persons//relationship/computers | DELETE | person_computers | Delete relationships between a person and computers |
204 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
205 | | /computers | GET | computer_list | Retrieve a collection of computers |
206 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
207 | | /computers | POST | computer_list | Create a computer |
208 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
209 | | /computers/ | GET | computer_detail | Retrieve details of a computer |
210 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
211 | | /computers/ | PATCH | computer_detail | Update a computer |
212 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
213 | | /computers/ | DELETE | computer_detail | Delete a computer |
214 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
215 | | /computers//owner | GET | person_detail | Retrieve details of the owner of a computer |
216 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
217 | | /computers//owner | PATCH | person_detail | Update the owner of a computer |
218 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
219 | | /computers//owner | DELETE | person_detail | Delete the owner of a computer |
220 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
221 | | /computers//relationship/owner | GET | person_computers | Retrieve relationships between a person and computers |
222 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
223 | | /computers//relationship/owner | POST | person_computers | Create relationships between a person and computers |
224 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
225 | | /computers//relationship/owner | PATCH | person_computers | Update relationships between a person and computers |
226 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
227 | | /computers//relationship/owner | DELETE | person_computers | Delete relationships between a person and computers |
228 | +------------------------------------------+--------+------------------+-------------------------------------------------------+
229 |
230 | .. warning::
231 |
232 | In this example, I use Flask-SQLAlchemy so you have to install it before to run the example.
233 |
234 | $ pip install flask_sqlalchemy
235 |
236 | Save this as api.py and run it using your Python interpreter. Note that we've enabled
237 | `Flask debugging `_ mode to provide code reloading and better error
238 | messages. ::
239 |
240 | $ python api.py
241 | * Running on http://127.0.0.1:5000/
242 | * Restarting with reloader
243 |
244 | .. warning::
245 |
246 | Debug mode should never be used in a production environment!
247 |
248 | Classical CRUD operations
249 | -------------------------
250 |
251 | Create object
252 | ~~~~~~~~~~~~~
253 |
254 | Request:
255 |
256 | .. sourcecode:: http
257 |
258 | POST /computers HTTP/1.1
259 | Content-Type: application/vnd.api+json
260 | Accept: application/vnd.api+json
261 |
262 | {
263 | "data": {
264 | "type": "computer",
265 | "attributes": {
266 | "serial": "Amstrad"
267 | }
268 | }
269 | }
270 |
271 | Response:
272 |
273 | .. sourcecode:: http
274 |
275 | HTTP/1.1 201 Created
276 | Content-Type: application/vnd.api+json
277 |
278 | {
279 | "data": {
280 | "type": "computer",
281 | "id": "1",
282 | "attributes": {
283 | "serial": "Amstrad"
284 | },
285 | "relationships": {
286 | "owner": {
287 | "links": {
288 | "related": "/computers/1/owner",
289 | "self": "/computers/1/relationships/owner"
290 | }
291 | }
292 | },
293 | "links": {
294 | "self": "/computers/1"
295 | }
296 | },
297 | "links": {
298 | "self": "/computers/1"
299 | },
300 | "jsonapi": {
301 | "version": "1.0"
302 | }
303 | }
304 |
305 | List objects
306 | ~~~~~~~~~~~~
307 |
308 | Request:
309 |
310 | .. sourcecode:: http
311 |
312 | GET /computers HTTP/1.1
313 | Accept: application/vnd.api+json
314 |
315 | Response:
316 |
317 | .. sourcecode:: http
318 |
319 | HTTP/1.1 200 OK
320 | Content-Type: application/vnd.api+json
321 |
322 | {
323 | "data": [
324 | {
325 | "type": "computer",
326 | "id": "1",
327 | "attributes": {
328 | "serial": "Amstrad"
329 | },
330 | "relationships": {
331 | "owner": {
332 | "links": {
333 | "related": "/computers/1/owner",
334 | "self": "/computers/1/relationships/owner"
335 | }
336 | }
337 | },
338 | "links": {
339 | "self": "/computers/1"
340 | }
341 | }
342 | ],
343 | "meta": {
344 | "count": 1
345 | },
346 | "links": {
347 | "self": "/computers"
348 | },
349 | "jsonapi": {
350 | "version": "1.0"
351 | },
352 | }
353 |
354 | Update object
355 | ~~~~~~~~~~~~~
356 |
357 | Request:
358 |
359 | .. sourcecode:: http
360 |
361 | PATCH /computers/1 HTTP/1.1
362 | Content-Type: application/vnd.api+json
363 | Accept: application/vnd.api+json
364 |
365 | {
366 | "data": {
367 | "type": "computer",
368 | "id": "1",
369 | "attributes": {
370 | "serial": "Amstrad 2"
371 | }
372 | }
373 | }
374 |
375 | Response:
376 |
377 | .. sourcecode:: http
378 |
379 | HTTP/1.1 200 OK
380 | Content-Type: application/vnd.api+json
381 |
382 | {
383 | "data": {
384 | "type": "computer",
385 | "id": "1",
386 | "attributes": {
387 | "serial": "Amstrad 2"
388 | },
389 | "relationships": {
390 | "owner": {
391 | "links": {
392 | "related": "/computers/1/owner",
393 | "self": "/computers/1/relationships/owner"
394 | }
395 | }
396 | },
397 | "links": {
398 | "self": "/computers/1"
399 | }
400 | },
401 | "links": {
402 | "self": "/computers/1"
403 | },
404 | "jsonapi": {
405 | "version": "1.0"
406 | }
407 | }
408 |
409 | Delete object
410 | ~~~~~~~~~~~~~
411 |
412 | Request:
413 |
414 | .. sourcecode:: http
415 |
416 | DELETE /computers/1 HTTP/1.1
417 | Accept: application/vnd.api+json
418 |
419 | Response:
420 |
421 | .. sourcecode:: http
422 |
423 | HTTP/1.1 200 OK
424 | Content-Type: application/vnd.api+json
425 |
426 | {
427 | "meta": {
428 | "Object successful deleted"
429 | },
430 | "jsonapi": {
431 | "version": "1.0"
432 | }
433 | }
434 |
435 | Relationships
436 | -------------
437 |
438 | | Now let's use relationships tools. First, create 3 computers named Halo, Nestor and Comodor like in previous example.
439 | |
440 | | Done ?
441 | | Ok. So let's continue this tutorial.
442 | |
443 | | We assume that Halo has id: 2, Nestor id: 3 and Comodor has id: 4.
444 |
445 | Create object with related object(s)
446 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
447 |
448 | Request:
449 |
450 | .. sourcecode:: http
451 |
452 | POST /persons?include=computers HTTP/1.1
453 | Content-Type: application/vnd.api+json
454 | Accept: application/vnd.api+json
455 |
456 | {
457 | "data": {
458 | "type": "person",
459 | "attributes": {
460 | "name": "John",
461 | "email": "john@gmail.com",
462 | "birth_date": "1990-12-18"
463 | },
464 | "relationships": {
465 | "computers": {
466 | "data": [
467 | {
468 | "type": "computer",
469 | "id": "1"
470 | }
471 | ]
472 | }
473 | }
474 | }
475 | }
476 |
477 | Response:
478 |
479 | .. sourcecode:: http
480 |
481 | HTTP/1.1 201 Created
482 | Content-Type: application/vnd.api+json
483 |
484 | {
485 | "data": {
486 | "type": "person",
487 | "id": "1",
488 | "attributes": {
489 | "display_name": "JOHN ",
490 | "birth_date": "1990-12-18"
491 | },
492 | "links": {
493 | "self": "/persons/1"
494 | },
495 | "relationships": {
496 | "computers": {
497 | "data": [
498 | {
499 | "id": "1",
500 | "type": "computer"
501 | }
502 | ],
503 | "links": {
504 | "related": "/persons/1/computers",
505 | "self": "/persons/1/relationships/computers"
506 | }
507 | }
508 | },
509 | },
510 | "included": [
511 | {
512 | "type": "computer",
513 | "id": "1",
514 | "attributes": {
515 | "serial": "Amstrad"
516 | },
517 | "links": {
518 | "self": "/computers/1"
519 | },
520 | "relationships": {
521 | "owner": {
522 | "links": {
523 | "related": "/computers/1/owner",
524 | "self": "/computers/1/relationships/owner"
525 | }
526 | }
527 | }
528 | }
529 | ],
530 | "jsonapi": {
531 | "version": "1.0"
532 | },
533 | "links": {
534 | "self": "/persons/1"
535 | }
536 | }
537 |
538 | You can see that I have added the querystring parameter "include" to the url
539 |
540 | .. sourcecode:: http
541 |
542 | POST /persons?include=computers HTTP/1.1
543 |
544 | Thanks to this parameter, related computers details are included to the result. If you want to learn more: :ref:`include_related_objects`
545 |
546 | Update object and his relationships
547 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
548 |
549 | Now John sell his Amstrad and buy a new computer named Nestor (id: 3). So we want to link this new computer to John. John have also made a mistake in his birth_date so let's update this 2 things in the same time.
550 |
551 | Request:
552 |
553 | .. sourcecode:: http
554 |
555 | PATCH /persons/1?include=computers HTTP/1.1
556 | Content-Type: application/vnd.api+json
557 | Accept: application/vnd.api+json
558 |
559 | {
560 | "data": {
561 | "type": "person",
562 | "id": "1",
563 | "attributes": {
564 | "birth_date": "1990-10-18"
565 | },
566 | "relationships": {
567 | "computers": {
568 | "data": [
569 | {
570 | "type": "computer",
571 | "id": "3"
572 | }
573 | ]
574 | }
575 | }
576 | }
577 | }
578 |
579 | Response:
580 |
581 | .. sourcecode:: http
582 |
583 | HTTP/1.1 200 OK
584 | Content-Type: application/vnd.api+json
585 |
586 | {
587 | "data": {
588 | "type": "person",
589 | "id": "1",
590 | "attributes": {
591 | "display_name": "JOHN ",
592 | "birth_date": "1990-10-18",
593 | },
594 | "links": {
595 | "self": "/persons/1"
596 | },
597 | "relationships": {
598 | "computers": {
599 | "data": [
600 | {
601 | "id": "3",
602 | "type": "computer"
603 | }
604 | ],
605 | "links": {
606 | "related": "/persons/1/computers",
607 | "self": "/persons/1/relationship/computers"
608 | }
609 | }
610 | },
611 | },
612 | "included": [
613 | {
614 | "type": "computer",
615 | "id": "3",
616 | "attributes": {
617 | "serial": "Nestor"
618 | },
619 | "relationships": {
620 | "owner": {
621 | "links": {
622 | "related": "/computers/3/owner",
623 | "self": "/computers/3/relationships/owner"
624 | }
625 | }
626 | },
627 | "links": {
628 | "self": "/computers/3"
629 | }
630 | }
631 | ],
632 | "links": {
633 | "self": "/persons/2"
634 | },
635 | "jsonapi": {
636 | "version": "1.0"
637 | }
638 | }
639 |
640 | Create relationship
641 | ~~~~~~~~~~~~~~~~~~~
642 |
643 | Now John buy a new computer named Comodor so let's link it to John.
644 |
645 | Request:
646 |
647 | .. sourcecode:: http
648 |
649 | POST /persons/1/relationships/computers HTTP/1.1
650 | Content-Type: application/vnd.api+json
651 | Accept: application/vnd.api+json
652 |
653 | {
654 | "data": [
655 | {
656 | "type": "computer",
657 | "id": "4"
658 | }
659 | ]
660 | }
661 |
662 | Response:
663 |
664 | .. sourcecode:: http
665 |
666 | HTTP/1.1 200 OK
667 | Content-Type: application/vnd.api+json
668 |
669 | {
670 | "data": {
671 | "type": "person",
672 | "id": "1",
673 | "attributes": {
674 | "display_name": "JOHN ",
675 | "birth_date": "1990-10-18"
676 | },
677 | "relationships": {
678 | "computers": {
679 | "data": [
680 | {
681 | "id": "3",
682 | "type": "computer"
683 | },
684 | {
685 | "id": "4",
686 | "type": "computer"
687 | }
688 | ],
689 | "links": {
690 | "related": "/persons/1/computers",
691 | "self": "/persons/1/relationships/computers"
692 | }
693 | }
694 | },
695 | "links": {
696 | "self": "/persons/1"
697 | }
698 | },
699 | "included": [
700 | {
701 | "type": "computer",
702 | "id": "3",
703 | "attributes": {
704 | "serial": "Nestor"
705 | },
706 | "relationships": {
707 | "owner": {
708 | "links": {
709 | "related": "/computers/3/owner",
710 | "self": "/computers/3/relationships/owner"
711 | }
712 | }
713 | },
714 | "links": {
715 | "self": "/computers/3"
716 | }
717 | },
718 | {
719 | "type": "computer",
720 | "id": "4",
721 | "attributes": {
722 | "serial": "Comodor"
723 | },
724 | "relationships": {
725 | "owner": {
726 | "links": {
727 | "related": "/computers/4/owner",
728 | "self": "/computers/4/relationships/owner"
729 | }
730 | }
731 | },
732 | "links": {
733 | "self": "/computers/4"
734 | }
735 | }
736 | ],
737 | "links": {
738 | "self": "/persons/1/relationships/computers"
739 | },
740 | "jsonapi": {
741 | "version": "1.0"
742 | }
743 | }
744 |
745 |
746 | Delete relationship
747 | ~~~~~~~~~~~~~~~~~~~
748 |
749 | Now John sell his old Nestor computer so let's unlink it from John.
750 |
751 | Request:
752 |
753 | .. sourcecode:: http
754 |
755 | DELETE /persons/1/relationships/computers HTTP/1.1
756 | Content-Type: application/vnd.api+json
757 | Accept: application/vnd.api+json
758 |
759 | {
760 | "data": [
761 | {
762 | "type": "computer",
763 | "id": "3"
764 | }
765 | ]
766 | }
767 |
768 | Response:
769 |
770 | .. sourcecode:: http
771 |
772 | HTTP/1.1 200 OK
773 | Content-Type: application/vnd.api+json
774 |
775 | {
776 | "data": {
777 | "type": "person",
778 | "id": "1",
779 | "attributes": {
780 | "display_name": "JOHN ",
781 | "birth_date": "1990-10-18"
782 | },
783 | "relationships": {
784 | "computers": {
785 | "data": [
786 | {
787 | "id": "4",
788 | "type": "computer"
789 | }
790 | ],
791 | "links": {
792 | "related": "/persons/1/computers",
793 | "self": "/persons/1/relationships/computers"
794 | }
795 | }
796 | },
797 | "links": {
798 | "self": "/persons/1"
799 | }
800 | },
801 | "included": [
802 | {
803 | "type": "computer",
804 | "id": "4",
805 | "attributes": {
806 | "serial": "Comodor"
807 | },
808 | "relationships": {
809 | "owner": {
810 | "links": {
811 | "related": "/computers/4/owner",
812 | "self": "/computers/4/relationships/owner"
813 | }
814 | }
815 | },
816 | "links": {
817 | "self": "/computers/4"
818 | }
819 | }
820 | ],
821 | "links": {
822 | "self": "/persons/1/relationships/computers"
823 | },
824 | "jsonapi": {
825 | "version": "1.0"
826 | }
827 | }
828 |
829 | If you want to see more examples to go `JSON API 1.0 specification `_
--------------------------------------------------------------------------------
/docs/resource_manager.rst:
--------------------------------------------------------------------------------
1 | .. _resource_manager:
2 |
3 | Resource Manager
4 | ================
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | Resource manager is the link between your logical data abstraction, your data layer and optionally other softwares. It is the place where logic management of your resource is located.
9 |
10 | Flask-REST-JSONAPI provides 3 kinds of resource manager with default methods implementation according to JSONAPI 1.0 specification:
11 |
12 | * **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
13 | * **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
14 | * **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.
15 |
16 | You can rewrite each default methods implementation to make custom work. If you rewrite all default methods implementation of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attribute of your resource manager.
17 |
18 | Required attributes
19 | -------------------
20 |
21 | If you want to use one of the resource manager default method implementation you have to set 2 required attributes in your resource manager: schema and data_layer.
22 |
23 | :schema: the logical data abstraction used by the resource manager. It must be a class inherited from marshmallow_jsonapi.schema.Schema.
24 | :data_layer: data layer information used to initialize your data layer (If you want to learn more: :ref:`data_layer`)
25 |
26 | Example:
27 |
28 | .. code-block:: python
29 |
30 | from flask_rest_jsonapi import ResourceList
31 | from your_project.schemas import PersonSchema
32 | from your_project.models import Person
33 | from your_project.extensions import db
34 |
35 | class PersonList(ResourceList):
36 | schema = PersonSchema
37 | data_layer = {'session': db.session,
38 | 'model': Person}
39 |
40 | Optional attributes
41 | -------------------
42 |
43 | All resource mangers are inherited from flask.views.MethodView so you can provides optional attributes to your resource manager:
44 |
45 | :methods: a list of methods this resource manager can handle. If you don't specify any method, all methods are handled.
46 | :decorators: a tuple of decorators plugged to all methods that the resource manager can handle
47 |
48 | You can provide default schema kwargs for each resource manager methods with this optional attributes:
49 |
50 | * **get_schema_kwargs**: a dict of default schema kwargs in get method
51 | * **post_schema_kwargs**: a dict of default schema kwargs in post method
52 | * **patch_schema_kwargs**: a dict of default schema kwargs in patch method
53 | * **delete_schema_kwargs**: a dict of default schema kwargs in delete method
54 |
55 | Each method of a resource manager got a pre and post process methods that take view args and kwargs as parameter for the pre process methods and the result of the method as parameter for the post process method. Thanks to this you can make custom work before and after the method process. Availables rewritable methods are:
56 |
57 | :before_get: pre process method of the get method
58 | :after_get: post process method of the get method
59 | :before_post: pre process method of the post method
60 | :after_post: post process method of the post method
61 | :before_patch: pre process method of the patch method
62 | :after_patch: post process method of the patch method
63 | :before_delete: pre process method of the delete method
64 | :after_delete: post process method of the delete method
65 |
66 | Example:
67 |
68 | .. code-block:: python
69 |
70 | from flask_rest_jsonapi import ResourceDetail
71 | from your_project.schemas import PersonSchema
72 | from your_project.models import Person
73 | from your_project.security import login_required
74 | from your_project.extensions import db
75 |
76 | class PersonList(ResourceDetail):
77 | schema = PersonSchema
78 | data_layer = {'session': db.session,
79 | 'model': Person}
80 | methods = ['GET', 'PATCH']
81 | decorators = (login_required, )
82 | get_schema_kwargs = {'only': ('name', )}
83 |
84 | def before_patch(*args, **kwargs):
85 | """Make custom work here. View args and kwargs are provided as parameter
86 | """
87 |
88 | def after_patch(result):
89 | """Make custom work here. Add something to the result of the view.
90 | """
91 |
92 | ResourceList
93 | ------------
94 |
95 | ResourceList manager has his own optional attributes:
96 |
97 | :view_kwargs: if you set this flag to True view kwargs will be used to compute the list url. If you have a list url pattern with parameter like that: /persons//computers you have to set this flag to True
98 |
99 | Example:
100 |
101 | .. code-block:: python
102 |
103 | from flask_rest_jsonapi import ResourceList
104 | from your_project.schemas import PersonSchema
105 | from your_project.models import Person
106 | from your_project.extensions import db
107 |
108 | class PersonList(ResourceList):
109 | schema = PersonSchema
110 | data_layer = {'session': db.session,
111 | 'model': Person}
112 |
113 | This minimal ResourceList configuration provides GET and POST interface to retrieve a collection of objects and create an object with all powerful features like pagination, sorting, sparse fieldsets, filtering and including related objects.
114 |
115 | If your schema has relationship(s) field(s) you can create an object and link related object(s) to it in the same time. If you want to see example go to :ref:`quickstart`.
116 |
117 | ResourceDetail
118 | --------------
119 |
120 | Example:
121 |
122 | .. code-block:: python
123 |
124 | from flask_rest_jsonapi import ResourceDetail
125 | from your_project.schemas import PersonSchema
126 | from your_project.models import Person
127 | from your_project.extensions import db
128 |
129 | class PersonDetail(ResourceDetail):
130 | schema = PersonSchema
131 | data_layer = {'session': db.session,
132 | 'model': Person}
133 |
134 | This minimal ResourceDetail configuration provides GET, PATCH and DELETE interface to retrieve details of objects, update an objects and delete an object with all powerful features like sparse fieldsets and including related objects.
135 |
136 | If your schema has relationship(s) field(s) you can update an object and also update his link(s) to related object(s) in the same time. If you want to see example go to :ref:`quickstart`.
137 |
138 | ResourceRelationship
139 | --------------------
140 |
141 | Example:
142 |
143 | .. code-block:: python
144 |
145 | from flask_rest_jsonapi import ResourceRelationship
146 | from your_project.schemas import PersonSchema
147 | from your_project.models import Person
148 | from your_project.extensions import db
149 |
150 | class PersonRelationship(ResourceRelationship):
151 | schema = PersonSchema
152 | data_layer = {'session': db.session,
153 | 'model': Person}
154 |
155 | This minimal ResourceRelationship configuration provides GET, POST, PATCH and DELETE interface to retrieve relationship(s), create relationsip(s), update relationship(s) and delete relationship(s) between objects with all powerful features like sparse fieldsets and including related objects.
156 |
--------------------------------------------------------------------------------
/docs/routing.rst:
--------------------------------------------------------------------------------
1 | .. _routing:
2 |
3 | Routing
4 | =======
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | The routing system is very simple and fits this pattern ::
9 |
10 | api.route(, , , , ...)
11 |
12 | Example:
13 |
14 | .. code-block:: python
15 |
16 | # all required imports are not displayed in this example
17 | from flask_rest_jsonapi import Api
18 |
19 | api = Api()
20 | api.route(PersonList, 'person_list', '/persons')
21 | api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
22 | api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
23 | api.route(ComputerList, 'computer_list', '/computers', '/persons//computers')
24 | api.route(ComputerDetail, 'computer_detail', '/computers/')
25 | api.route(ComputerRelationship, 'computer_person', '/computers//relationships/owner')
26 |
--------------------------------------------------------------------------------
/docs/sorting.rst:
--------------------------------------------------------------------------------
1 | .. _sorting:
2 |
3 | Sorting
4 | =======
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | You can sort results with querystring parameter named "sort"
9 |
10 | .. note::
11 |
12 | Examples are not urlencoded for a better readability
13 |
14 | Example:
15 |
16 | .. sourcecode:: http
17 |
18 | GET /persons?sort=name HTTP/1.1
19 | Accept: application/vnd.api+json
20 |
21 | Muliple sort
22 | ------------
23 |
24 | You can sort on multiple fields like that:
25 |
26 | .. sourcecode:: http
27 |
28 | GET /persons?sort=name,birth_date HTTP/1.1
29 | Accept: application/vnd.api+json
30 |
31 | Descendant sort
32 | ---------------
33 |
34 | You can make desc sort with the character "-" like that:
35 |
36 | .. sourcecode:: http
37 |
38 | GET /persons?sort=-name HTTP/1.1
39 | Accept: application/vnd.api+json
40 |
41 | Muliple sort + Descendant sort
42 | ------------------------------
43 |
44 | Of course, you can combine both like that:
45 |
46 | .. sourcecode:: http
47 |
48 | GET /persons?sort=-name,birth_date HTTP/1.1
49 | Accept: application/vnd.api+json
50 |
--------------------------------------------------------------------------------
/docs/sparse_fieldsets.rst:
--------------------------------------------------------------------------------
1 | .. _sparse_fieldsets:
2 |
3 | Sparse fieldsets
4 | ================
5 |
6 | .. currentmodule:: flask_rest_jsonapi
7 |
8 | You can restrict the fields returned by api with the querystring parameter called "fields". It is very useful for performance purpose because fields not returned are not resolved by api. You can use "fields" parameter on any kind of route (classical CRUD route or relationships route) and any kind of http methods as long as method return data.
9 |
10 | .. note::
11 |
12 | Examples are not urlencoded for a better readability
13 |
14 | The syntax of a fields is like that ::
15 |
16 | ?fields[]=
17 |
18 | Example:
19 |
20 | .. sourcecode:: http
21 |
22 | GET /persons?fields[person]=display_name HTTP/1.1
23 | Accept: application/vnd.api+json
24 |
25 | In this example person's display_name is the only field returned by the api. No relationships links are returned so the response is very fast because api doesn't have to compute relationships link and it is a very costly work.
26 |
27 | You can manage returned fields for the entire response even for included objects
28 |
29 | Example:
30 |
31 | If you don't want to compute relationships links for included computers of a person you can do something like that
32 |
33 | .. sourcecode:: http
34 |
35 | GET /persons/1?include=computers&fields[computer]=serial HTTP/1.1
36 | Accept: application/vnd.api+json
37 |
38 | And of course you can combine both like that:
39 |
40 | Example:
41 |
42 | .. sourcecode:: http
43 |
44 | GET /persons/1?include=computers&fields[computer]=serial&fields[person]=name,computers HTTP/1.1
45 | Accept: application/vnd.api+json
46 |
47 | .. warning::
48 |
49 | If you want to use both "fields" and "include" don't forget to specify the name of the relationship in fields; if you don't the include wont work.
50 |
--------------------------------------------------------------------------------
/examples/api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from flask import Flask
4 | from flask_rest_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
5 | from flask_rest_jsonapi.exceptions import ObjectNotFound
6 | from flask_sqlalchemy import SQLAlchemy
7 | from sqlalchemy.orm.exc import NoResultFound
8 | from marshmallow_jsonapi.flask import Schema, Relationship
9 | from marshmallow_jsonapi import fields
10 |
11 | # Create the Flask application
12 | app = Flask(__name__)
13 | app.config['DEBUG'] = True
14 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
15 |
16 |
17 | # Initialize SQLAlchemy
18 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
19 | db = SQLAlchemy(app)
20 |
21 |
22 | # Create data storage
23 | class Person(db.Model):
24 | id = db.Column(db.Integer, primary_key=True)
25 | name = db.Column(db.String)
26 | email = db.Column(db.String)
27 | birth_date = db.Column(db.Date)
28 | password = db.Column(db.String)
29 |
30 |
31 | class Computer(db.Model):
32 | id = db.Column(db.Integer, primary_key=True)
33 | serial = db.Column(db.String)
34 | person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
35 | person = db.relationship('Person', backref=db.backref('computers'))
36 |
37 | db.create_all()
38 |
39 |
40 | # Create logical data abstraction (same as data storage for this first example)
41 | class PersonSchema(Schema):
42 | class Meta:
43 | type_ = 'person'
44 | self_view = 'person_detail'
45 | self_view_kwargs = {'id': ''}
46 | self_view_many = 'person_list'
47 |
48 | id = fields.Str(dump_only=True)
49 | name = fields.Str(requried=True, load_only=True)
50 | email = fields.Email(load_only=True)
51 | birth_date = fields.Date()
52 | display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
53 | computers = Relationship(self_view='person_computers',
54 | self_view_kwargs={'id': ''},
55 | related_view='computer_list',
56 | related_view_kwargs={'id': ''},
57 | many=True,
58 | schema='ComputerSchema',
59 | type_='computer')
60 |
61 |
62 | class ComputerSchema(Schema):
63 | class Meta:
64 | type_ = 'computer'
65 | self_view = 'computer_detail'
66 | self_view_kwargs = {'id': ''}
67 |
68 | id = fields.Str(dump_only=True)
69 | serial = fields.Str(requried=True)
70 | owner = Relationship(attribute='person',
71 | self_view='computer_person',
72 | self_view_kwargs={'id': ''},
73 | related_view='person_detail',
74 | related_view_kwargs={'computer_id': ''},
75 | schema='PersonSchema',
76 | type_='person')
77 |
78 |
79 | # Create resource managers
80 | class PersonList(ResourceList):
81 | schema = PersonSchema
82 | data_layer = {'session': db.session,
83 | 'model': Person}
84 |
85 |
86 | class PersonDetail(ResourceDetail):
87 | def before_get_object(self, view_kwargs):
88 | if view_kwargs.get('computer_id') is not None:
89 | try:
90 | computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
91 | except NoResultFound:
92 | raise ObjectNotFound({'parameter': 'computer_id'},
93 | "Computer: {} not found".format(view_kwargs['computer_id']))
94 | else:
95 | if computer.person is not None:
96 | view_kwargs['id'] = computer.person.id
97 | else:
98 | view_kwargs['id'] = None
99 |
100 | schema = PersonSchema
101 | data_layer = {'session': db.session,
102 | 'model': Person,
103 | 'methods': {'before_get_object': before_get_object}}
104 |
105 |
106 | class PersonRelationship(ResourceRelationship):
107 | schema = PersonSchema
108 | data_layer = {'session': db.session,
109 | 'model': Person}
110 |
111 |
112 | class ComputerList(ResourceList):
113 | def query(self, view_kwargs):
114 | query_ = self.session.query(Computer)
115 | if view_kwargs.get('id') is not None:
116 | try:
117 | self.session.query(Person).filter_by(id=view_kwargs['id']).one()
118 | except NoResultFound:
119 | raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
120 | else:
121 | query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
122 | return query_
123 |
124 | def before_create_object(self, data, view_kwargs):
125 | if view_kwargs.get('id') is not None:
126 | person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
127 | data['person_id'] = person.id
128 |
129 | schema = ComputerSchema
130 | data_layer = {'session': db.session,
131 | 'model': Computer,
132 | 'methods': {'query': query,
133 | 'before_create_object': before_create_object}}
134 |
135 |
136 | class ComputerDetail(ResourceDetail):
137 | schema = ComputerSchema
138 | data_layer = {'session': db.session,
139 | 'model': Computer}
140 |
141 |
142 | class ComputerRelationship(ResourceRelationship):
143 | schema = ComputerSchema
144 | data_layer = {'session': db.session,
145 | 'model': Computer}
146 |
147 |
148 | # Create endpoints
149 | api = Api(app)
150 | api.route(PersonList, 'person_list', '/persons')
151 | api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
152 | api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
153 | api.route(ComputerList, 'computer_list', '/computers', '/persons//computers')
154 | api.route(ComputerDetail, 'computer_detail', '/computers/')
155 | api.route(ComputerRelationship, 'computer_person', '/computers//relationships/owner')
156 |
157 | if __name__ == '__main__':
158 | # Start application
159 | app.run(debug=True)
160 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from flask_rest_jsonapi.api import Api
4 | from flask_rest_jsonapi.resource import ResourceList, ResourceDetail, ResourceRelationship
5 | from flask_rest_jsonapi.exceptions import JsonApiException
6 |
7 | __all__ = [
8 | 'Api',
9 | 'ResourceList',
10 | 'ResourceDetail',
11 | 'ResourceRelationship',
12 | 'JsonApiException'
13 | ]
14 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import inspect
4 | from functools import wraps
5 |
6 | from flask_rest_jsonapi.resource import ResourceList
7 |
8 |
9 | class Api(object):
10 |
11 | def __init__(self, app=None, blueprint=None, decorators=None):
12 | self.app = app
13 | self.blueprint = blueprint
14 | self.resources = []
15 | self.resource_registry = []
16 | self.decorators = decorators or tuple()
17 |
18 | def init_app(self, app=None, blueprint=None):
19 | """Update flask application with our api
20 |
21 | :param Application app: a flask application
22 | """
23 | if app is not None:
24 | self.app = app
25 |
26 | if blueprint is not None:
27 | self.blueprint = blueprint
28 |
29 | for resource in self.resources:
30 | self.route(resource['resource'],
31 | resource['view'],
32 | *resource['urls'],
33 | url_rule_options=resource['url_rule_options'])
34 |
35 | if self.blueprint is not None:
36 | self.app.register_blueprint(self.blueprint)
37 |
38 | def route(self, resource, view, *urls, **kwargs):
39 | """Create an api view.
40 |
41 | :param Resource resource: a resource class inherited from flask_rest_jsonapi.resource.Resource
42 | :param str view: the view name
43 | :param list urls: the urls of the view
44 | :param dict kwargs: additional options of the route
45 | """
46 | resource.view = view
47 | view_func = resource.as_view(view)
48 | url_rule_options = kwargs.get('url_rule_options') or dict()
49 |
50 | for decorator in self.decorators:
51 | if hasattr(resource, 'decorators'):
52 | resource.decorators += self.decorators
53 | else:
54 | resource.decorators = self.decorators
55 |
56 | if self.blueprint is not None:
57 | resource.view = '.'.join([self.blueprint.name, resource.view])
58 | for url in urls:
59 | self.blueprint.add_url_rule(url, view_func=view_func, **url_rule_options)
60 | elif self.app is not None:
61 | for url in urls:
62 | self.app.add_url_rule(url, view_func=view_func, **url_rule_options)
63 | else:
64 | self.resources.append({'resource': resource,
65 | 'view': view,
66 | 'urls': urls,
67 | 'url_rule_options': url_rule_options})
68 |
69 | self.resource_registry.append(resource)
70 |
71 | def oauth_manager(self, oauth_manager):
72 | """Use the oauth manager to enable oauth for API
73 |
74 | :param oauth_manager: the oauth manager
75 | """
76 | for resource in self.resource_registry:
77 | if getattr(resource, 'disable_oauth', None) is not True:
78 | for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
79 | scope = self.get_scope(resource, method)
80 | setattr(resource,
81 | method.lower(),
82 | oauth_manager.require_oauth(scope)(getattr(resource, method.lower())))
83 |
84 | def scope_setter(self, func):
85 | """Plug oauth scope setter function to the API
86 |
87 | :param callable func: the callable to use a scope getter
88 | """
89 | self.get_scope = func
90 |
91 | @staticmethod
92 | def get_scope(resource, method):
93 | """Compute the name of the scope for oauth
94 |
95 | :param Resource resource: the resource manager
96 | :param str method: an http method
97 | :return str: the name of the scope
98 | """
99 | if ResourceList in inspect.getmro(resource) and method == 'GET':
100 | prefix = 'list'
101 | else:
102 | method_to_prefix = {'GET': 'get',
103 | 'POST': 'create',
104 | 'PATCH': 'update',
105 | 'DELETE': 'delete'}
106 | prefix = method_to_prefix[method]
107 |
108 | return '_'.join([prefix, resource.schema.opts.type_])
109 |
110 | def permission_manager(self, permission_manager):
111 | """Use permission manager to enable permission for API
112 |
113 | :param callable permission_manager: the permission manager
114 | """
115 | self.check_permissions = permission_manager
116 |
117 | for resource in self.resource_registry:
118 | if getattr(resource, 'disable_permission', None) is not True:
119 | for method in getattr(resource, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
120 | setattr(resource,
121 | method.lower(),
122 | self.has_permission()(getattr(resource, method.lower())))
123 |
124 | def has_permission(self, *args, **kwargs):
125 | """Decorator used to check permissions before to call resource manager method
126 | """
127 | def wrapper(view):
128 | if getattr(view, '_has_permissions_decorator', False) is True:
129 | return view
130 |
131 | @wraps(view)
132 | def decorated(*view_args, **view_kwargs):
133 | return self.check_permissions(view, view_args, view_kwargs, *args, **kwargs)
134 | decorated._has_permissions_decorator = True
135 | return decorated
136 | return wrapper
137 |
138 | @staticmethod
139 | def check_permissions(view, view_args, view_kwargs, *args, **kwargs):
140 | """The function use to check permissions
141 |
142 | :param callable view: the view
143 | :param list view_args: view args
144 | :param dict view_kwargs: view kwargs
145 | :param list args: decorator args
146 | :param dict kwargs: decorator kwargs
147 | """
148 | raise NotImplementedError
149 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/constants.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # default number of items for pagination
4 | DEFAULT_PAGE_SIZE = 20
5 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/data_layers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossasia/flask-rest-jsonapi/a03408bffd5ef96bf3b8abe3a30d147db46fbe47/flask_rest_jsonapi/data_layers/__init__.py
--------------------------------------------------------------------------------
/flask_rest_jsonapi/data_layers/alchemy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from flask import current_app
4 | from sqlalchemy.orm.exc import NoResultFound
5 | from sqlalchemy.orm.collections import InstrumentedList
6 | from sqlalchemy.inspection import inspect
7 | from flask import request
8 | from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE
9 | from flask_rest_jsonapi.data_layers.base import BaseDataLayer
10 | from flask_rest_jsonapi.exceptions import RelationNotFound, RelatedObjectNotFound, JsonApiException,\
11 | InvalidSort, ObjectNotFound
12 | from flask_rest_jsonapi.data_layers.filtering.alchemy import create_filters
13 | from flask_rest_jsonapi.schema import get_relationships
14 |
15 |
16 | class SqlalchemyDataLayer(BaseDataLayer):
17 |
18 | def __init__(self, kwargs):
19 | super(SqlalchemyDataLayer, self).__init__(kwargs)
20 |
21 | if not hasattr(self, 'session'):
22 | raise Exception("You must provide a session in data_layer_kwargs to use sqlalchemy data layer in {}"
23 | .format(self.resource.__name__))
24 | if not hasattr(self, 'model'):
25 | raise Exception("You must provide a model in data_layer_kwargs to use sqlalchemy data layer in {}"
26 | .format(self.resource.__name__))
27 |
28 | def create_object(self, data, view_kwargs):
29 | """Create an object through sqlalchemy
30 |
31 | :param dict data: the data validated by marshmallow
32 | :param dict view_kwargs: kwargs from the resource view
33 | :return DeclarativeMeta: an object from sqlalchemy
34 | """
35 | self.before_create_object(data, view_kwargs)
36 |
37 | relationship_fields = get_relationships(self.resource.schema)
38 | obj = self.model(**{key: value for (key, value) in data.items() if key not in relationship_fields})
39 | self.apply_relationships(data, obj)
40 |
41 | self.session.add(obj)
42 | try:
43 | self.session.commit()
44 | except Exception as e:
45 | self.session.rollback()
46 | if current_app.config.get('PROPOGATE_ERROR') == True:
47 | raise JsonApiException({'pointer': '/data'}, "Object creation error: " + str(e))
48 | else:
49 | raise JsonApiException({'pointer': '/data'}, "Object creation error")
50 |
51 | self.after_create_object(obj, data, view_kwargs)
52 |
53 | return obj
54 |
55 | def get_object(self, view_kwargs, get_trashed=False):
56 | """Retrieve an object through sqlalchemy
57 |
58 | :params dict view_kwargs: kwargs from the resource view
59 | :return DeclarativeMeta: an object from sqlalchemy
60 | """
61 | self.before_get_object(view_kwargs)
62 |
63 | id_field = getattr(self, 'id_field', inspect(self.model).primary_key[0].name)
64 | try:
65 | filter_field = getattr(self.model, id_field)
66 | except Exception:
67 | raise Exception("{} has no attribute {}".format(self.model.__name__, id_field))
68 |
69 | url_field = getattr(self, 'url_field', 'id')
70 | filter_value = view_kwargs[url_field]
71 | try:
72 | if not getattr(self, 'resource', None) or 'deleted_at' not in self.resource.schema._declared_fields or get_trashed or current_app.config.get('SOFT_DELETE') != True:
73 | obj = self.session.query(self.model).filter(filter_field == filter_value).one()
74 | else:
75 | obj = self.session.query(self.model).filter(filter_field == filter_value).filter_by(deleted_at=None).one()
76 | except NoResultFound:
77 | obj = None
78 |
79 | self.after_get_object(obj, view_kwargs)
80 |
81 | return obj
82 |
83 | def get_collection(self, qs, view_kwargs):
84 | """Retrieve a collection of objects through sqlalchemy
85 |
86 | :param QueryStringManager qs: a querystring manager to retrieve information from url
87 | :param dict view_kwargs: kwargs from the resource view
88 | :return tuple: the number of object and the list of objects
89 | """
90 | self.before_get_collection(qs, view_kwargs)
91 | if 'deleted_at' not in self.resource.schema._declared_fields or request.args.get('get_trashed') == 'true' or current_app.config['SOFT_DELETE'] is False:
92 | query = self.query(view_kwargs)
93 | else:
94 | query = self.query(view_kwargs).filter(self.model.deleted_at == None)
95 |
96 | if qs.filters:
97 | query = self.filter_query(query, qs.filters, self.model)
98 |
99 | if qs.sorting:
100 | query = self.sort_query(query, qs.sorting)
101 |
102 | object_count = query.count()
103 |
104 | query = self.paginate_query(query, qs.pagination)
105 |
106 | collection = query.all()
107 |
108 | self.after_get_collection(collection, qs, view_kwargs)
109 |
110 | return object_count, collection
111 |
112 | def update_object(self, obj, data, view_kwargs):
113 | """Update an object through sqlalchemy
114 |
115 | :param DeclarativeMeta obj: an object from sqlalchemy
116 | :param dict data: the data validated by marshmallow
117 | :param dict view_kwargs: kwargs from the resource view
118 | :return boolean: True if object have changed else False
119 | """
120 | if obj is None:
121 | url_field = getattr(self, 'url_field', 'id')
122 | filter_value = view_kwargs[url_field]
123 | raise ObjectNotFound({'parameter': url_field},
124 | '{}: {} not found'.format(self.model.__name__, filter_value))
125 |
126 | self.before_update_object(obj, data, view_kwargs)
127 |
128 | relationship_fields = get_relationships(self.resource.schema)
129 | for key, value in data.items():
130 | if hasattr(obj, key) and key not in relationship_fields:
131 | setattr(obj, key, value)
132 |
133 | self.apply_relationships(data, obj)
134 |
135 | try:
136 | self.session.commit()
137 | except Exception as e:
138 | self.session.rollback()
139 | if current_app.config.get('PROPOGATE_ERROR') == True:
140 | raise JsonApiException({'pointer': '/data'}, "Update object error: " + str(e))
141 | else:
142 | raise JsonApiException({'pointer': '/data'}, "Update object error")
143 |
144 | self.after_update_object(obj, data, view_kwargs)
145 |
146 | def delete_object(self, obj, view_kwargs):
147 | """Delete an object through sqlalchemy
148 |
149 | :param DeclarativeMeta item: an item from sqlalchemy
150 | :param dict view_kwargs: kwargs from the resource view
151 | """
152 | if obj is None:
153 | url_field = getattr(self, 'url_field', 'id')
154 | filter_value = view_kwargs[url_field]
155 | raise ObjectNotFound({'parameter': url_field},
156 | '{}: {} not found'.format(self.model.__name__, filter_value))
157 |
158 | self.before_delete_object(obj, view_kwargs)
159 |
160 | self.session.delete(obj)
161 | try:
162 | self.session.commit()
163 | except Exception as e:
164 | self.session.rollback()
165 | if current_app.config.get('PROPOGATE_ERROR') == True:
166 | raise JsonApiException('', "Delete object error: " + str(e))
167 | else:
168 | raise JsonApiException('', "Delete object error")
169 |
170 | self.after_delete_object(obj, view_kwargs)
171 |
172 | def create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
173 | """Create a relationship
174 |
175 | :param dict json_data: the request params
176 | :param str relationship_field: the model attribute used for relationship
177 | :param str related_id_field: the identifier field of the related model
178 | :param dict view_kwargs: kwargs from the resource view
179 | :return boolean: True if relationship have changed else False
180 | """
181 | self.before_create_relationship(json_data, relationship_field, related_id_field, view_kwargs)
182 |
183 | obj = self.get_object(view_kwargs)
184 |
185 | if obj is None:
186 | url_field = getattr(self, 'url_field', 'id')
187 | filter_value = view_kwargs[url_field]
188 | raise ObjectNotFound({'parameter': url_field},
189 | '{}: {} not found'.format(self.model.__name__, filter_value))
190 |
191 | if not hasattr(obj, relationship_field):
192 | raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field))
193 |
194 | related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
195 |
196 | updated = False
197 |
198 | if isinstance(json_data['data'], list):
199 | obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)}
200 |
201 | for obj_ in json_data['data']:
202 | if obj_['id'] not in obj_ids:
203 | getattr(obj,
204 | relationship_field).append(self.get_related_object(related_model, related_id_field, obj_))
205 | updated = True
206 | else:
207 | related_object = None
208 |
209 | if json_data['data'] is not None:
210 | related_object = self.get_related_object(related_model, related_id_field, json_data['data'])
211 |
212 | obj_id = getattr(getattr(obj, relationship_field), related_id_field, None)
213 | new_obj_id = getattr(related_object, related_id_field, None)
214 | if obj_id != new_obj_id:
215 | setattr(obj, relationship_field, related_object)
216 | updated = True
217 |
218 | try:
219 | self.session.commit()
220 | except Exception as e:
221 | self.session.rollback()
222 | if current_app.config.get('PROPOGATE_ERROR') == True:
223 | raise JsonApiException('', "Create relationship error: " + str(e))
224 | else:
225 | raise JsonApiException('', "Create relationship error")
226 | self.after_create_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
227 |
228 | return obj, updated
229 |
230 | def get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
231 | """Get a relationship
232 |
233 | :param str relationship_field: the model attribute used for relationship
234 | :param str related_type_: the related resource type
235 | :param str related_id_field: the identifier field of the related model
236 | :param dict view_kwargs: kwargs from the resource view
237 | :return tuple: the object and related object(s)
238 | """
239 | self.before_get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)
240 |
241 | obj = self.get_object(view_kwargs)
242 |
243 | if obj is None:
244 | url_field = getattr(self, 'url_field', 'id')
245 | filter_value = view_kwargs[url_field]
246 | raise ObjectNotFound({'parameter': url_field},
247 | '{}: {} not found'.format(self.model.__name__, filter_value))
248 |
249 | if not hasattr(obj, relationship_field):
250 | raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field))
251 |
252 | related_objects = getattr(obj, relationship_field)
253 |
254 | if related_objects is None:
255 | return obj, related_objects
256 |
257 | self.after_get_relationship(obj, related_objects, relationship_field, related_type_, related_id_field,
258 | view_kwargs)
259 |
260 | if isinstance(related_objects, InstrumentedList):
261 | return obj,\
262 | [{'type': related_type_, 'id': getattr(obj_, related_id_field)} for obj_ in related_objects]
263 | else:
264 | return obj, {'type': related_type_, 'id': getattr(related_objects, related_id_field)}
265 |
266 | def update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
267 | """Update a relationship
268 |
269 | :param dict json_data: the request params
270 | :param str relationship_field: the model attribute used for relationship
271 | :param str related_id_field: the identifier field of the related model
272 | :param dict view_kwargs: kwargs from the resource view
273 | :return boolean: True if relationship have changed else False
274 | """
275 | self.before_update_relationship(json_data, relationship_field, related_id_field, view_kwargs)
276 |
277 | obj = self.get_object(view_kwargs)
278 |
279 | if obj is None:
280 | url_field = getattr(self, 'url_field', 'id')
281 | filter_value = view_kwargs[url_field]
282 | raise ObjectNotFound({'parameter': url_field},
283 | '{}: {} not found'.format(self.model.__name__, filter_value))
284 |
285 | if not hasattr(obj, relationship_field):
286 | raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field))
287 |
288 | related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
289 |
290 | updated = False
291 |
292 | if isinstance(json_data['data'], list):
293 | related_objects = []
294 |
295 | for obj_ in json_data['data']:
296 | related_objects.append(self.get_related_object(related_model, related_id_field, obj_))
297 |
298 | obj_ids = {getattr(obj__, related_id_field) for obj__ in getattr(obj, relationship_field)}
299 | new_obj_ids = {getattr(related_object, related_id_field) for related_object in related_objects}
300 | if obj_ids != new_obj_ids:
301 | setattr(obj, relationship_field, related_objects)
302 | updated = True
303 |
304 | else:
305 | related_object = None
306 |
307 | if json_data['data'] is not None:
308 | related_object = self.get_related_object(related_model, related_id_field, json_data['data'])
309 |
310 | obj_id = getattr(getattr(obj, relationship_field), related_id_field, None)
311 | new_obj_id = getattr(related_object, related_id_field, None)
312 | if obj_id != new_obj_id:
313 | setattr(obj, relationship_field, related_object)
314 | updated = True
315 |
316 | try:
317 | self.session.commit()
318 | except Exception as e:
319 | self.session.rollback()
320 | if current_app.config.get('PROPOGATE_ERROR') == True:
321 | raise JsonApiException('', "Update relationship error: " + str(e))
322 | else:
323 | raise JsonApiException('', "Update relationship error")
324 | self.after_update_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
325 |
326 | return obj, updated
327 |
328 | def delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
329 | """Delete a relationship
330 |
331 | :param dict json_data: the request params
332 | :param str relationship_field: the model attribute used for relationship
333 | :param str related_id_field: the identifier field of the related model
334 | :param dict view_kwargs: kwargs from the resource view
335 | """
336 | self.before_delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)
337 |
338 | obj = self.get_object(view_kwargs)
339 |
340 | if obj is None:
341 | url_field = getattr(self, 'url_field', 'id')
342 | filter_value = view_kwargs[url_field]
343 | raise ObjectNotFound({'parameter': url_field},
344 | '{}: {} not found'.format(self.model.__name__, filter_value))
345 |
346 | if not hasattr(obj, relationship_field):
347 | raise RelationNotFound('', "{} has no attribute {}".format(obj.__class__.__name__, relationship_field))
348 |
349 | related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
350 |
351 | updated = False
352 |
353 | if isinstance(json_data['data'], list):
354 | obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)}
355 |
356 | for obj_ in json_data['data']:
357 | if obj_['id'] in obj_ids:
358 | getattr(obj,
359 | relationship_field).remove(self.get_related_object(related_model, related_id_field, obj_))
360 | updated = True
361 | else:
362 | setattr(obj, relationship_field, None)
363 | updated = True
364 |
365 | try:
366 | self.session.commit()
367 | except Exception as e:
368 | self.session.rollback()
369 | if current_app.config.get('PROPOGATE_ERROR') == True:
370 | raise JsonApiException('', "Delete relationship error: " + str(e))
371 | else:
372 | raise JsonApiException('', "Delete relationship error")
373 |
374 | self.after_delete_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
375 |
376 | return obj, updated
377 |
378 | def get_related_object(self, related_model, related_id_field, obj):
379 | """Get a related object
380 |
381 | :param Model related_model: an sqlalchemy model
382 | :param str related_id_field: the identifier field of the related model
383 | :param DeclarativeMeta obj: the sqlalchemy object to retrieve related objects from
384 | :return DeclarativeMeta: a related object
385 | """
386 | try:
387 | related_object = self.session.query(related_model)\
388 | .filter(getattr(related_model, related_id_field) == obj['id'])\
389 | .one()
390 | except NoResultFound:
391 | raise RelatedObjectNotFound('', "{}.{}: {} not found".format(related_model.__name__,
392 | related_id_field,
393 | obj['id']))
394 |
395 | return related_object
396 |
397 | def apply_relationships(self, data, obj):
398 | """Apply relationship provided by data to obj
399 |
400 | :param dict data: data provided by the client
401 | :param DeclarativeMeta obj: the sqlalchemy object to plug relationships to
402 | :return boolean: True if relationship have changed else False
403 | """
404 | relationships_to_apply = []
405 | relationship_fields = get_relationships(self.resource.schema)
406 | for key, value in data.items():
407 | if key in relationship_fields:
408 | related_model = getattr(obj.__class__, key).property.mapper.class_
409 | related_id_field = self.resource.schema._declared_fields[relationship_fields[key]].id_field
410 |
411 | if isinstance(value, list):
412 | related_objects = []
413 |
414 | for identifier in value:
415 | related_object = self.get_related_object(related_model, related_id_field, {'id': identifier})
416 | related_objects.append(related_object)
417 |
418 | relationships_to_apply.append({'field': key, 'value': related_objects})
419 | else:
420 | related_object = None
421 |
422 | if value is not None:
423 | related_object = self.get_related_object(related_model, related_id_field, {'id': value})
424 |
425 | relationships_to_apply.append({'field': key, 'value': related_object})
426 |
427 | for relationship in relationships_to_apply:
428 | setattr(obj, relationship['field'], relationship['value'])
429 |
430 |
431 | def filter_query(self, query, filter_info, model):
432 | """Filter query according to jsonapi 1.0
433 |
434 | :param Query query: sqlalchemy query to sort
435 | :param filter_info: filter information
436 | :type filter_info: dict or None
437 | :param DeclarativeMeta model: an sqlalchemy model
438 | :return Query: the sorted query
439 | """
440 | if filter_info:
441 | filters = create_filters(model, filter_info, self.resource)
442 | query = query.filter(*filters)
443 |
444 | return query
445 |
446 | def sort_query(self, query, sort_info):
447 | """Sort query according to jsonapi 1.0
448 | :param Query query: sqlalchemy query to sort
449 | :param list sort_info: sort information
450 | :return Query: the sorted query
451 | """
452 | for sort_opt in sort_info:
453 | field = sort_opt['field']
454 | model = self.model
455 | if sort_opt.get('relation'):
456 | relation = getattr(model, sort_opt['relation'])
457 | query = query.join(relation)
458 | model = relation.property.mapper.class_
459 | if not hasattr(model, field):
460 | raise InvalidSort("{} has no attribute {}".format(self.model.__name__, field))
461 | query = query.order_by(getattr(getattr(model, field), sort_opt['order'])())
462 | return query
463 |
464 | def paginate_query(self, query, paginate_info):
465 | """Paginate query according to jsonapi 1.0
466 |
467 | :param Query query: sqlalchemy queryset
468 | :param dict paginate_info: pagination information
469 | :return Query: the paginated query
470 | """
471 | if int(paginate_info.get('size', 1)) == 0:
472 | return query
473 |
474 | page_size = int(paginate_info.get('size', 0)) or DEFAULT_PAGE_SIZE
475 | query = query.limit(page_size)
476 | if paginate_info.get('number'):
477 | query = query.offset((int(paginate_info['number']) - 1) * page_size)
478 |
479 | return query
480 |
481 | def query(self, view_kwargs):
482 | """Construct the base query to retrieve wanted data
483 |
484 | :param dict view_kwargs: kwargs from the resource view
485 | """
486 | return self.session.query(self.model)
487 |
488 | def before_create_object(self, data, view_kwargs):
489 | """Provide additional data before object creation
490 |
491 | :param dict data: the data validated by marshmallow
492 | :param dict view_kwargs: kwargs from the resource view
493 | """
494 | pass
495 |
496 | def after_create_object(self, obj, data, view_kwargs):
497 | """Provide additional data after object creation
498 |
499 | :param obj: an object from data layer
500 | :param dict data: the data validated by marshmallow
501 | :param dict view_kwargs: kwargs from the resource view
502 | """
503 | pass
504 |
505 | def before_get_object(self, view_kwargs):
506 | """Make work before to retrieve an object
507 |
508 | :param dict view_kwargs: kwargs from the resource view
509 | """
510 | pass
511 |
512 | def after_get_object(self, obj, view_kwargs):
513 | """Make work after to retrieve an object
514 |
515 | :param obj: an object from data layer
516 | :param dict view_kwargs: kwargs from the resource view
517 | """
518 | pass
519 |
520 | def before_get_collection(self, qs, view_kwargs):
521 | """Make work before to retrieve a collection of objects
522 |
523 | :param QueryStringManager qs: a querystring manager to retrieve information from url
524 | :param dict view_kwargs: kwargs from the resource view
525 | """
526 | pass
527 |
528 | def after_get_collection(self, collection, qs, view_kwargs):
529 | """Make work after to retrieve a collection of objects
530 |
531 | :param iterable collection: the collection of objects
532 | :param QueryStringManager qs: a querystring manager to retrieve information from url
533 | :param dict view_kwargs: kwargs from the resource view
534 | """
535 | pass
536 |
537 | def before_update_object(self, obj, data, view_kwargs):
538 | """Make checks or provide additional data before update object
539 |
540 | :param obj: an object from data layer
541 | :param dict data: the data validated by marshmallow
542 | :param dict view_kwargs: kwargs from the resource view
543 | """
544 | pass
545 |
546 | def after_update_object(self, obj, data, view_kwargs):
547 | """Make work after update object
548 |
549 | :param obj: an object from data layer
550 | :param dict data: the data validated by marshmallow
551 | :param dict view_kwargs: kwargs from the resource view
552 | """
553 | pass
554 |
555 | def before_delete_object(self, obj, view_kwargs):
556 | """Make checks before delete object
557 |
558 | :param obj: an object from data layer
559 | :param dict view_kwargs: kwargs from the resource view
560 | """
561 | pass
562 |
563 | def after_delete_object(self, obj, view_kwargs):
564 | """Make work after delete object
565 |
566 | :param obj: an object from data layer
567 | :param dict view_kwargs: kwargs from the resource view
568 | """
569 | pass
570 |
571 | def before_create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
572 | """Make work before to create a relationship
573 |
574 | :param dict json_data: the request params
575 | :param str relationship_field: the model attribute used for relationship
576 | :param str related_id_field: the identifier field of the related model
577 | :param dict view_kwargs: kwargs from the resource view
578 | :return boolean: True if relationship have changed else False
579 | """
580 | pass
581 |
582 | def after_create_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
583 | """Make work after to create a relationship
584 |
585 | :param obj: an object from data layer
586 | :param bool updated: True if object was updated else False
587 | :param dict json_data: the request params
588 | :param str relationship_field: the model attribute used for relationship
589 | :param str related_id_field: the identifier field of the related model
590 | :param dict view_kwargs: kwargs from the resource view
591 | :return boolean: True if relationship have changed else False
592 | """
593 | pass
594 |
595 | def before_get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
596 | """Make work before to get information about a relationship
597 |
598 | :param str relationship_field: the model attribute used for relationship
599 | :param str related_type_: the related resource type
600 | :param str related_id_field: the identifier field of the related model
601 | :param dict view_kwargs: kwargs from the resource view
602 | :return tuple: the object and related object(s)
603 | """
604 | pass
605 |
606 | def after_get_relationship(self, obj, related_objects, relationship_field, related_type_, related_id_field,
607 | view_kwargs):
608 | """Make work after to get information about a relationship
609 |
610 | :param obj: an object from data layer
611 | :param iterable related_objects: related objects of the object
612 | :param str relationship_field: the model attribute used for relationship
613 | :param str related_type_: the related resource type
614 | :param str related_id_field: the identifier field of the related model
615 | :param dict view_kwargs: kwargs from the resource view
616 | :return tuple: the object and related object(s)
617 | """
618 | pass
619 |
620 | def before_update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
621 | """Make work before to update a relationship
622 |
623 | :param dict json_data: the request params
624 | :param str relationship_field: the model attribute used for relationship
625 | :param str related_id_field: the identifier field of the related model
626 | :param dict view_kwargs: kwargs from the resource view
627 | :return boolean: True if relationship have changed else False
628 | """
629 | pass
630 |
631 | def after_update_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
632 | """Make work after to update a relationship
633 |
634 | :param obj: an object from data layer
635 | :param bool updated: True if object was updated else False
636 | :param dict json_data: the request params
637 | :param str relationship_field: the model attribute used for relationship
638 | :param str related_id_field: the identifier field of the related model
639 | :param dict view_kwargs: kwargs from the resource view
640 | :return boolean: True if relationship have changed else False
641 | """
642 | pass
643 |
644 | def before_delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
645 | """Make work before to delete a relationship
646 |
647 | :param dict json_data: the request params
648 | :param str relationship_field: the model attribute used for relationship
649 | :param str related_id_field: the identifier field of the related model
650 | :param dict view_kwargs: kwargs from the resource view
651 | """
652 | pass
653 |
654 | def after_delete_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
655 | """Make work after to delete a relationship
656 |
657 | :param obj: an object from data layer
658 | :param bool updated: True if object was updated else False
659 | :param dict json_data: the request params
660 | :param str relationship_field: the model attribute used for relationship
661 | :param str related_id_field: the identifier field of the related model
662 | :param dict view_kwargs: kwargs from the resource view
663 | """
664 | pass
665 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/data_layers/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import types
4 |
5 |
6 | class BaseDataLayer(object):
7 |
8 | ADDITIONAL_METHODS = ('query',
9 | 'before_create_object',
10 | 'after_create_object',
11 | 'before_get_object',
12 | 'after_get_object',
13 | 'before_get_collection',
14 | 'after_get_collection',
15 | 'before_update_object',
16 | 'after_update_object',
17 | 'before_delete_object',
18 | 'after_delete_object',
19 | 'before_create_relationship'
20 | 'after_create_relationship',
21 | 'before_get_relationship',
22 | 'after_get_relationship',
23 | 'before_update_relationship',
24 | 'after_update_relationship',
25 | 'before_delete_relationship',
26 | 'after_delete_relationship')
27 |
28 | def __init__(self, kwargs):
29 | """Intialize an data layer instance with kwargs
30 |
31 | :param dict kwargs: information about data layer instance
32 | """
33 | if kwargs.get('methods') is not None:
34 | self.bound_additional_methods(kwargs['methods'])
35 | kwargs.pop('methods')
36 |
37 | kwargs.pop('class', None)
38 |
39 | for key, value in kwargs.items():
40 | setattr(self, key, value)
41 |
42 | def create_object(self, data, view_kwargs):
43 | """Create an object
44 |
45 | :param dict data: the data validated by marshmallow
46 | :param dict view_kwargs: kwargs from the resource view
47 | :return DeclarativeMeta: an object
48 | """
49 | raise NotImplementedError
50 |
51 | def get_object(self, view_kwargs):
52 | """Retrieve an object
53 |
54 | :params dict view_kwargs: kwargs from the resource view
55 | :return DeclarativeMeta: an object
56 | """
57 | raise NotImplementedError
58 |
59 | def get_collection(self, qs, view_kwargs):
60 | """Retrieve a collection of objects
61 |
62 | :param QueryStringManager qs: a querystring manager to retrieve information from url
63 | :param dict view_kwargs: kwargs from the resource view
64 | :return tuple: the number of object and the list of objects
65 | """
66 | raise NotImplementedError
67 |
68 | def update_object(self, obj, data, view_kwargs):
69 | """Update an object
70 |
71 | :param DeclarativeMeta obj: an object
72 | :param dict data: the data validated by marshmallow
73 | :param dict view_kwargs: kwargs from the resource view
74 | :return boolean: True if object have changed else False
75 | """
76 | raise NotImplementedError
77 |
78 | def delete_object(self, obj, view_kwargs):
79 | """Delete an item through the data layer
80 |
81 | :param DeclarativeMeta obj: an object
82 | :param dict view_kwargs: kwargs from the resource view
83 | """
84 | raise NotImplementedError
85 |
86 | def create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
87 | """Create a relationship
88 |
89 | :param dict json_data: the request params
90 | :param str relationship_field: the model attribute used for relationship
91 | :param str related_id_field: the identifier field of the related model
92 | :param dict view_kwargs: kwargs from the resource view
93 | :return boolean: True if relationship have changed else False
94 | """
95 | raise NotImplementedError
96 |
97 | def get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
98 | """Get information about a relationship
99 |
100 | :param str relationship_field: the model attribute used for relationship
101 | :param str related_type_: the related resource type
102 | :param str related_id_field: the identifier field of the related model
103 | :param dict view_kwargs: kwargs from the resource view
104 | :return tuple: the object and related object(s)
105 | """
106 | raise NotImplementedError
107 |
108 | def update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
109 | """Update a relationship
110 |
111 | :param dict json_data: the request params
112 | :param str relationship_field: the model attribute used for relationship
113 | :param str related_id_field: the identifier field of the related model
114 | :param dict view_kwargs: kwargs from the resource view
115 | :return boolean: True if relationship have changed else False
116 | """
117 | raise NotImplementedError
118 |
119 | def delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
120 | """Delete a relationship
121 |
122 | :param dict json_data: the request params
123 | :param str relationship_field: the model attribute used for relationship
124 | :param str related_id_field: the identifier field of the related model
125 | :param dict view_kwargs: kwargs from the resource view
126 | """
127 | raise NotImplementedError
128 |
129 | def query(self, view_kwargs):
130 | """Construct the base query to retrieve wanted data
131 |
132 | :param dict view_kwargs: kwargs from the resource view
133 | """
134 | raise NotImplementedError
135 |
136 | def before_create_object(self, data, view_kwargs):
137 | """Provide additional data before object creation
138 |
139 | :param dict data: the data validated by marshmallow
140 | :param dict view_kwargs: kwargs from the resource view
141 | """
142 | raise NotImplementedError
143 |
144 | def after_create_object(self, obj, data, view_kwargs):
145 | """Provide additional data after object creation
146 |
147 | :param obj: an object from data layer
148 | :param dict data: the data validated by marshmallow
149 | :param dict view_kwargs: kwargs from the resource view
150 | """
151 | raise NotImplementedError
152 |
153 | def before_get_object(self, view_kwargs):
154 | """Make work before to retrieve an object
155 |
156 | :param dict view_kwargs: kwargs from the resource view
157 | """
158 | raise NotImplementedError
159 |
160 | def after_get_object(self, obj, view_kwargs):
161 | """Make work after to retrieve an object
162 |
163 | :param obj: an object from data layer
164 | :param dict view_kwargs: kwargs from the resource view
165 | """
166 | raise NotImplementedError
167 |
168 | def before_get_collection(self, qs, view_kwargs):
169 | """Make work before to retrieve a collection of objects
170 |
171 | :param QueryStringManager qs: a querystring manager to retrieve information from url
172 | :param dict view_kwargs: kwargs from the resource view
173 | """
174 | raise NotImplementedError
175 |
176 | def after_get_collection(self, collection, qs, view_kwargs):
177 | """Make work after to retrieve a collection of objects
178 |
179 | :param iterable collection: the collection of objects
180 | :param QueryStringManager qs: a querystring manager to retrieve information from url
181 | :param dict view_kwargs: kwargs from the resource view
182 | """
183 | raise NotImplementedError
184 |
185 | def before_update_object(self, obj, data, view_kwargs):
186 | """Make checks or provide additional data before update object
187 |
188 | :param obj: an object from data layer
189 | :param dict data: the data validated by marshmallow
190 | :param dict view_kwargs: kwargs from the resource view
191 | """
192 | raise NotImplementedError
193 |
194 | def after_update_object(self, obj, data, view_kwargs):
195 | """Make work after update object
196 |
197 | :param obj: an object from data layer
198 | :param dict data: the data validated by marshmallow
199 | :param dict view_kwargs: kwargs from the resource view
200 | """
201 | raise NotImplementedError
202 |
203 | def before_delete_object(self, obj, view_kwargs):
204 | """Make checks before delete object
205 |
206 | :param obj: an object from data layer
207 | :param dict view_kwargs: kwargs from the resource view
208 | """
209 | raise NotImplementedError
210 |
211 | def after_delete_object(self, obj, view_kwargs):
212 | """Make work after delete object
213 |
214 | :param obj: an object from data layer
215 | :param dict view_kwargs: kwargs from the resource view
216 | """
217 | raise NotImplementedError
218 |
219 | def before_create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
220 | """Make work before to create a relationship
221 |
222 | :param dict json_data: the request params
223 | :param str relationship_field: the model attribute used for relationship
224 | :param str related_id_field: the identifier field of the related model
225 | :param dict view_kwargs: kwargs from the resource view
226 | :return boolean: True if relationship have changed else False
227 | """
228 | raise NotImplementedError
229 |
230 | def after_create_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
231 | """Make work after to create a relationship
232 |
233 | :param obj: an object from data layer
234 | :param bool updated: True if object was updated else False
235 | :param dict json_data: the request params
236 | :param str relationship_field: the model attribute used for relationship
237 | :param str related_id_field: the identifier field of the related model
238 | :param dict view_kwargs: kwargs from the resource view
239 | :return boolean: True if relationship have changed else False
240 | """
241 | raise NotImplementedError
242 |
243 | def before_get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
244 | """Make work before to get information about a relationship
245 |
246 | :param str relationship_field: the model attribute used for relationship
247 | :param str related_type_: the related resource type
248 | :param str related_id_field: the identifier field of the related model
249 | :param dict view_kwargs: kwargs from the resource view
250 | :return tuple: the object and related object(s)
251 | """
252 | raise NotImplementedError
253 |
254 | def after_get_relationship(self, obj, related_objects, relationship_field, related_type_, related_id_field,
255 | view_kwargs):
256 | """Make work after to get information about a relationship
257 |
258 | :param obj: an object from data layer
259 | :param iterable related_objects: related objects of the object
260 | :param str relationship_field: the model attribute used for relationship
261 | :param str related_type_: the related resource type
262 | :param str related_id_field: the identifier field of the related model
263 | :param dict view_kwargs: kwargs from the resource view
264 | :return tuple: the object and related object(s)
265 | """
266 | raise NotImplementedError
267 |
268 | def before_update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
269 | """Make work before to update a relationship
270 |
271 | :param dict json_data: the request params
272 | :param str relationship_field: the model attribute used for relationship
273 | :param str related_id_field: the identifier field of the related model
274 | :param dict view_kwargs: kwargs from the resource view
275 | :return boolean: True if relationship have changed else False
276 | """
277 | raise NotImplementedError
278 |
279 | def after_update_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
280 | """Make work after to update a relationship
281 |
282 | :param obj: an object from data layer
283 | :param bool updated: True if object was updated else False
284 | :param dict json_data: the request params
285 | :param str relationship_field: the model attribute used for relationship
286 | :param str related_id_field: the identifier field of the related model
287 | :param dict view_kwargs: kwargs from the resource view
288 | :return boolean: True if relationship have changed else False
289 | """
290 | raise NotImplementedError
291 |
292 | def before_delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
293 | """Make work before to delete a relationship
294 |
295 | :param dict json_data: the request params
296 | :param str relationship_field: the model attribute used for relationship
297 | :param str related_id_field: the identifier field of the related model
298 | :param dict view_kwargs: kwargs from the resource view
299 | """
300 | raise NotImplementedError
301 |
302 | def after_delete_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
303 | """Make work after to delete a relationship
304 |
305 | :param obj: an object from data layer
306 | :param bool updated: True if object was updated else False
307 | :param dict json_data: the request params
308 | :param str relationship_field: the model attribute used for relationship
309 | :param str related_id_field: the identifier field of the related model
310 | :param dict view_kwargs: kwargs from the resource view
311 | """
312 | raise NotImplementedError
313 |
314 | def bound_additional_methods(self, methods):
315 | """Bound additional methods to current instance
316 |
317 | :param class meta: information from Meta class used to configure the data layer instance
318 | """
319 | for key, value in methods.items():
320 | if key in self.ADDITIONAL_METHODS:
321 | setattr(self, key, types.MethodType(value, self))
322 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/data_layers/filtering/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossasia/flask-rest-jsonapi/a03408bffd5ef96bf3b8abe3a30d147db46fbe47/flask_rest_jsonapi/data_layers/filtering/__init__.py
--------------------------------------------------------------------------------
/flask_rest_jsonapi/data_layers/filtering/alchemy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from sqlalchemy import and_, or_, not_
4 |
5 | from flask import current_app
6 | from flask_rest_jsonapi.exceptions import InvalidFilters
7 | from flask_rest_jsonapi.schema import get_relationships, get_model_field
8 |
9 |
10 | def create_filters(model, filter_info, resource):
11 | """Apply filters from filters information to base query
12 |
13 | :param DeclarativeMeta model: the model of the node
14 | :param dict filter_info: current node filter information
15 | :param Resource resource: the resource
16 | """
17 | filters = []
18 | for filter_ in filter_info:
19 | filters.append(Node(model, filter_, resource, resource.schema).resolve())
20 |
21 | return filters
22 |
23 |
24 | class Node(object):
25 |
26 | def __init__(self, model, filter_, resource, schema):
27 | self.model = model
28 | self.filter_ = filter_
29 | self.resource = resource
30 | self.schema = schema
31 |
32 | def resolve(self):
33 | if 'or' not in self.filter_ and 'and' not in self.filter_ and 'not' not in self.filter_:
34 | value = self.value
35 |
36 | if isinstance(value, dict):
37 | value = Node(self.related_model, value, self.resource, self.related_schema).resolve()
38 |
39 | if '__' in self.filter_.get('name', ''):
40 | value = {self.filter_['name'].split('__')[1]: value}
41 |
42 | if isinstance(value, dict):
43 | return getattr(self.column, self.operator)(**value)
44 | else:
45 | return getattr(self.column, self.operator)(value)
46 |
47 | if 'or' in self.filter_:
48 | return or_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['or'])
49 | if 'and' in self.filter_:
50 | return and_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['and'])
51 | if 'not' in self.filter_:
52 | return not_(Node(self.model, self.filter_['not'], self.resource, self.schema).resolve())
53 |
54 | @property
55 | def name(self):
56 | """Return the name of the node or raise a BadRequest exception
57 |
58 | :return str: the name of the field to filter on
59 | """
60 | name = self.filter_.get('name')
61 |
62 | if name is None:
63 | raise InvalidFilters("Can't find name of a filter")
64 |
65 | if '__' in name:
66 | name = name.split('__')[0]
67 |
68 | if current_app.config.get('DASHERIZE_API') == True:
69 | name = name.replace('-', '_')
70 |
71 | if name not in self.schema._declared_fields:
72 | raise InvalidFilters("{} has no attribute {}".format(self.schema.__name__, name))
73 |
74 | return name
75 |
76 | @property
77 | def op(self):
78 | """Return the operator of the node
79 |
80 | :return str: the operator to use in the filter
81 | """
82 | try:
83 | return self.filter_['op']
84 | except KeyError:
85 | raise InvalidFilters("Can't find op of a filter")
86 |
87 | @property
88 | def column(self):
89 | """Get the column object
90 |
91 | :param DeclarativeMeta model: the model
92 | :param str field: the field
93 | :return InstrumentedAttribute: the column to filter on
94 | """
95 | field = self.name
96 |
97 | model_field = get_model_field(self.schema, field)
98 |
99 | try:
100 | return getattr(self.model, model_field)
101 | except AttributeError:
102 | raise InvalidFilters("{} has no attribute {}".format(self.model.__name__, model_field))
103 |
104 | @property
105 | def operator(self):
106 | """Get the function operator from his name
107 |
108 | :return callable: a callable to make operation on a column
109 | """
110 | operators = (self.op, self.op + '_', '__' + self.op + '__')
111 |
112 | for op in operators:
113 | if hasattr(self.column, op):
114 | return op
115 |
116 | raise InvalidFilters("{} has no operator {}".format(self.column.key, self.op))
117 |
118 | @property
119 | def value(self):
120 | """Get the value to filter on
121 |
122 | :return: the value to filter on
123 | """
124 | if self.filter_.get('field') is not None:
125 | try:
126 | result = getattr(self.model, self.filter_['field'])
127 | except AttributeError:
128 | raise InvalidFilters("{} has no attribute {}".format(self.model.__name__, self.filter_['field']))
129 | else:
130 | return result
131 | else:
132 | if 'val' not in self.filter_:
133 | raise InvalidFilters("Can't find value or field in a filter")
134 |
135 | return self.filter_['val']
136 |
137 | @property
138 | def related_model(self):
139 | """Get the related model of a relationship field
140 |
141 | :return DeclarativeMeta: the related model
142 | """
143 | relationship_field = self.name
144 |
145 | if relationship_field not in get_relationships(self.schema).values():
146 | raise InvalidFilters("{} has no relationship attribute {}".format(self.schema.__name__, relationship_field))
147 |
148 | relationship_model_field = get_model_field(self.schema, relationship_field)
149 |
150 | return getattr(self.model, relationship_model_field).property.mapper.class_
151 |
152 | @property
153 | def related_schema(self):
154 | """Get the related schema of a relationship field
155 |
156 | :return Schema: the related schema
157 | """
158 | relationship_field = self.name
159 |
160 | if relationship_field not in get_relationships(self.schema).values():
161 | raise InvalidFilters("{} has no relationship attribute {}".format(self.schema.__name__, relationship_field))
162 |
163 | return self.schema._declared_fields[relationship_field].schema.__class__
164 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/decorators.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 | from functools import wraps
5 |
6 | from flask import request, make_response
7 |
8 | from flask_rest_jsonapi.errors import jsonapi_errors
9 |
10 |
11 | def check_headers(func):
12 | """Check headers according to jsonapi reference
13 |
14 | :param callable func: the function to decorate
15 | :return callable: the wrapped function
16 | """
17 | @wraps(func)
18 | def wrapper(*args, **kwargs):
19 | if request.method in ('POST', 'PATCH'):
20 | if 'Content-Type' not in request.headers or request.headers['Content-Type'] != 'application/vnd.api+json':
21 | error = json.dumps(jsonapi_errors([{'source': '',
22 | 'detail': "Content-Type header must be application/vnd.api+json",
23 | 'title': 'InvalidRequestHeader',
24 | 'status': 415}]))
25 | return make_response(error, 415, {'Content-Type': 'application/vnd.api+json'})
26 | if request.headers.get('Accept') and not 'application/vnd.api+json' in request.accept_mimetypes:
27 | error = json.dumps(jsonapi_errors([{'source': '',
28 | 'detail': "Accept header must be application/vnd.api+json",
29 | 'title': 'InvalidRequestHeader',
30 | 'status': 406}]))
31 | return make_response(error, 406, {'Content-Type': 'application/vnd.api+json'})
32 | return func(*args, **kwargs)
33 | return wrapper
34 |
35 |
36 | def check_method_requirements(func):
37 | """Check methods requirements
38 |
39 | :param callable func: the function to decorate
40 | :return callable: the wrapped function
41 | """
42 | @wraps(func)
43 | def wrapper(*args, **kwargs):
44 | error_message = "You must provide {error_field} in {cls} to get access to the default {method} method"
45 | error_data = {'cls': args[0].__class__.__name__, 'method': request.method.lower()}
46 |
47 | if not hasattr(args[0], '_data_layer'):
48 | error_data.update({'error_field': 'a data layer class'})
49 | raise Exception(error_message.format(**error_data))
50 |
51 | if request.method != 'DELETE':
52 | if not hasattr(args[0], 'schema'):
53 | error_data.update({'error_field': 'a schema class'})
54 | raise Exception(error_message.format(**error_data))
55 |
56 | return func(*args, **kwargs)
57 | return wrapper
58 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/errors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | def jsonapi_errors(jsonapi_errors):
5 | """Construct api error according to jsonapi 1.0
6 |
7 | :param iterable jsonapi_errors: an iterable of jsonapi error
8 | :return dict: a dict of errors according to jsonapi 1.0
9 | """
10 | return {'errors': [jsonapi_error for jsonapi_error in jsonapi_errors],
11 | 'jsonapi': {'version': '1.0'}}
12 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | class JsonApiException(Exception):
5 |
6 | title = 'Unknown error'
7 | status = 500
8 |
9 | def __init__(self, source, detail, title=None, status=None):
10 | """Initialize a jsonapi exception
11 |
12 | :param dict source: the source of the error
13 | :param str detail: the detail of the error
14 | """
15 | self.source = source
16 | self.detail = detail
17 | if title is not None:
18 | self.title = title
19 | if status is not None:
20 | self.status = status
21 |
22 | def to_dict(self):
23 | return {'status': self.status,
24 | 'source': self.source,
25 | 'title': self.title,
26 | 'detail': self.detail}
27 |
28 |
29 | class BadRequest(JsonApiException):
30 | title = "Bad request"
31 | status = 400
32 |
33 |
34 | class InvalidField(BadRequest):
35 | title = "Invalid fields querystring parameter."
36 |
37 | def __init__(self, detail):
38 | self.source = {'parameter': 'fields'}
39 | self.detail = detail
40 |
41 |
42 | class InvalidInclude(BadRequest):
43 | title = "Invalid include querystring parameter."
44 |
45 | def __init__(self, detail):
46 | self.source = {'parameter': 'include'}
47 | self.detail = detail
48 |
49 |
50 | class InvalidFilters(BadRequest):
51 | title = "Invalid filters querystring parameter."
52 |
53 | def __init__(self, detail):
54 | self.source = {'parameter': 'filters'}
55 | self.detail = detail
56 |
57 |
58 | class InvalidSort(BadRequest):
59 | title = "Invalid sort querystring parameter."
60 |
61 | def __init__(self, detail):
62 | self.source = {'parameter': 'sort'}
63 | self.detail = detail
64 |
65 |
66 | class ObjectNotFound(JsonApiException):
67 | title = "Object not found"
68 | status = 404
69 |
70 |
71 | class RelatedObjectNotFound(ObjectNotFound):
72 | title = "Related object not found"
73 |
74 |
75 | class RelationNotFound(JsonApiException):
76 | title = "Relation not found"
77 |
78 |
79 | class InvalidType(JsonApiException):
80 | title = "Invalid type"
81 | status = 409
82 |
83 |
84 | class PreconditionFailed(JsonApiException):
85 | title = "Precondition Failed"
86 | status = 412
87 |
88 |
89 | class NotModified(JsonApiException):
90 | title = "Not Modified"
91 | status = 304
92 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/pagination.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from six.moves.urllib.parse import urlencode
4 | from math import ceil
5 | from copy import copy
6 |
7 | from flask_rest_jsonapi.constants import DEFAULT_PAGE_SIZE
8 |
9 |
10 | def add_pagination_links(data, object_count, querystring, base_url):
11 | """Add pagination links to result
12 |
13 | :param dict data: the result of the view
14 | :param int object_count: number of objects in result
15 | :param QueryStringManager querystring: the managed querystring fields and values
16 | :param str base_url: the base url for pagination
17 | """
18 | links = {}
19 | all_qs_args = copy(querystring.querystring)
20 |
21 | links['self'] = base_url
22 |
23 | # compute self link
24 | if all_qs_args:
25 | links['self'] += '?' + urlencode(all_qs_args)
26 |
27 | if querystring.pagination.get('size') != '0' and object_count > 1:
28 | # compute last link
29 | page_size = int(querystring.pagination.get('size', 0)) or DEFAULT_PAGE_SIZE
30 | last_page = int(ceil(object_count / page_size))
31 |
32 | if last_page > 1:
33 | links['first'] = links['last'] = base_url
34 |
35 | all_qs_args.pop('page[number]', None)
36 |
37 | # compute first link
38 | if all_qs_args:
39 | links['first'] += '?' + urlencode(all_qs_args)
40 |
41 | all_qs_args.update({'page[number]': last_page})
42 | links['last'] += '?' + urlencode(all_qs_args)
43 |
44 | # compute previous and next link
45 | current_page = int(querystring.pagination.get('number', 0)) or 1
46 | if current_page > 1:
47 | all_qs_args.update({'page[number]': current_page - 1})
48 | links['prev'] = '?'.join((base_url, urlencode(all_qs_args)))
49 | if current_page < last_page:
50 | all_qs_args.update({'page[number]': current_page + 1})
51 | links['next'] = '?'.join((base_url, urlencode(all_qs_args)))
52 |
53 | data['links'] = links
54 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/querystring.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 |
5 | from flask_rest_jsonapi.exceptions import BadRequest, InvalidFilters, InvalidSort
6 | from flask_rest_jsonapi.schema import get_model_field, get_relationships
7 | from flask import current_app
8 |
9 |
10 | class QueryStringManager(object):
11 | """Querystring parser according to jsonapi reference
12 | """
13 |
14 | MANAGED_KEYS = (
15 | 'filter',
16 | 'page',
17 | 'fields',
18 | 'sort',
19 | 'include'
20 | )
21 |
22 | def __init__(self, querystring, schema):
23 | """Initialization instance
24 |
25 | :param dict querystring: query string dict from request.args
26 | """
27 | if not isinstance(querystring, dict):
28 | raise ValueError('QueryStringManager require a dict-like object query_string parameter')
29 |
30 | self.qs = querystring
31 | self.schema = schema
32 |
33 | def _get_key_values(self, name):
34 | """Return a dict containing key / values items for a given key, used for items like filters, page, etc.
35 |
36 | :param str name: name of the querystring parameter
37 | :return dict: a dict of key / values items
38 | """
39 | results = {}
40 |
41 | for key, value in self.qs.items():
42 | try:
43 | if not key.startswith(name):
44 | continue
45 |
46 | key_start = key.index('[') + 1
47 | key_end = key.index(']')
48 | item_key = key[key_start:key_end]
49 |
50 | if ',' in value:
51 | item_value = value.split(',')
52 | else:
53 | item_value = value
54 | results.update({item_key: item_value})
55 | except Exception:
56 | raise BadRequest({'parameter': key}, "Parse error")
57 |
58 | return results
59 |
60 | @property
61 | def querystring(self):
62 | """Return original querystring but containing only managed keys
63 |
64 | :return dict: dict of managed querystring parameter
65 | """
66 | return {key: value for (key, value) in self.qs.items() if key.startswith(self.MANAGED_KEYS)}
67 |
68 | @property
69 | def filters(self):
70 | """Return filters from query string.
71 |
72 | :return list: filter information
73 | """
74 | filters = self.qs.get('filter')
75 | if filters is not None:
76 | try:
77 | filters = json.loads(filters)
78 | except (ValueError, TypeError):
79 | raise InvalidFilters("Parse error")
80 |
81 | return filters
82 |
83 | @property
84 | def pagination(self):
85 | """Return all page parameters as a dict.
86 |
87 | :return dict: a dict of pagination information
88 |
89 | To allow multiples strategies, all parameters starting with `page` will be included. e.g::
90 |
91 | {
92 | "number": '25',
93 | "size": '150',
94 | }
95 |
96 | Example with number strategy::
97 |
98 | >>> query_string = {'page[number]': '25', 'page[size]': '10'}
99 | >>> parsed_query.pagination
100 | {'number': '25', 'size': '10'}
101 | """
102 | # check values type
103 | result = self._get_key_values('page')
104 | for key, value in result.items():
105 | if key not in ('number', 'size'):
106 | raise BadRequest({'parameter': 'page'}, "{} is not a valid parameter of pagination".format(key))
107 | try:
108 | int(value)
109 | except ValueError:
110 | raise BadRequest({'parameter': 'page[{}]'.format(key)}, "Parse error")
111 |
112 | return result
113 |
114 | @property
115 | def fields(self):
116 | """Return fields wanted by client.
117 |
118 | :return dict: a dict of sparse fieldsets information
119 |
120 | Return value will be a dict containing all fields by resource, for example::
121 |
122 | {
123 | "user": ['name', 'email'],
124 | }
125 |
126 | """
127 | result = self._get_key_values('fields')
128 | for key, value in result.items():
129 | if not isinstance(value, list):
130 | if current_app.config.get('DASHERIZE_API') == True:
131 | result[key] = [value.replace('-', '_')]
132 | else:
133 | result[key] = [value]
134 | return result
135 |
136 | @property
137 | def sorting(self):
138 | """Return fields to sort by including sort name for SQLAlchemy and row
139 | sort parameter for other ORMs
140 |
141 | :return list: a list of sorting information
142 |
143 | Example of return value::
144 |
145 | [
146 | {'field': 'created_at', 'order': 'desc'},
147 | ]
148 |
149 | """
150 | if self.qs.get('sort'):
151 | sorting_results = []
152 | for sort_field in self.qs['sort'].split(','):
153 | if current_app.config.get('DASHERIZE_API') == True:
154 | field = sort_field[0].replace('-', '') + sort_field[1:].replace('-', '_')
155 | else:
156 | field = sort_field[0].replace('-', '') + sort_field[1:]
157 | def check_schema_field(schema, field):
158 | if field not in schema._declared_fields:
159 | raise InvalidSort("{} has no attribute {}".format(schema.__name__, field))
160 | if field in get_relationships(schema).values():
161 | raise InvalidSort("You can't sort on {} because it is a relationship field".format(field))
162 | schema = self.schema
163 | relation = None
164 | if '.' in field:
165 | relation, field = field.split('.')
166 | if relation not in get_relationships(self.schema).values():
167 | raise InvalidSort("{} has no relationship {}".format(self.schema.__name__, relation))
168 | relation_field = self.schema._declared_fields[relation]
169 | relation = get_model_field(self.schema, relation)
170 | schema = relation_field.schema.__class__
171 |
172 | check_schema_field(schema, field)
173 | field = get_model_field(schema, field)
174 | order = 'desc' if sort_field.startswith('-') else 'asc'
175 | sorting_results.append({'field': field, 'order': order, 'relation': relation})
176 | return sorting_results
177 |
178 | return []
179 |
180 | @property
181 | def include(self):
182 | """Return fields to include
183 |
184 | :return list: a list of include information
185 | """
186 | include_param = self.qs.get('include')
187 | if include_param:
188 | param_results = []
189 | for param in include_param.split(','):
190 | if current_app.config.get('DASHERIZE_API') == True:
191 | param = param.replace('-', '_')
192 | param_results.append(param)
193 | return param_results
194 | return []
195 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/resource.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import inspect
4 | import json
5 | from copy import copy
6 | from six import with_metaclass
7 | from datetime import datetime, timezone
8 | import hashlib
9 |
10 | from werkzeug.wrappers import Response
11 | from flask import request, url_for, make_response, current_app
12 | from flask.views import MethodView, MethodViewType
13 | from marshmallow_jsonapi.exceptions import IncorrectTypeError
14 | from marshmallow import ValidationError
15 |
16 | from flask_rest_jsonapi.errors import jsonapi_errors
17 | from flask_rest_jsonapi.querystring import QueryStringManager as QSManager
18 | from flask_rest_jsonapi.pagination import add_pagination_links
19 | from flask_rest_jsonapi.exceptions import InvalidType, BadRequest, JsonApiException, RelationNotFound, ObjectNotFound, NotModified, PreconditionFailed
20 | from flask_rest_jsonapi.decorators import check_headers, check_method_requirements
21 | from flask_rest_jsonapi.schema import compute_schema, get_relationships, get_model_field
22 | from flask_rest_jsonapi.data_layers.base import BaseDataLayer
23 | from flask_rest_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
24 |
25 |
26 | class ResourceMeta(MethodViewType):
27 |
28 | def __new__(cls, name, bases, d):
29 | rv = super(ResourceMeta, cls).__new__(cls, name, bases, d)
30 | if 'data_layer' in d:
31 | if not isinstance(d['data_layer'], dict):
32 | raise Exception("You must provide a data layer information as dict in {}".format(cls.__name__))
33 |
34 | if d['data_layer'].get('class') is not None\
35 | and BaseDataLayer not in inspect.getmro(d['data_layer']['class']):
36 | raise Exception("You must provide a data layer class inherited from BaseDataLayer in {}"
37 | .format(cls.__name__))
38 |
39 | data_layer_cls = d['data_layer'].get('class', SqlalchemyDataLayer)
40 | data_layer_kwargs = d['data_layer']
41 | rv._data_layer = data_layer_cls(data_layer_kwargs)
42 |
43 | rv.decorators = (check_headers,)
44 | if 'decorators' in d:
45 | rv.decorators += d['decorators']
46 |
47 | return rv
48 |
49 |
50 | class Resource(MethodView):
51 |
52 | def __new__(cls):
53 | if hasattr(cls, '_data_layer'):
54 | cls._data_layer.resource = cls
55 |
56 | return super(Resource, cls).__new__(cls)
57 |
58 | def dispatch_request(self, *args, **kwargs):
59 | method = getattr(self, request.method.lower(), None)
60 | if method is None and request.method == 'HEAD':
61 | method = getattr(self, 'get', None)
62 | assert method is not None, 'Unimplemented method {}'.format(request.method)
63 |
64 | headers = {'Content-Type': 'application/vnd.api+json'}
65 |
66 | try:
67 | response = method(*args, **kwargs)
68 | except JsonApiException as e:
69 | return make_response(json.dumps(jsonapi_errors([e.to_dict()])),
70 | e.status,
71 | headers)
72 | except Exception as e:
73 | if current_app.config.get('API_PROPOGATE_UNCAUGHT_EXCEPTIONS') == True:
74 | raise
75 | if current_app.config['DEBUG'] is True:
76 | raise e
77 | if current_app.config.get('PROPOGATE_ERROR') == True:
78 | exc = JsonApiException({'pointer': ''}, str(e))
79 | else:
80 | exc = JsonApiException({'pointer': ''}, 'Unknown error')
81 | return make_response(json.dumps(jsonapi_errors([exc.to_dict()])),
82 | exc.status,
83 | headers)
84 |
85 | if isinstance(response, Response):
86 | response.headers.add('Content-Type', 'application/vnd.api+json')
87 | resp = response
88 | elif not isinstance(response, tuple):
89 | if isinstance(response, dict):
90 | response.update({'jsonapi': {'version': '1.0'}})
91 | resp = make_response(json.dumps(response), 200, headers)
92 | else:
93 | try:
94 | data, status_code, headers = response
95 | headers.update({'Content-Type': 'application/vnd.api+json'})
96 | except ValueError:
97 | pass
98 |
99 | try:
100 | data, status_code = response
101 | except ValueError:
102 | pass
103 |
104 | if isinstance(data, dict):
105 | data.update({'jsonapi': {'version': '1.0'}})
106 |
107 | resp = make_response(json.dumps(data), status_code, headers)
108 |
109 | # ETag Handling
110 | if current_app.config.get('ETAG') == True:
111 | etag = f'W/"{hashlib.sha1(resp.get_data()).hexdigest()}"'
112 | resp.headers['ETag'] = etag
113 |
114 | if_match = request.headers.get('If-Match')
115 | if_none_match = request.headers.get('If-None-Match')
116 | if if_match:
117 | etag_list = [tag.strip() for tag in if_match.split(',')]
118 | if etag not in etag_list and '*' not in etag_list:
119 | exc = PreconditionFailed({'pointer': ''}, 'Precondition failed')
120 | return make_response(json.dumps(jsonapi_errors([exc.to_dict()])),
121 | exc.status,
122 | headers)
123 | elif if_none_match:
124 | etag_list = [tag.strip() for tag in if_none_match.split(',')]
125 | if etag in etag_list or '*' in etag_list:
126 | exc = NotModified({'pointer': ''}, 'Resource not modified')
127 | return make_response(json.dumps(jsonapi_errors([exc.to_dict()])),
128 | exc.status,
129 | headers)
130 | return resp
131 |
132 |
133 | class ResourceList(with_metaclass(ResourceMeta, Resource)):
134 |
135 | @check_method_requirements
136 | def get(self, *args, **kwargs):
137 | """Retrieve a collection of objects
138 | """
139 | self.before_get(args, kwargs)
140 |
141 | qs = QSManager(request.args, self.schema)
142 | objects_count, objects = self._data_layer.get_collection(qs, kwargs)
143 |
144 | schema_kwargs = getattr(self, 'get_schema_kwargs', dict())
145 | schema_kwargs.update({'many': True})
146 |
147 | schema = compute_schema(self.schema,
148 | schema_kwargs,
149 | qs,
150 | qs.include)
151 |
152 | result = schema.dump(objects).data
153 |
154 | view_kwargs = request.view_args if getattr(self, 'view_kwargs', None) is True else dict()
155 | add_pagination_links(result,
156 | objects_count,
157 | qs,
158 | url_for(self.view, **view_kwargs))
159 |
160 | result.update({'meta': {'count': objects_count}})
161 |
162 | self.after_get(result)
163 | return result
164 |
165 | @check_method_requirements
166 | def post(self, *args, **kwargs):
167 | """Create an object
168 | """
169 | json_data = request.get_json()
170 | self.decide_schema(json_data)
171 | qs = QSManager(request.args, self.schema)
172 |
173 | schema = compute_schema(self.schema,
174 | getattr(self, 'post_schema_kwargs', dict()),
175 | qs,
176 | qs.include)
177 |
178 | try:
179 | data, errors = schema.load(json_data)
180 | except IncorrectTypeError as e:
181 | errors = e.messages
182 | for error in errors['errors']:
183 | error['status'] = '409'
184 | error['title'] = "Incorrect type"
185 | return errors, 409
186 | except ValidationError as e:
187 | errors = e.messages
188 | for message in errors['errors']:
189 | message['status'] = '422'
190 | message['title'] = "Validation error"
191 | return errors, 422
192 |
193 | if errors:
194 | for error in errors['errors']:
195 | error['status'] = "422"
196 | error['title'] = "Validation error"
197 | return errors, 422
198 |
199 | self.before_post(args, kwargs, data=data)
200 |
201 | obj = self._data_layer.create_object(data, kwargs)
202 |
203 | result = schema.dump(obj).data
204 | self.after_post(result)
205 | return result, 201, {'Location': result['data']['links']['self']}
206 |
207 | def before_get(self, args, kwargs):
208 | pass
209 |
210 | def after_get(self, result):
211 | pass
212 |
213 | def before_post(self, args, kwargs, data=None):
214 | pass
215 |
216 | def after_post(self, result):
217 | pass
218 |
219 | def decide_schema(self, json_data):
220 | """Method to decide schema before post."""
221 | pass
222 |
223 |
224 | class ResourceDetail(with_metaclass(ResourceMeta, Resource)):
225 |
226 | @check_method_requirements
227 | def get(self, *args, **kwargs):
228 | """Get object details
229 | """
230 | self.before_get(args, kwargs)
231 | if request.args.get('get_trashed') == 'true':
232 | obj = self._data_layer.get_object(kwargs, get_trashed=True)
233 | else:
234 | obj = self._data_layer.get_object(kwargs)
235 |
236 | if obj is None:
237 | raise ObjectNotFound({'pointer': ''}, 'Object Not Found')
238 | qs = QSManager(request.args, self.schema)
239 |
240 | schema = compute_schema(self.schema,
241 | getattr(self, 'get_schema_kwargs', dict()),
242 | qs,
243 | qs.include)
244 |
245 | result = schema.dump(obj).data
246 |
247 | # Deleting the soft-deleted entries from the included fields
248 | if (not request.args.get('get_trashed') == 'true') and result.get('included', None):
249 | for idx, include in enumerate(result.get('included')):
250 | # if the deleted-at field is not None, then it has been soft deleted.
251 | if 'attributes' in include:
252 | if include.get('attributes', None).get('deleted-at', None):
253 | del result.get('included')[idx]
254 |
255 | self.after_get(result)
256 | return result
257 |
258 | @check_method_requirements
259 | def patch(self, *args, **kwargs):
260 | """Update an object
261 | """
262 | json_data = request.get_json()
263 | self.decide_schema(json_data)
264 | qs = QSManager(request.args, self.schema)
265 | schema_kwargs = getattr(self, 'patch_schema_kwargs', dict())
266 | schema_kwargs.update({'partial': True})
267 |
268 | schema = compute_schema(self.schema,
269 | schema_kwargs,
270 | qs,
271 | qs.include)
272 |
273 | try:
274 | data, errors = schema.load(json_data)
275 | except IncorrectTypeError as e:
276 | errors = e.messages
277 | for error in errors['errors']:
278 | error['status'] = '409'
279 | error['title'] = "Incorrect type"
280 | return errors, 409
281 | except ValidationError as e:
282 | errors = e.messages
283 | for message in errors['errors']:
284 | message['status'] = '422'
285 | message['title'] = "Validation error"
286 | return errors, 422
287 |
288 | if errors:
289 | for error in errors['errors']:
290 | error['status'] = "422"
291 | error['title'] = "Validation error"
292 | return errors, 422
293 |
294 | if 'id' not in json_data['data']:
295 | raise BadRequest('/data/id', 'Missing id in "data" node')
296 | if json_data['data']['id'] != str(kwargs[self.data_layer.get('url_field', 'id')]):
297 | raise BadRequest('/data/id', 'Value of id does not match the resource identifier in url')
298 |
299 | self.before_patch(args, kwargs, data=data)
300 |
301 | if request.args.get('get_trashed') == 'true':
302 | obj = self._data_layer.get_object(kwargs, get_trashed=True)
303 | else:
304 | obj = self._data_layer.get_object(kwargs)
305 |
306 | if obj is None:
307 | raise ObjectNotFound({'pointer': ''}, 'Object Not Found')
308 |
309 | self._data_layer.update_object(obj, data, kwargs)
310 |
311 | result = schema.dump(obj).data
312 |
313 | self.after_patch(result)
314 | return result
315 |
316 | @check_method_requirements
317 | def delete(self, *args, **kwargs):
318 | """Delete an object
319 | """
320 | self.before_delete(args, kwargs)
321 | obj = self._data_layer.get_object(kwargs, get_trashed=(request.args.get('permanent') == 'true'))
322 | if obj is None:
323 | raise ObjectNotFound({'pointer': ''}, 'Object Not Found')
324 | if 'deleted_at' not in self.schema._declared_fields or request.args.get('permanent') == 'true' or current_app.config['SOFT_DELETE'] is False:
325 | self._data_layer.delete_object(obj, kwargs)
326 | else:
327 | data = {'deleted_at': str(datetime.now(timezone.utc))}
328 | self._data_layer.update_object(obj, data, kwargs)
329 |
330 | result = {'meta': {'message': 'Object successfully deleted'}}
331 | self.after_delete(result)
332 | return result
333 |
334 | def before_get(self, args, kwargs):
335 | pass
336 |
337 | def after_get(self, result):
338 | pass
339 |
340 | def before_patch(self, args, kwargs, data=None):
341 | pass
342 |
343 | def after_patch(self, result):
344 | pass
345 |
346 | def before_delete(self, args, kwargs):
347 | pass
348 |
349 | def after_delete(self, result):
350 | pass
351 |
352 | def decide_schema(self, json_data):
353 | """Method to decide schema before post."""
354 | pass
355 |
356 |
357 | class ResourceRelationship(with_metaclass(ResourceMeta, Resource)):
358 |
359 | @check_method_requirements
360 | def get(self, *args, **kwargs):
361 | """Get a relationship details
362 | """
363 | self.before_get(args, kwargs)
364 |
365 | relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
366 | related_view = self.schema._declared_fields[relationship_field].related_view
367 | related_view_kwargs = self.schema._declared_fields[relationship_field].related_view_kwargs
368 |
369 | obj, data = self._data_layer.get_relationship(model_relationship_field,
370 | related_type_,
371 | related_id_field,
372 | kwargs)
373 |
374 | for key, value in copy(related_view_kwargs).items():
375 | if isinstance(value, str) and value.startswith('<') and value.endswith('>'):
376 | tmp_obj = obj
377 | for field in value[1:-1].split('.'):
378 | tmp_obj = getattr(tmp_obj, field)
379 | related_view_kwargs[key] = tmp_obj
380 |
381 | result = {'links': {'self': request.path,
382 | 'related': url_for(related_view, **related_view_kwargs)},
383 | 'data': data}
384 |
385 | qs = QSManager(request.args, self.schema)
386 | if qs.include:
387 | schema = compute_schema(self.schema, dict(), qs, qs.include)
388 |
389 | serialized_obj = schema.dump(obj)
390 | result['included'] = serialized_obj.data.get('included', dict())
391 |
392 | self.after_get(result)
393 | return result
394 |
395 | @check_method_requirements
396 | def post(self, *args, **kwargs):
397 | """Add / create relationship(s)
398 | """
399 | json_data = request.get_json()
400 |
401 | relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
402 |
403 | if 'data' not in json_data:
404 | raise BadRequest('/data', 'You must provide data with a "data" route node')
405 | if isinstance(json_data['data'], dict):
406 | if 'type' not in json_data['data']:
407 | raise BadRequest('/data/type', 'Missing type in "data" node')
408 | if 'id' not in json_data['data']:
409 | raise BadRequest('/data/id', 'Missing id in "data" node')
410 | if json_data['data']['type'] != related_type_:
411 | raise InvalidType('/data/type', 'The type field does not match the resource type')
412 | if isinstance(json_data['data'], list):
413 | for obj in json_data['data']:
414 | if 'type' not in obj:
415 | raise BadRequest('/data/type', 'Missing type in "data" node')
416 | if 'id' not in obj:
417 | raise BadRequest('/data/id', 'Missing id in "data" node')
418 | if obj['type'] != related_type_:
419 | raise InvalidType('/data/type', 'The type provided does not match the resource type')
420 |
421 | self.before_post(args, kwargs, json_data=json_data)
422 |
423 | obj_, updated = self._data_layer.create_relationship(json_data,
424 | model_relationship_field,
425 | related_id_field,
426 | kwargs)
427 |
428 | qs = QSManager(request.args, self.schema)
429 | includes = qs.include
430 | if relationship_field not in qs.include:
431 | includes.append(relationship_field)
432 | schema = compute_schema(self.schema, dict(), qs, includes)
433 |
434 | if updated is False:
435 | return '', 204
436 |
437 | result = schema.dump(obj_).data
438 | if result.get('links', {}).get('self') is not None:
439 | result['links']['self'] = request.path
440 | self.after_post(result)
441 | return result, 200
442 |
443 | @check_method_requirements
444 | def patch(self, *args, **kwargs):
445 | """Update a relationship
446 | """
447 | json_data = request.get_json()
448 |
449 | relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
450 |
451 | if 'data' not in json_data:
452 | raise BadRequest('/data', 'You must provide data with a "data" route node')
453 | if isinstance(json_data['data'], dict):
454 | if 'type' not in json_data['data']:
455 | raise BadRequest('/data/type', 'Missing type in "data" node')
456 | if 'id' not in json_data['data']:
457 | raise BadRequest('/data/id', 'Missing id in "data" node')
458 | if json_data['data']['type'] != related_type_:
459 | raise InvalidType('/data/type', 'The type field does not match the resource type')
460 | if isinstance(json_data['data'], list):
461 | for obj in json_data['data']:
462 | if 'type' not in obj:
463 | raise BadRequest('/data/type', 'Missing type in "data" node')
464 | if 'id' not in obj:
465 | raise BadRequest('/data/id', 'Missing id in "data" node')
466 | if obj['type'] != related_type_:
467 | raise InvalidType('/data/type', 'The type provided does not match the resource type')
468 |
469 | self.before_patch(args, kwargs, json_data=json_data)
470 |
471 | obj_, updated = self._data_layer.update_relationship(json_data,
472 | model_relationship_field,
473 | related_id_field,
474 | kwargs)
475 |
476 | qs = QSManager(request.args, self.schema)
477 | includes = qs.include
478 | if relationship_field not in qs.include:
479 | includes.append(relationship_field)
480 | schema = compute_schema(self.schema, dict(), qs, includes)
481 |
482 | if updated is False:
483 | return '', 204
484 |
485 | result = schema.dump(obj_).data
486 | if result.get('links', {}).get('self') is not None:
487 | result['links']['self'] = request.path
488 | self.after_patch(result)
489 | return result, 200
490 |
491 | @check_method_requirements
492 | def delete(self, *args, **kwargs):
493 | """Delete relationship(s)
494 | """
495 | json_data = request.get_json()
496 |
497 | relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
498 |
499 | if 'data' not in json_data:
500 | raise BadRequest('/data', 'You must provide data with a "data" route node')
501 | if isinstance(json_data['data'], dict):
502 | if 'type' not in json_data['data']:
503 | raise BadRequest('/data/type', 'Missing type in "data" node')
504 | if 'id' not in json_data['data']:
505 | raise BadRequest('/data/id', 'Missing id in "data" node')
506 | if json_data['data']['type'] != related_type_:
507 | raise InvalidType('/data/type', 'The type field does not match the resource type')
508 | if isinstance(json_data['data'], list):
509 | for obj in json_data['data']:
510 | if 'type' not in obj:
511 | raise BadRequest('/data/type', 'Missing type in "data" node')
512 | if 'id' not in obj:
513 | raise BadRequest('/data/id', 'Missing id in "data" node')
514 | if obj['type'] != related_type_:
515 | raise InvalidType('/data/type', 'The type provided does not match the resource type')
516 |
517 | self.before_delete(args, kwargs, json_data=json_data)
518 |
519 | obj_, updated = self._data_layer.delete_relationship(json_data,
520 | model_relationship_field,
521 | related_id_field,
522 | kwargs)
523 |
524 | qs = QSManager(request.args, self.schema)
525 | includes = qs.include
526 | if relationship_field not in qs.include:
527 | includes.append(relationship_field)
528 | schema = compute_schema(self.schema, dict(), qs, includes)
529 |
530 | status_code = 200 if updated is True else 204
531 | result = schema.dump(obj_).data
532 | if result.get('links', {}).get('self') is not None:
533 | result['links']['self'] = request.path
534 | self.after_delete(result)
535 | return result, status_code
536 |
537 | def _get_relationship_data(self):
538 | """Get useful data for relationship management
539 | """
540 | relationship_field = request.path.split('/')[-1]
541 | if current_app.config.get('DASHERIZE_API') == True:
542 | relationship_field = relationship_field.replace('-', '_')
543 |
544 | if relationship_field not in get_relationships(self.schema).values():
545 | raise RelationNotFound('', "{} has no attribute {}".format(self.schema.__name__, relationship_field))
546 |
547 | related_type_ = self.schema._declared_fields[relationship_field].type_
548 | related_id_field = self.schema._declared_fields[relationship_field].id_field
549 | model_relationship_field = get_model_field(self.schema, relationship_field)
550 |
551 | return relationship_field, model_relationship_field, related_type_, related_id_field
552 |
553 | def before_get(self, args, kwargs):
554 | pass
555 |
556 | def after_get(self, result):
557 | pass
558 |
559 | def before_post(self, args, kwargs, json_data=None):
560 | pass
561 |
562 | def after_post(self, result):
563 | pass
564 |
565 | def before_patch(self, args, kwargs, json_data=None):
566 | pass
567 |
568 | def after_patch(self, result):
569 | pass
570 |
571 | def before_delete(self, args, kwargs, json_data=None):
572 | pass
573 |
574 | def after_delete(self, result):
575 | pass
576 |
--------------------------------------------------------------------------------
/flask_rest_jsonapi/schema.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from marshmallow import class_registry
4 | from marshmallow.base import SchemaABC
5 | from marshmallow_jsonapi.fields import Relationship
6 |
7 | from flask_rest_jsonapi.exceptions import InvalidField, InvalidInclude
8 |
9 |
10 | def compute_schema(schema_cls, default_kwargs, qs, include):
11 | """Compute a schema around compound documents and sparse fieldsets
12 |
13 | :param Schema schema_cls: the schema class
14 | :param dict default_kwargs: the schema default kwargs
15 | :param QueryStringManager qs: qs
16 | :param list include: the relation field to include data from
17 |
18 | :return Schema schema: the schema computed
19 | """
20 | # manage include_data parameter of the schema
21 | schema_kwargs = default_kwargs
22 | schema_kwargs['include_data'] = schema_kwargs.get('include_data', tuple())
23 |
24 | if include:
25 | for include_path in include:
26 | field = include_path.split('.')[0]
27 | if field not in schema_cls._declared_fields:
28 | raise InvalidInclude("{} has no attribute {}".format(schema_cls.__name__, field))
29 | elif not isinstance(schema_cls._declared_fields[field], Relationship):
30 | raise InvalidInclude("{} is not a relationship attribute of {}".format(field, schema_cls.__name__))
31 | schema_kwargs['include_data'] += (field, )
32 |
33 | # make sure id field is in only parameter unless marshamllow will raise an Exception
34 | if schema_kwargs.get('only') is not None and 'id' not in schema_kwargs['only']:
35 | schema_kwargs['only'] += ('id',)
36 |
37 | # create base schema instance
38 | schema = schema_cls(**schema_kwargs)
39 |
40 | # manage sparse fieldsets
41 | if schema.opts.type_ in qs.fields:
42 | # check that sparse fieldsets exists in the schema
43 | for field in qs.fields[schema.opts.type_]:
44 | if field not in schema.declared_fields:
45 | raise InvalidField("{} has no attribute {}".format(schema.__class__.__name__, field))
46 |
47 | tmp_only = set(schema.declared_fields.keys()) & set(qs.fields[schema.opts.type_])
48 | if schema.only:
49 | tmp_only &= set(schema.only)
50 | schema.only = tuple(tmp_only)
51 |
52 | # make sure again that id field is in only parameter unless marshamllow will raise an Exception
53 | if schema.only is not None and 'id' not in schema.only:
54 | schema.only += ('id',)
55 |
56 | # manage compound documents
57 | if include:
58 | for include_path in include:
59 | field = include_path.split('.')[0]
60 | relation_field = schema.declared_fields[field]
61 | related_schema_cls = schema.declared_fields[field].__dict__['_Relationship__schema']
62 | related_schema_kwargs = {}
63 | if isinstance(related_schema_cls, SchemaABC):
64 | related_schema_kwargs['many'] = related_schema_cls.many
65 | related_schema_kwargs['include_data'] = related_schema_cls.__dict__.get('include_data')
66 | related_schema_cls = related_schema_cls.__class__
67 | if isinstance(related_schema_cls, str):
68 | related_schema_cls = class_registry.get_class(related_schema_cls)
69 | if '.' in include_path:
70 | related_include = ['.'.join(include_path.split('.')[1:])]
71 | else:
72 | related_include = None
73 | related_schema = compute_schema(related_schema_cls, related_schema_kwargs, qs, related_include)
74 | relation_field.__dict__['_Relationship__schema'] = related_schema
75 |
76 | return schema
77 |
78 |
79 | def get_model_field(schema, field):
80 | """Get the model field of a schema field
81 |
82 | :param Schema schema: a marshmallow schema
83 | :param str field: the name of the schema field
84 | :return str: the name of the field in the model
85 | """
86 | if schema._declared_fields[field].attribute is not None:
87 | return schema._declared_fields[field].attribute
88 | return field
89 |
90 |
91 | def get_relationships(schema):
92 | """Return relationship mapping from schema to model
93 |
94 | :param Schema schema: a marshmallow schema
95 | :param list: list of dict with schema field and model field
96 | """
97 | return {get_model_field(schema, key): key for (key, value) in schema._declared_fields.items()
98 | if isinstance(value, Relationship)}
99 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | six
2 | Flask>=0.11
3 | marshmallow>=2.15.1,<3
4 | marshmallow_jsonapi
5 | sqlalchemy
6 | pytz
7 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
4 | [aliases]
5 | test=pytest
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | __version__ = '0.12.6.5'
5 |
6 |
7 | setup(
8 | name="Flask-REST-JSONAPI",
9 | version=__version__,
10 | description='Flask extension to create REST web api according to JSONAPI 1.0 specification with Flask, Marshmallow \
11 | and data provider of your choice (SQLAlchemy, MongoDB, ...)',
12 | url='https://github.com/miLibris/flask-rest-jsonapi',
13 | author='miLibris API Team',
14 | author_email='pf@milibris.net',
15 | license='MIT',
16 | classifiers=[
17 | 'Framework :: Flask',
18 | 'Programming Language :: Python :: 2',
19 | 'Programming Language :: Python :: 2.7',
20 | 'Programming Language :: Python :: 3',
21 | 'Programming Language :: Python :: 3.4',
22 | 'Programming Language :: Python :: 3.5',
23 | 'License :: OSI Approved :: MIT License',
24 | ],
25 | keywords='web api rest jsonapi flask sqlalchemy marshmallow',
26 | packages=find_packages(exclude=['tests']),
27 | zip_safe=False,
28 | platforms='any',
29 | install_requires=['six',
30 | 'Flask>=0.11',
31 | 'marshmallow>=2.15.1,<3',
32 | 'marshmallow_jsonapi==0.23.2',
33 | 'sqlalchemy'],
34 | setup_requires=['pytest-runner'],
35 | tests_require=['pytest'],
36 | extras_require={'tests': 'pytest', 'docs': 'sphinx'}
37 | )
38 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossasia/flask-rest-jsonapi/a03408bffd5ef96bf3b8abe3a30d147db46fbe47/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import pytest
4 |
5 | from flask import Flask
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def app():
10 | app = Flask(__name__)
11 | return app
12 |
13 |
14 | @pytest.yield_fixture(scope="session")
15 | def client(app):
16 | return app.test_client()
17 |
--------------------------------------------------------------------------------