├── .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 | --------------------------------------------------------------------------------