Repository: marshmallow-code/marshmallow-jsonapi Branch: dev Commit: e7273b4c3e6b Files: 39 Total size: 158.0 KB Directory structure: gitextract_g53knd5l/ ├── .gitignore ├── .pre-commit-config.yaml ├── .readthdocs.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── azure-pipelines.yml ├── docs/ │ ├── Makefile │ ├── api_reference.rst │ ├── authors.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributing.rst │ ├── index.rst │ ├── license.rst │ ├── make.bat │ ├── quickstart.rst │ └── requirements.txt ├── examples/ │ └── flask_example.py ├── marshmallow_jsonapi/ │ ├── __init__.py │ ├── exceptions.py │ ├── fields.py │ ├── flask.py │ ├── schema.py │ └── utils.py ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── base.py │ ├── conftest.py │ ├── test_fields.py │ ├── test_flask.py │ ├── test_options.py │ ├── test_schema.py │ └── test_utils.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .cache nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Complexity output/*.html output/*/index.html # Sphinx docs/_build README.html _sandbox .konchrc # Virtual Environment env venv .python-version ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/python/black rev: 22.1.0 hooks: - id: black language_version: python3 - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-bugbear==22.1.11] - repo: https://github.com/asottile/blacken-docs rev: v1.12.1 hooks: - id: blacken-docs additional_dependencies: [black==22.1.0] ================================================ FILE: .readthdocs.yml ================================================ version: 2 sphinx: configuration: docs/conf.py formats: all python: version: 3.7 install: - requirements: docs/requirements.txt ================================================ FILE: AUTHORS.rst ================================================ ******* Authors ******* Lead ==== - Steven Loria `@sloria `_ Contributors (chronological) ============================ - Jotham Apaloo `@jo-tham `_ - Anders Steinlein `@asteinlein `_ - `@floqqi `_ - Colton Allen `@cmanallen `_ - Dominik Steinberger `@ZeeD26 `_ - Tim Mundt `@Tim-Erwin `_ - Brandon Wood `@woodb `_ - Frazer McLean `@RazerM `_ - J Rob Gant `@rgant `_ - Dan Poland `@danpoland `_ - Pierre CHAISY `@akira-dev `_ - `@mrhanky17 `_ - Mark Hall `@scmmmh `_ - Scott Werner `@scottwernervt `_ - Michael Dodsworth `@mdodsworth `_ - Mathieu Alorent `@kumy `_ - Grant Harris `@grantHarris `_ - Robert Sawicki `@ww3pl `_ - `@aberres `_ - George Alton `@georgealton `_ - Areeb Jamal `@iamareebjamal `_ - Suren Khorenyan `@mahenzon `_ - Karthikeyan Singaravelan `@tirkarthi `_ ================================================ FILE: CHANGELOG.rst ================================================ ********* Changelog ********* 0.24.0 (2020-12-27) =================== Deprecations/Removals: * Drop support for marshmallow 2, which is now EOL (:pr:`332`). Bug fixes: * Fix behavior when serializing ``None`` (:pr:`302`). Thanks :user:`mahenzon`. Other changes: * Test against Python 3.8 and 3.9 (:pr:`332`). 0.23.2 (2020-07-20) =================== Bug fixes: * Import from `collections.abc` for forward-compatibility with Python 3.10 (:issue:`318`). Thanks :user:`tirkarthi`. 0.23.1 (2020-03-22) =================== Bug fixes: * Fix nested fields validation error formatting (:issue:`120`). Thanks :user:`mahenzon` and :user:`debonzi` for the PRs. 0.23.0 (2020-02-02) =================== * Improve performance of link generation from `Relationship` (:issue:`277`). Thanks :user:`iamareebjamal` for reporting and fixing. 0.22.0 (2019-09-15) =================== Deprecation/Removals: * Drop support for Python 2.7 and 3.5. Only Python>=3.6 is supported (:issue:`251`). * Drop support for marshmallow 3 pre-releases. Only stable versions >=2.15.2 are supported. * Remove ``fields.Meta``. Bug fixes: * Address ``DeprecationWarning`` raised by ``Field.fail`` on marshmallow 3. 0.21.2 (2019-07-01) =================== Bug fixes: * marshmallow 3.0.0rc7 compatibility (:pr:`233`). Other changes: * Format with pyupgrade and black (:pr:`235`). * Switch to Azure Pipelines for CI (:pr:`234`). 0.21.1 (2019-05-05) =================== Bug fixes: * marshmallow 3.0.0rc6 cmpatibility (:pr:`221`). 0.21.0 (2018-12-16) =================== Bug fixes: * *Backwards-incompatible*: Revert URL quoting introduced in 0.20.2 (:issue:`184`). If you need quoting, override `Schema.generate_url`. .. code-block:: python from marshmallow_jsonapi import Schema from werkzeug.urls import url_fix class MySchema(Schema): def generate_url(self, link, **kwargs): url = super().generate_url(link, **kwargs) return url_fix(url) Thanks :user:`kgutwin` for reporting the issue. * Fix `Relationship` deserialization behavior when ``required=False`` (:issue:`177`). Thanks :user:`aberres` for reporting and :user:`scottwernervt` for the fix. Other changes: * Test against Python 3.7. 0.20.5 (2018-10-27) =================== Bug fixes: * Fix deserializing ``id`` field to non-string types (:pr:`179`). Thanks :user:`aberres` for the catch and patch. 0.20.4 (2018-10-04) =================== Bug fixes: * Fix bug where multi-level nested relationships would not be properly deserialized (:issue:`127`). Thanks :user:`ww3pl` for the catch and patch. 0.20.3 (2018-09-13) =================== Bug fixes: * Fix missing load validation when data is not a collection but many=True (:pr:`161`). Thanks :user:`grantHarris`. 0.20.2 (2018-08-15) =================== Bug fixes: * Fix issues where generated URLs are unquoted (:pr:`147`). Thanks :user:`grantHarris`. Other changes: * Fix tests against marshmallow 3.0.0b13. 0.20.1 (2018-07-15) =================== Bug fixes: * Fix deserializing ``missing`` with a `Relationship` field (:issue:`130`). Thanks :user:`kumy` for the catch and patch. 0.20.0 (2018-06-10) =================== Bug fixes: * Fix serialization of ``id`` for ``Relationship`` fields when ``attribute`` is set (:issue:`69`). Thanks :user:`jordal` for reporting and thanks :user:`scottwernervt` for the fix. Note: The above fix could break some code that set ``Relationship.id_field`` before instantiating it. Set ``Relationship.default_id_field`` instead. .. code-block:: python # before fields.Relationship.id_field = "item_id" # after fields.Relationship.default_id_field = "item_id" Support: * Test refactoring and various doc improvements (:issue:`63`, :issue:`86`, :issue:`121,` and :issue:`122`). Thanks :user:`scottwernervt`. 0.19.0 (2018-05-27) =================== Features: * Schemas passed to ``fields.Relationship`` will inherit context from the parent schema (:issue:`84`). Thanks :user:`asteinlein` and :user:`scottwernervt` for the PRs. 0.18.0 (2018-05-19) =================== Features: * Add ``fields.ResourceMeta`` for serializing a resource-level meta object (:issue:`107`). Thanks :user:`scottwernervt`. Other changes: * *Backwards-incompatible*: Drop official support for Python 3.4. 0.17.0 (2018-04-29) =================== Features: * Add support for marshmallow 3 (:issue:`97`). Thanks :user:`rockmnew`. * Thanks :user:`mdodsworth` for helping with :issue:`101`. * Move meta information object to document top level (:issue:`95`). Thanks :user:`scottwernervt`. 0.16.0 (2017-11-08) =================== Features: * Add support for exluding or including nested fields on relationships (:issue:`94`). Thanks :user:`scottwernervt` for the PR. Other changes: * *Backwards-incompatible*: Drop support for marshmallow<2.8.0 0.15.1 (2017-08-23) =================== Bug fixes: * Fix pointer for ``id`` in error objects (:issue:`90`). Thanks :user:`rgant` for the catch and patch. 0.15.0 (2017-06-27) =================== Features: * ``Relationship`` field supports deserializing included data (:issue:`83`). Thanks :user:`anuragagarwal561994` for the suggestion and thanks :user:`asteinlein` for the PR. 0.14.0 (2017-04-30) =================== Features: * ``Relationship`` respects its passed ``Schema's`` ``get_attribute`` method when getting the ``id`` field for resource linkages (:issue:`80`). Thanks :user:`scmmmh` for the PR. 0.13.0 (2017-04-18) =================== Features: * Add support for including deeply nested relationships in compount documents (:issue:`61`). Thanks :user:`mrhanky17` for the PR. 0.12.0 (2017-04-16) =================== Features: * Use default attribute value instead of raising exception if relationship is ``None`` on ``Relationship`` field (:issue:`75`). Thanks :user:`akira-dev`. 0.11.1 (2017-04-06) =================== Bug fixes: - Fix formatting JSON pointer when serializing an invalid object at index 0 (:issue:`77`). Thanks :user:`danpoland` for the catch and patch. 0.11.0 (2017-03-12) =================== Bug fixes: * Fix compatibility with marshmallow 3.x. Other changes: * *Backwards-incompatible*: Remove unused `utils.get_value_or_raise` function. 0.10.2 (2017-03-08) =================== Bug fixes: * Fix format of error object returned when ``data`` key is not included in input (:issue:`66`). Thanks :user:`RazerM`. * Fix serializing compound documents when ``Relationship`` is passed a schema class and ``many=True`` (:issue:`67`). Thanks :user:`danpoland` for the catch and patch. 0.10.1 (2017-02-05) =================== Bug fixes: * Serialize ``None`` and empty lists (``[]``) to valid JSON-API objects (:issue:`58`). Thanks :user:`rgant` for reporting and sending a PR. 0.10.0 (2017-01-05) =================== Features: * Add ``fields.Meta`` for (de)serializing ``meta`` data on resource objects (:issue:`28`). Thanks :user:`rubdos` for the suggestion and initial work. Thanks :user:`RazerM` for the PR. Other changes: * Test against Python 3.6. 0.9.0 (2016-10-08) ================== Features: * Add Flask-specific schema with class Meta options for self link generation: ``self_view``, ``self_view_kwargs``, and ``self_view_many`` (:issue:`51`). Thanks :user:`asteinlein`. Bug fixes: * Fix formatting of validation error messages on newer versions of marshmallow. Other changes: * Drop official support for Python 3.3. 0.8.0 (2016-06-20) ================== Features: * Add support for compound documents (:issue:`11`). Thanks :user:`Tim-Erwin` and :user:`woodb` for implementing this. * *Backwards-incompatible*: Remove ``include_data`` parameter from ``Relationship``. Use ``include_resource_linkage`` instead. 0.7.1 (2016-05-08) ================== Bug fixes: * Format correction for error objects (:issue:`47`). Thanks :user:`ZeeD26` for the PR. 0.7.0 (2016-04-03) ================== Features: * Correctly format ``messages`` attribute of ``ValidationError`` raised when ``type`` key is missing in input (:issue:`43`). Thanks :user:`ZeeD26` for the catch and patch. * JSON pointers for error objects for relationships will point to the ``data`` key (:issue:`41`). Thanks :user:`cmanallen` for the PR. 0.6.0 (2016-03-24) ================== Features: * ``Relationship`` deserialization improvements: properly validate to-one and to-many relatinoships and validate the presense of the ``data`` key (:issue:`37`). Thanks :user:`cmanallen` for the PR. * ``attributes`` is no longer a required key in the ``data`` object (:issue:`#39`, :issue:`42`). Thanks :user:`ZeeD26` for reporting and :user:`cmanallen` for the PR. * Added ``id`` serialization (:issue:`39`). Thanks again :user:`cmanallen`. 0.5.0 (2016-02-08) ================== Features: * Add relationship deserialization (:issue:`15`). * Allow serialization of foreign key attributes (:issue:`32`). * Relationship IDs serialize to strings, as is required by JSON-API (:issue:`31`). * ``Relationship`` field respects ``dump_to`` parameter (:issue:`33`). Thanks :user:`cmanallen` for all of these changes. Other changes: * The minimum supported marshmallow version is 2.3.0. 0.4.2 (2015-12-21) ================== Bug fixes: * Relationship names are inflected when appropriate (:issue:`22`). Thanks :user:`angelosarto` for reporting. 0.4.1 (2015-12-19) ================== Bug fixes: * Fix serializing null and empty relationships with ``flask.Relationship`` (:issue:`24`). Thanks :user:`floqqi` for the catch and patch. 0.4.0 (2015-12-06) ================== * Correctly serialize null and empty relationships (:issue:`10`). Thanks :user:`jo-tham` for the PR. * Add ``self_url``, ``self_url_kwargs``, and ``self_url_many`` class Meta options for adding ``self`` links. Thanks :user:`asteinlein` for the PR. 0.3.0 (2015-10-18) ================== * *Backwards-incompatible*: Replace ``HyperlinkRelated`` with ``Relationship`` field. Supports related links (``related``), relationship links (``self``), and resource linkages. * *Backwards-incompatible*: Validate and deserialize JSON API-formatted request payloads. * Fix error formatting when ``many=True``. * Fix error formatting in strict mode. 0.2.2 (2015-09-26) ================== * Fix for marshmallow 2.0.0 compat. 0.2.1 (2015-09-16) ================== * Compatibility with marshmallow>=2.0.0rc2. 0.2.0 (2015-09-13) ================== Features: * Add framework-independent ``HyperlinkRelated`` field. * Support inflection of attribute names via the ``inflect`` class Meta option. Bug fixes: * Fix for making ``HyperlinkRelated`` read-only by defualt. Support: * Docs updates. * Tested on Python 3.5. 0.1.0 (2015-09-12) ================== * First PyPI release. * Include Schema that serializes objects to resource objects. * Flask-compatible HyperlinkRelate field for serializing relationships. * Errors are formatted as JSON API errror objects. ================================================ FILE: CONTRIBUTING.rst ================================================ Contributing Guidelines ======================= Questions, Feature Requests, Bug Reports, and Feedback… ------------------------------------------------------- …should all be reported on the `Github Issue Tracker`_ . .. _`Github Issue Tracker`: https://github.com/marshmallow-code/marshmallow-jsonapi/issues?state=open Setting Up for Local Development -------------------------------- 1. Fork marshmallow-jsonapi_ on Github. :: $ git clone https://github.com/marshmallow-code/marshmallow-jsonapi.git $ cd marshmallow-jsonapi 2. Install development requirements. **It is highly recommended that you use a virtualenv.** Use the following command to install an editable version of marshmallow-jsonapi along with its development requirements. :: # After activating your virtualenv $ pip install -e '.[dev]' 3. Install the pre-commit hooks, which will format and lint your git staged files. :: # The pre-commit CLI was installed above $ pre-commit install Git Branch Structure -------------------- Marshmallow abides by the following branching model: ``dev`` Current development branch. **New features should branch off here**. ``X.Y-line`` Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.** The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes. **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. Pull Requests -------------- 1. Create a new local branch. :: $ git checkout -b name-of-feature dev 2. Commit your changes. Write `good commit messages `_. :: $ git commit -m "Detailed commit message" $ git push origin name-of-feature 3. Before submitting a pull request, check the following: - If the pull request adds functionality, it is tested and the docs are updated. - You've added yourself to ``AUTHORS.rst``. 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `CI `_ build must be passing before your pull request is merged. Running tests ------------- To run all To run all tests: :: $ pytest To run syntax checks: :: $ tox -e lint (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: $ tox Documentation ------------- Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_. To build the docs in "watch" mode: :: $ tox -e watch-docs Changes in the `docs/` directory will automatically trigger a rebuild. .. _Sphinx: http://sphinx.pocoo.org/ .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html .. _marshmallow-jsonapi: https://github.com/marshmallow-code/marshmallow-jsonapi ================================================ FILE: LICENSE ================================================ Copyright 2015-2020 Steven Loria and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include *.rst LICENSE recursive-include tests * recursive-include docs * recursive-include examples * recursive-exclude docs *.pyc recursive-exclude docs *.pyo recursive-exclude tests *.pyc recursive-exclude tests *.pyo recursive-exclude examples *.pyc recursive-exclude examples *.pyo prune docs/_build ================================================ FILE: README.rst ================================================ ******************* marshmallow-jsonapi ******************* .. image:: https://badgen.net/pypi/v/marshmallow-jsonapi :target: https://pypi.org/project/marshmallow-jsonapi/ :alt: PyPI version .. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.marshmallow-jsonapi?branchName=dev :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=7&branchName=dev :alt: Build status .. image:: https://readthedocs.org/projects/marshmallow-jsonapi/badge/ :target: https://marshmallow-jsonapi.readthedocs.io/ :alt: Documentation .. image:: https://badgen.net/badge/marshmallow/3 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html :alt: marshmallow 3 compatible .. image:: https://badgen.net/badge/code%20style/black/000 :target: https://github.com/ambv/black :alt: code style: black Homepage: http://marshmallow-jsonapi.readthedocs.io/ JSON API 1.0 (`https://jsonapi.org `_) formatting with `marshmallow `_. marshmallow-jsonapi provides a simple way to produce JSON API-compliant data in any Python web framework. .. code-block:: python from marshmallow_jsonapi import Schema, fields class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = fields.Relationship( "/authors/{author_id}", related_url_kwargs={"author_id": ""} ) comments = fields.Relationship( "/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, # Include resource linkage many=True, include_resource_linkage=True, type_="comments", ) class Meta: type_ = "posts" post_schema = PostSchema() post_schema.dump(post) # { # "data": { # "id": "1", # "type": "posts" # "attributes": { # "title": "JSON API paints my bikeshed!" # }, # "relationships": { # "author": { # "links": { # "related": "/authors/9" # } # }, # "comments": { # "links": { # "related": "/posts/1/comments/" # } # "data": [ # {"id": 5, "type": "comments"}, # {"id": 12, "type": "comments"} # ], # } # }, # } # } Installation ============ :: pip install marshmallow-jsonapi Documentation ============= Full documentation is available at https://marshmallow-jsonapi.readthedocs.io/. Requirements ============ - Python >= 3.6 Project Links ============= - Docs: http://marshmallow-jsonapi.readthedocs.io/ - Changelog: http://marshmallow-jsonapi.readthedocs.io/en/latest/changelog.html - Contributing Guidelines: https://marshmallow-jsonapi.readthedocs.io/en/latest/contributing.html - PyPI: https://pypi.python.org/pypi/marshmallow-jsonapi - Issues: https://github.com/marshmallow-code/marshmallow-jsonapi/issues License ======= MIT licensed. See the bundled `LICENSE `_ file for more details. ================================================ FILE: azure-pipelines.yml ================================================ trigger: branches: include: [dev, test-me-*] tags: include: ["*"] # Run builds nightly to catch incompatibilities with new marshmallow releases schedules: - cron: "0 0 * * *" displayName: Daily midnight build branches: include: - dev always: "true" resources: repositories: - repository: sloria type: github endpoint: github name: sloria/azure-pipeline-templates ref: refs/heads/sloria jobs: - template: job--python-tox.yml@sloria parameters: toxenvs: - lint - py36-marshmallow3 - py37-marshmallow3 - py38-marshmallow3 - py39-marshmallow3 - py39-marshmallowdev - docs os: linux - template: job--pypi-release.yml@sloria parameters: dependsOn: - tox_linux ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/api_reference.rst ================================================ .. _api: ************* API Reference ************* Core ==== .. automodule:: marshmallow_jsonapi :members: Fields ====== .. automodule:: marshmallow_jsonapi.fields :members: Flask ===== .. automodule:: marshmallow_jsonapi.flask :members: Exceptions ========== .. automodule:: marshmallow_jsonapi.exceptions :members: Utilities ========= .. automodule:: marshmallow_jsonapi.utils :members: ================================================ FILE: docs/authors.rst ================================================ .. include:: ../AUTHORS.rst ================================================ FILE: docs/changelog.rst ================================================ .. _changelog: .. include:: ../CHANGELOG.rst ================================================ FILE: docs/conf.py ================================================ import datetime as dt import os import sys sys.path.insert(0, os.path.abspath("..")) import marshmallow_jsonapi # noqa: E402 extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_issues", ] primary_domain = "py" default_role = "py:obj" intersphinx_mapping = { "python": ("http://python.readthedocs.io/en/latest/", None), "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None), } issues_github_path = "marshmallow-code/marshmallow-jsonapi" source_suffix = ".rst" master_doc = "index" project = "marshmallow-jsonapi" copyright = f"Steven Loria {dt.datetime.utcnow():%Y}" version = release = marshmallow_jsonapi.__version__ exclude_patterns = ["_build"] # THEME # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get("READTHEDOCS", None) == "True" if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] ================================================ FILE: docs/contributing.rst ================================================ .. include:: ../CONTRIBUTING.rst ================================================ FILE: docs/index.rst ================================================ ******************* marshmallow-jsonapi ******************* Release v\ |version|. (:ref:`Changelog `) JSON API 1.0 (`https://jsonapi.org `_) formatting with `marshmallow `_. marshmallow-jsonapi provides a simple way to produce JSON API-compliant data in any Python web framework. .. code-block:: python from marshmallow_jsonapi import Schema, fields class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = fields.Relationship( related_url="/authors/{author_id}", related_url_kwargs={"author_id": ""}, ) comments = fields.Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, # Include resource linkage many=True, include_resource_linkage=True, type_="comments", ) class Meta: type_ = "posts" strict = True post_schema = PostSchema() post_schema.dump(post) # { # "data": { # "id": "1", # "type": "posts" # "attributes": { # "title": "JSON API paints my bikeshed!" # }, # "relationships": { # "author": { # "links": { # "related": "/authors/9" # } # }, # "comments": { # "data": [ # {"id": 5, "type": "comments"}, # {"id": 12, "type": "comments"} # ], # "links": { # "related": "/posts/1/comments/" # } # } # }, # } # } Installation ============ :: pip install marshmallow-jsonapi Guide ===== .. toctree:: :maxdepth: 2 quickstart API Reference ============= .. toctree:: :maxdepth: 2 api_reference Project info ============ .. toctree:: :maxdepth: 1 changelog authors contributing license Links ===== - `marshmallow-jsonapi @ GitHub `_ - `marshmallow-jsonapi @ PyPI `_ - `Issue Tracker `_ ================================================ FILE: docs/license.rst ================================================ ******* License ******* .. literalinclude:: ../LICENSE ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: docs/quickstart.rst ================================================ ********** Quickstart ********** .. note:: The following guide assumes some familiarity with the marshmallow API. To learn more about marshmallow, see its official documentation at `https://marshmallow.readthedocs.io `_. Declaring schemas ================= Let’s start with a basic post “model”. .. code-block:: python class Post: def __init__(self, id, title): self.id = id self.title = title Declare your schemas as you would with marshmallow. A :class:`.Schema` **MUST** define: - An ``id`` field - The ``type_`` class Meta option It is **RECOMMENDED** to set strict mode to `True`. Automatic self-linking is supported through these Meta options: - ``self_url`` specifies the URL to the resource itself - ``self_url_kwargs`` specifies replacement fields for `self_url` - ``self_url_many`` specifies the URL the resource when a collection (many) are serialized .. code-block:: python from marshmallow_jsonapi import Schema, fields class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() class Meta: type_ = "posts" self_url = "/posts/{id}" self_url_kwargs = {"id": ""} self_url_many = "/posts/" These URLs can be auto-generated by specifying ``self_view``, ``self_view_kwargs`` and ``self_view_many`` instead when using the :ref:`flask-integration`. Serialization ============= Objects will be serialized to `JSON API documents `_ with primary data. .. code-block:: python post = Post(id="1", title="Django is Omakase") PostSchema().dump(post) # { # 'data': { # 'id': '1', # 'type': 'posts', # 'attributes': {'title': 'Django is Omakase'}, # 'links': {'self': '/posts/1'} # }, # 'links': {'self': '/posts/1'} # } Relationships ============= The `Relationship ` field is used to serialize `relationship objects `_. For example, a Post may have an author and comments associated with it. .. code-block:: python class User: def __init__(self, id, name): self.id = id self.name = name class Comment: def __init__(self, id, body, author): self.id = id self.body = body self.author = author class Post: def __init__(self, id, title, author, comments=None): self.id = id self.title = title self.author = author # User object self.comments = [] if comments is None else comments # Comment objects To serialize links, pass a URL format string and a dictionary of keyword arguments. String arguments enclosed in `< >` will be interpreted as attributes to pull from the object being serialized. The relationship links can automatically be generated from Flask view names when using the :ref:`flask-integration`. .. code-block:: python :emphasize-lines: 5-10 class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = fields.Relationship( self_url="/posts/{post_id}/relationships/author", self_url_kwargs={"post_id": ""}, related_url="/authors/{author_id}", related_url_kwargs={"author_id": ""}, ) class Meta: type_ = "posts" user = User(id="94", name="Laura") post = Post(id="1", title="Django is Omakase", author=user) PostSchema().dump(post) # { # 'data': { # 'id': '1', # 'type': 'posts', # 'attributes': {'title': 'Django is Omakase'}, # 'relationships': { # 'author': { # 'links': { # 'self': '/posts/1/relationships/author', # 'related': '/authors/94' # } # } # } # } # } Resource linkages ----------------- You can serialize `resource linkages `_ by passing ``include_resource_linkage=True`` and the resource ``type_`` argument. .. code-block:: python :emphasize-lines: 10-12 class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = fields.Relationship( self_url="/posts/{post_id}/relationships/author", self_url_kwargs={"post_id": ""}, related_url="/authors/{author_id}", related_url_kwargs={"author_id": ""}, # Include resource linkage include_resource_linkage=True, type_="users", ) class Meta: type_ = "posts" PostSchema().dump(post) # { # 'data': { # 'id': '1', # 'type': 'posts', # 'attributes': {'title': 'Django is Omakase'}, # 'relationships': { # 'author': { # 'data': {'type': 'users', 'id': '94'}, # 'links': { # 'self': '/posts/1/relationships/author', # 'related': '/authors/94' # } # } # } # } # } Compound documents ------------------ `Compound documents `_ allow to include related resources into the request with the primary resource. In order to include objects, you have to define a :class:`.Schema` for the respective relationship, which will be used to render those objects. .. code-block:: python :emphasize-lines: 10-11 class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() comments = fields.Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", # define a schema for rendering included data schema="CommentSchema", ) author = fields.Relationship( self_url="/posts/{post_id}/relationships/author", self_url_kwargs={"post_id": ""}, related_url="/authors/{author_id}", related_url_kwargs={"author_id": ""}, include_resource_linkage=True, type_="users", ) class Meta: type_ = "posts" class CommentSchema(Schema): id = fields.Str(dump_only=True) body = fields.Str() author = fields.Relationship( self_url="/comments/{comment_id}/relationships/author", self_url_kwargs={"comment_id": ""}, related_url="/comments/{author_id}", related_url_kwargs={"author_id": ""}, type_="users", # define a schema for rendering included data schema="UserSchema", ) class Meta: type_ = "comments" class UserSchema(Schema): id = fields.Str(dump_only=True) name = fields.Str() class Meta: type_ = "users" Just as with nested fields the ``schema`` can be a class or a string with a simple or fully qualified class name. Make sure to import the schema beforehand. Now you can include some data in a dump by specifying the ``include_data`` argument (also supports nested relations via the dot syntax). .. code-block:: python :emphasize-lines: 8 armin = User(id="101", name="Armin") laura = User(id="94", name="Laura") steven = User(id="23", name="Steven") comments = [ Comment(id="5", body="Marshmallow is sweet like sugar!", author=steven), Comment(id="12", body="Flask is Fun!", author=armin), ] post = Post(id="1", title="Django is Omakase", author=laura, comments=comments) PostSchema(include_data=("comments", "comments.author")).dump(post) # { # 'data': { # 'id': '1', # 'type': 'posts', # 'attributes': {'title': 'Django is Omakase'}, # 'relationships': { # 'author': { # 'data': {'type': 'users', 'id': '94'}, # 'links': { # 'self': '/posts/1/relationships/author', # 'related': '/authors/94' # } # }, # 'comments': { # 'data': [ # {'type': 'comments', 'id': '5'}, # {'type': 'comments', 'id': '12'} # ], # 'links': { # 'related': '/posts/1/comments' # } # } # } # }, # 'included': [ # { # 'id': '5', # 'type': 'comments', # 'attributes': {'body': 'Marshmallow is sweet like sugar!'}, # 'relationships': { # 'author': { # 'data': {'type': 'users', 'id': '23'}, # 'links': { # 'self': '/comments/5/relationships/author', # 'related': '/comments/23' # } # } # } # }, # { # 'id': '12', # 'type': 'comments', # 'attributes': {'body': 'Flask is Fun!'}, # 'relationships': { # 'author': { # 'data': {'type': 'users', 'id': '101'}, # 'links': { # 'self': '/comments/12/relationships/author', # 'related': '/comments/101' # } # } # }, # # }, # { # 'id': '23', # 'type': 'users', # 'attributes': {'name': 'Steven'} # }, # { # 'id': '101', # 'type': 'users', # 'attributes': {'name': 'Armin'} # } # ] # } Meta Information ================ The :class:`.DocumentMeta` field is used to serialize the meta object within a `document’s "top level" `_. .. code-block:: python :emphasize-lines: 6 from marshmallow_jsonapi import Schema, fields class UserSchema(Schema): id = fields.Str(dump_only=True) name = fields.Str() document_meta = fields.DocumentMeta() class Meta: type_ = "users" user = {"name": "Alice", "document_meta": {"page": {"offset": 10}}} UserSchema().dump(user) # { # "meta": { # "page": { # "offset": 10 # } # }, # "data": { # "id": "1", # "type": "users" # "attributes": {"name": "Alice"}, # } # } The :class:`.ResourceMeta` field is used to serialize the meta object within a `resource object `_. .. code-block:: python :emphasize-lines: 6 from marshmallow_jsonapi import Schema, fields class UserSchema(Schema): id = fields.Str(dump_only=True) name = fields.Str() resource_meta = fields.ResourceMeta() class Meta: type_ = "users" user = {"name": "Alice", "resource_meta": {"active": True}} UserSchema().dump(user) # { # "data": { # "type": "users", # "attributes": {"name": "Alice"}, # "meta": { # "active": true # } # } # } Errors ====== :func:`.Schema.load` and :func:`.Schema.validate` will return JSON API-formatted `Error objects `_. .. code-block:: python from marshmallow_jsonapi import Schema, fields from marshmallow import validate, ValidationError class AuthorSchema(Schema): id = fields.Str(dump_only=True) first_name = fields.Str(required=True) last_name = fields.Str(required=True) password = fields.Str(load_only=True, validate=validate.Length(6)) twitter = fields.Str() class Meta: type_ = "authors" author_data = { "data": {"type": "users", "attributes": {"first_name": "Dan", "password": "short"}} } AuthorSchema().validate(author_data) # { # 'errors': [ # { # 'detail': 'Missing data for required field.', # 'source': { # 'pointer': '/data/attributes/last_name' # } # }, # { # 'detail': 'Shorter than minimum length 6.', # 'source': { # 'pointer': '/data/attributes/password' # } # } # ] # } If an invalid "type" is passed in the input data, an :class:`.IncorrectTypeError` is raised. .. code-block:: python from marshmallow_jsonapi.exceptions import IncorrectTypeError author_data = { "data": { "type": "invalid-type", "attributes": { "first_name": "Dan", "last_name": "Gebhardt", "password": "verysecure", }, } } try: AuthorSchema().validate(author_data) except IncorrectTypeError as err: pprint(err.messages) # { # 'errors': [ # { # 'detail': 'Invalid type. Expected "users".', # 'source': { # 'pointer': '/data/type' # } # } # ] # } Inflection ========== You can optionally specify a function to transform attribute names. For example, you may decide to follow JSON API's `recommendation `_ to use "dasherized" names. .. code-block:: python from marshmallow_jsonapi import Schema, fields def dasherize(text): return text.replace("_", "-") class UserSchema(Schema): id = fields.Str(dump_only=True) first_name = fields.Str(required=True) last_name = fields.Str(required=True) class Meta: type_ = "users" inflect = dasherize UserSchema().dump(user) # { # 'data': { # 'id': '9', # 'type': 'users', # 'attributes': { # 'first-name': 'Dan', # 'last-name': 'Gebhardt' # } # } # } .. _flask-integration: Flask integration ================= marshmallow-jsonapi includes optional utilities to integrate with Flask. A Flask-specific schema in `marshmallow_jsonapi.flask` can be used to auto-generate self-links based on view names instead of hard-coding URLs. Additionally, the ``Relationship`` field in the `marshmallow_jsonapi.flask` module allows you to pass view names instead of path templates to generate relationship links. .. code-block:: python from marshmallow_jsonapi import fields from marshmallow_jsonapi.flask import Relationship, Schema class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = fields.Relationship( self_view="post_author", self_url_kwargs={"post_id": ""}, related_view="author_detail", related_view_kwargs={"author_id": ""}, ) comments = Relationship( related_view="post_comments", related_view_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", ) class Meta: type_ = "posts" self_view = "post_detail" self_view_kwargs = {"post_detail": ""} self_view_many = "posts_list" See `here `_ for a full example. ================================================ FILE: docs/requirements.txt ================================================ marshmallow>=2.0.0rc1 Flask==1.1.2 sphinx==3.5.3 sphinx-rtd-theme==0.5.0 sphinx-issues>=0.2.0 ================================================ FILE: examples/flask_example.py ================================================ from flask import Flask, request, jsonify ### MODELS ### class Model: def __init__(self, **kwargs): for key, val in kwargs.items(): setattr(self, key, val) class Comment(Model): pass class Author(Model): pass class Post(Model): pass ### MOCK DATABASE ### comment1 = Comment(id=1, body="First!") comment2 = Comment(id=2, body="I like XML better!") author1 = Author(id=1, first_name="Dan", last_name="Gebhardt", twitter="dgeb") post1 = Post( id=1, title="JSON API paints my bikeshed!", author=author1, comments=[comment1, comment2], ) db = {"comments": [comment1, comment2], "authors": [author1], "posts": [post1]} ### SCHEMAS ### from marshmallow import validate, ValidationError # noqa: E402 from marshmallow_jsonapi import fields # noqa: E402 from marshmallow_jsonapi.flask import Relationship, Schema # noqa: E402 class CommentSchema(Schema): id = fields.Str(dump_only=True) body = fields.Str() class Meta: type_ = "comments" self_view = "comment_detail" self_view_kwargs = {"comment_id": "", "_external": True} self_view_many = "comments_list" class AuthorSchema(Schema): id = fields.Str(dump_only=True) first_name = fields.Str(required=True) last_name = fields.Str(required=True) password = fields.Str(load_only=True, validate=validate.Length(6)) twitter = fields.Str() class Meta: type_ = "people" self_view = "author_detail" self_view_kwargs = {"author_id": ""} self_view_many = "authors_list" class PostSchema(Schema): id = fields.Str(dump_only=True) title = fields.Str() author = Relationship( related_view="author_detail", related_view_kwargs={"author_id": "", "_external": True}, include_data=True, type_="people", ) comments = Relationship( related_view="posts_comments", related_view_kwargs={"post_id": "", "_external": True}, many=True, include_data=True, type_="comments", ) class Meta: type_ = "posts" self_view = "posts_detail" self_view_kwargs = {"post_id": ""} self_view_many = "posts_list" ### VIEWS ### app = Flask(__name__) app.config["DEBUG"] = True def J(*args, **kwargs): """Wrapper around jsonify that sets the Content-Type of the response to application/vnd.api+json. """ response = jsonify(*args, **kwargs) response.mimetype = "application/vnd.api+json" return response @app.route("/posts/", methods=["GET"]) def posts_list(): posts = db["posts"] data = PostSchema(many=True).dump(posts) return J(data) @app.route("/posts/") def posts_detail(post_id): post = db["posts"][post_id - 1] data = PostSchema().dump(post) return J(data) @app.route("/posts//comments/") def posts_comments(post_id): post = db["posts"][post_id - 1] comments = post.comments data = CommentSchema(many=True).dump(comments) return J(data) @app.route("/authors/") def authors_list(): author = db["authors"] data = AuthorSchema(many=True).dump(author) return J(data) @app.route("/authors/") def author_detail(author_id): author = db["authors"][author_id - 1] data = AuthorSchema().dump(author) return J(data) @app.route("/authors/", methods=["POST"]) def author_create(): schema = AuthorSchema() input_data = request.get_json() or {} try: data = schema.load(input_data) except ValidationError as err: return J(err.messages), 422 id_ = len(db["authors"]) author = Author(id=id_, **data) db["authors"].append(author) data = schema.dump(author) return J(data) @app.route("/comments/") def comments_list(): comment = db["comments"] data = CommentSchema(many=True).dump(comment) return J(data) @app.route("/comments/") def comment_detail(comment_id): comment = db["comments"][comment_id - 1] data = CommentSchema().dump(comment) return J(data) if __name__ == "__main__": app.run() ================================================ FILE: marshmallow_jsonapi/__init__.py ================================================ from .schema import Schema, SchemaOpts __version__ = "0.24.0" __all__ = ("Schema", "SchemaOpts") ================================================ FILE: marshmallow_jsonapi/exceptions.py ================================================ """Exception classes.""" class JSONAPIError(Exception): """Base class for all exceptions in this package.""" pass class IncorrectTypeError(JSONAPIError, ValueError): """Raised when client provides an invalid `type` in a request.""" pointer = "/data/type" default_message = 'Invalid type. Expected "{expected}".' def __init__(self, message=None, actual=None, expected=None): message = message or self.default_message format_kwargs = {} if actual: format_kwargs["actual"] = actual if expected: format_kwargs["expected"] = expected self.detail = message.format(**format_kwargs) super().__init__(self.detail) @property def messages(self): """JSON API-formatted error representation.""" return { "errors": [{"detail": self.detail, "source": {"pointer": self.pointer}}] } ================================================ FILE: marshmallow_jsonapi/fields.py ================================================ """Includes all the fields classes from `marshmallow.fields` as well as fields for serializing JSON API-formatted hyperlinks. """ import collections.abc from marshmallow import ValidationError, class_registry from marshmallow.fields import Field # Make core fields importable from marshmallow_jsonapi from marshmallow.fields import * # noqa from marshmallow.base import SchemaABC from marshmallow.utils import is_collection, missing as missing_, get_value from .utils import resolve_params _RECURSIVE_NESTED = "self" # JSON API disallows U+005F LOW LINE at the start of a member name, so we can # use it to load the Meta type from since it can't clash with an attribute # named meta (which isn't disallowed by the spec). _DOCUMENT_META_LOAD_FROM = "_document_meta" _RESOURCE_META_LOAD_FROM = "_resource_meta" class BaseRelationship(Field): """Base relationship field. This is used by `marshmallow_jsonapi.Schema` to determine which fields should be formatted as relationship objects. See: http://jsonapi.org/format/#document-resource-object-relationships """ pass def _stringify(value): if value is not None: return str(value) return value class Relationship(BaseRelationship): """Framework-independent field which serializes to a "relationship object". See: http://jsonapi.org/format/#document-resource-object-relationships Examples: :: author = Relationship( related_url='/authors/{author_id}', related_url_kwargs={'author_id': ''}, ) comments = Relationship( related_url='/posts/{post_id}/comments/', related_url_kwargs={'post_id': ''}, many=True, include_resource_linkage=True, type_='comments' ) This field is read-only by default. :param str related_url: Format string for related resource links. :param dict related_url_kwargs: Replacement fields for `related_url`. String arguments enclosed in `< >` will be interpreted as attributes to pull from the target object. :param str self_url: Format string for self relationship links. :param dict self_url_kwargs: Replacement fields for `self_url`. String arguments enclosed in `< >` will be interpreted as attributes to pull from the target object. :param bool include_resource_linkage: Whether to include a resource linkage (http://jsonapi.org/format/#document-resource-object-linkage) in the serialized result. :param marshmallow_jsonapi.Schema schema: The schema to render the included data with. :param bool many: Whether the relationship represents a many-to-one or many-to-many relationship. Only affects serialization of the resource linkage. :param str type_: The type of resource. :param str id_field: Attribute name to pull ids from if a resource linkage is included. """ default_id_field = "id" def __init__( self, related_url="", related_url_kwargs=None, *, self_url="", self_url_kwargs=None, include_resource_linkage=False, schema=None, many=False, type_=None, id_field=None, **kwargs ): self.related_url = related_url self.related_url_kwargs = related_url_kwargs or {} self.self_url = self_url self.self_url_kwargs = self_url_kwargs or {} if include_resource_linkage and not type_: raise ValueError( "include_resource_linkage=True requires the type_ argument." ) self.many = many self.include_resource_linkage = include_resource_linkage self.include_data = False self.type_ = type_ self.__id_field = id_field self.__schema = schema super().__init__(**kwargs) @property def id_field(self): if self.__id_field: return self.__id_field if self.__schema: field = self.schema.fields["id"] return field.attribute or self.default_id_field else: return self.default_id_field @property def schema(self): only = getattr(self, "only", None) exclude = getattr(self, "exclude", ()) context = getattr(self, "context", {}) if isinstance(self.__schema, SchemaABC): return self.__schema if isinstance(self.__schema, type) and issubclass(self.__schema, SchemaABC): self.__schema = self.__schema(only=only, exclude=exclude, context=context) return self.__schema if isinstance(self.__schema, (str, bytes)): if self.__schema == _RECURSIVE_NESTED: parent_class = self.parent.__class__ self.__schema = parent_class( only=only, exclude=exclude, context=context, include_data=self.parent.include_data, ) else: schema_class = class_registry.get_class(self.__schema) self.__schema = schema_class( only=only, exclude=exclude, context=context ) return self.__schema else: raise ValueError( "A Schema is required to serialize a nested " "relationship with include_data" ) def get_related_url(self, obj): if self.related_url: params = resolve_params(obj, self.related_url_kwargs, default=self.default) non_null_params = { key: value for key, value in params.items() if value is not None } if non_null_params: return self.related_url.format(**non_null_params) return None def get_self_url(self, obj): if self.self_url: params = resolve_params(obj, self.self_url_kwargs, default=self.default) non_null_params = { key: value for key, value in params.items() if value is not None } if non_null_params: return self.self_url.format(**non_null_params) return None def get_resource_linkage(self, value): if self.many: resource_object = [ {"type": self.type_, "id": _stringify(self._get_id(each))} for each in value ] else: resource_object = { "type": self.type_, "id": _stringify(self._get_id(value)), } return resource_object def extract_value(self, data): """Extract the id key and validate the request structure.""" errors = [] if "id" not in data: errors.append("Must have an `id` field") if "type" not in data: errors.append("Must have a `type` field") elif data["type"] != self.type_: errors.append("Invalid `type` specified") if errors: raise ValidationError(errors) # If ``attributes`` is set, we've folded included data into this # relationship. Unserialize it if we have a schema set; otherwise we # fall back below to old behaviour of only IDs. if "attributes" in data and self.__schema: result = self.schema.load( {"data": data, "included": self.root.included_data} ) return result id_value = data.get("id") if self.__schema: id_value = self.schema.fields["id"].deserialize(id_value) return id_value def deserialize(self, value, attr=None, data=None, **kwargs): """Deserialize ``value``. :raise ValidationError: If the value is not type `dict`, if the value does not contain a `data` key, and if the value is required but unspecified. """ if value is missing_: return super().deserialize(value, attr, data) if not isinstance(value, dict) or "data" not in value: # a relationships object does not need 'data' if 'links' is present if value and "links" in value: return missing_ else: raise ValidationError("Must include a `data` key") return super().deserialize(value["data"], attr, data, **kwargs) def _deserialize(self, value, attr, obj, **kwargs): if self.many: if not is_collection(value): raise ValidationError("Relationship is list-like") return [self.extract_value(item) for item in value] if is_collection(value): raise ValidationError("Relationship is not list-like") return self.extract_value(value) # We have to override serialize because we don't want those fields # to be serialized which are related to the resource but not included # in the request. And we don't have enough control in _serialize # to prevent their serialization def serialize(self, attr, obj, accessor=None): if obj is None or self.include_resource_linkage or self.include_data: return super().serialize(attr, obj, accessor) return self._serialize(None, attr, obj) def _serialize(self, value, attr, obj): dict_class = self.parent.dict_class if self.parent else dict ret = dict_class() self_url = self.get_self_url(obj) related_url = self.get_related_url(obj) if self_url or related_url: ret["links"] = dict_class() if self_url: ret["links"]["self"] = self_url if related_url: ret["links"]["related"] = related_url # resource linkage is required when including the data if self.include_resource_linkage or self.include_data: if value is None: ret["data"] = [] if self.many else None else: ret["data"] = self.get_resource_linkage(value) if self.include_data and value is not None: if self.many: for item in value: self._serialize_included(item) else: self._serialize_included(value) return ret def _serialize_included(self, value): result = self.schema.dump(value) item = result["data"] self.root.included_data[(item["type"], item["id"])] = item for key, value in self.schema.included_data.items(): self.root.included_data[key] = value def _get_id(self, value): if self.__schema: return self.schema.get_attribute(value, self.id_field, value) else: return get_value(value, self.id_field, value) class DocumentMeta(Field): """Field which serializes to a "meta object" within a document’s “top level”. Examples: :: from marshmallow_jsonapi import Schema, fields class UserSchema(Schema): id = fields.String() metadata = fields.DocumentMeta() class Meta: type_ = 'product' See: http://jsonapi.org/format/#document-meta """ default_error_messages = {"invalid": "Not a valid mapping type."} def __init__(self, **kwargs): super().__init__(**kwargs) self.data_key = _DOCUMENT_META_LOAD_FROM def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, collections.abc.Mapping): return value else: raise self.make_error("invalid") def _serialize(self, value, *args, **kwargs): if isinstance(value, collections.abc.Mapping): return super()._serialize(value, *args, **kwargs) else: raise self.make_error("invalid") class ResourceMeta(Field): """Field which serializes to a "meta object" within a "resource object". Examples: :: from marshmallow_jsonapi import Schema, fields class UserSchema(Schema): id = fields.String() meta_resource = fields.ResourceMeta() class Meta: type_ = 'product' See: http://jsonapi.org/format/#document-resource-objects """ default_error_messages = {"invalid": "Not a valid mapping type."} def __init__(self, **kwargs): super().__init__(**kwargs) self.data_key = _RESOURCE_META_LOAD_FROM def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, collections.abc.Mapping): return value else: raise self.make_error("invalid") def _serialize(self, value, *args, **kwargs): if isinstance(value, collections.abc.Mapping): return super()._serialize(value, *args, **kwargs) else: raise self.make_error("invalid") ================================================ FILE: marshmallow_jsonapi/flask.py ================================================ """Flask integration that avoids the need to hard-code URLs for links. This includes a Flask-specific schema with custom Meta options and a relationship field for linking to related resources. """ import flask from werkzeug.routing import BuildError from .fields import Relationship as GenericRelationship from .schema import Schema as DefaultSchema, SchemaOpts as DefaultOpts from .utils import resolve_params class SchemaOpts(DefaultOpts): """Options to use Flask view names instead of hard coding URLs.""" def __init__(self, meta, *args, **kwargs): if getattr(meta, "self_url", None): raise ValueError( "Use `self_view` instead of `self_url` " "using the Flask extension." ) if getattr(meta, "self_url_kwargs", None): raise ValueError( "Use `self_view_kwargs` instead of `self_url_kwargs` " "when using the Flask extension." ) if getattr(meta, "self_url_many", None): raise ValueError( "Use `self_view_many` instead of `self_url_many` " "when using the Flask extension." ) if getattr(meta, "self_view_kwargs", None) and not getattr( meta, "self_view", None ): raise ValueError( "Must specify `self_view` Meta option when " "`self_view_kwargs` is specified." ) # Transfer Flask options to URL options, to piggy-back on its handling meta.self_url = getattr(meta, "self_view", None) meta.self_url_kwargs = getattr(meta, "self_view_kwargs", None) meta.self_url_many = getattr(meta, "self_view_many", None) super().__init__(meta, *args, **kwargs) class Schema(DefaultSchema): """A Flask specific schema that resolves self URLs from view names.""" OPTIONS_CLASS = SchemaOpts class Meta: """Options object that takes the same options as `marshmallow-jsonapi.Schema`, but instead of ``self_url``, ``self_url_kwargs`` and ``self_url_many`` has the following options to resolve the URLs from Flask views: * ``self_view`` - View name to resolve the self URL link from. * ``self_view_kwargs`` - Replacement fields for ``self_view``. String attributes enclosed in ``< >`` will be interpreted as attributes to pull from the schema data. * ``self_view_many`` - View name to resolve the self URL link when a collection of resources is returned. """ pass def generate_url(self, view_name, **kwargs): """Generate URL with any kwargs interpolated.""" return flask.url_for(view_name, **kwargs) if view_name else None class Relationship(GenericRelationship): r"""Field which serializes to a "relationship object" with a "related resource link". See: http://jsonapi.org/format/#document-resource-object-relationships Examples: :: author = Relationship( related_view='author_detail', related_view_kwargs={'author_id': ''}, ) comments = Relationship( related_view='posts_comments', related_view_kwargs={'post_id': ''}, many=True, include_resource_linkage=True, type_='comments' ) This field is read-only by default. :param str related_view: View name for related resource link. :param dict related_view_kwargs: Path kwargs fields for `related_view`. String arguments enclosed in `< >` will be interpreted as attributes to pull from the target object. :param str self_view: View name for self relationship link. :param dict self_view_kwargs: Path kwargs for `self_view`. String arguments enclosed in `< >` will be interpreted as attributes to pull from the target object. :param \*\*kwargs: Same keyword arguments as `marshmallow_jsonapi.fields.Relationship`. """ def __init__( self, related_view=None, related_view_kwargs=None, *, self_view=None, self_view_kwargs=None, **kwargs ): self.related_view = related_view self.related_view_kwargs = related_view_kwargs or {} self.self_view = self_view self.self_view_kwargs = self_view_kwargs or {} super().__init__(**kwargs) def get_url(self, obj, view_name, view_kwargs): if view_name: kwargs = resolve_params(obj, view_kwargs, default=self.default) kwargs["endpoint"] = view_name try: return flask.url_for(**kwargs) except BuildError: if ( None in kwargs.values() ): # most likely to be caused by empty relationship return None raise return None def get_related_url(self, obj): return self.get_url(obj, self.related_view, self.related_view_kwargs) def get_self_url(self, obj): return self.get_url(obj, self.self_view, self.self_view_kwargs) ================================================ FILE: marshmallow_jsonapi/schema.py ================================================ import itertools import marshmallow as ma from marshmallow.exceptions import ValidationError from marshmallow.utils import is_collection from .fields import BaseRelationship, DocumentMeta, ResourceMeta from .fields import _RESOURCE_META_LOAD_FROM, _DOCUMENT_META_LOAD_FROM from .exceptions import IncorrectTypeError from .utils import resolve_params TYPE = "type" ID = "id" class SchemaOpts(ma.SchemaOpts): def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.type_ = getattr(meta, "type_", None) self.inflect = getattr(meta, "inflect", None) self.self_url = getattr(meta, "self_url", None) self.self_url_kwargs = getattr(meta, "self_url_kwargs", None) self.self_url_many = getattr(meta, "self_url_many", None) class Schema(ma.Schema): """Schema class that formats data according to JSON API 1.0. Must define the ``type_`` `class Meta` option. Example: :: from marshmallow_jsonapi import Schema, fields def dasherize(text): return text.replace('_', '-') class PostSchema(Schema): id = fields.Str(dump_only=True) # Required title = fields.Str() author = fields.HyperlinkRelated( '/authors/{author_id}', url_kwargs={'author_id': ''}, ) comments = fields.HyperlinkRelated( '/posts/{post_id}/comments', url_kwargs={'post_id': ''}, # Include resource linkage many=True, include_resource_linkage=True, type_='comments' ) class Meta: type_ = 'posts' # Required inflect = dasherize """ class Meta: """Options object for `Schema`. Takes the same options as `marshmallow.Schema.Meta` with the addition of: * ``type_`` - required, the JSON API resource type as a string. * ``inflect`` - optional, an inflection function to modify attribute names. * ``self_url`` - optional, URL to use to `self` in links * ``self_url_kwargs`` - optional, replacement fields for `self_url`. String arguments enclosed in ``< >`` will be interpreted as attributes to pull from the schema data. * ``self_url_many`` - optional, URL to use to `self` in top-level ``links`` when a collection of resources is returned. """ pass def __init__(self, *args, **kwargs): self.include_data = kwargs.pop("include_data", ()) super().__init__(*args, **kwargs) if self.include_data: self.check_relations(self.include_data) if not self.opts.type_: raise ValueError("Must specify type_ class Meta option") if "id" not in self.fields: raise ValueError("Must have an `id` field") if self.opts.self_url_kwargs and not self.opts.self_url: raise ValueError( "Must specify `self_url` Meta option when " "`self_url_kwargs` is specified" ) self.included_data = {} self.document_meta = {} OPTIONS_CLASS = SchemaOpts def check_relations(self, relations): """Recursive function which checks if a relation is valid.""" for rel in relations: if not rel: continue fields = rel.split(".", 1) local_field = fields[0] if local_field not in self.fields: raise ValueError(f'Unknown field "{local_field}"') field = self.fields[local_field] if not isinstance(field, BaseRelationship): raise ValueError( 'Can only include relationships. "{}" is a "{}"'.format( field.name, field.__class__.__name__ ) ) field.include_data = True if len(fields) > 1: field.schema.check_relations(fields[1:]) @ma.post_dump(pass_many=True) def format_json_api_response(self, data, many, **kwargs): """Post-dump hook that formats serialized data as a top-level JSON API object. See: http://jsonapi.org/format/#document-top-level """ ret = self.format_items(data, many) ret = self.wrap_response(ret, many) ret = self.render_included_data(ret) ret = self.render_meta_document(ret) return ret def render_included_data(self, data): if not self.included_data: return data data["included"] = list(self.included_data.values()) return data def render_meta_document(self, data): if not self.document_meta: return data data["meta"] = self.document_meta return data def unwrap_item(self, item): if "type" not in item: raise ma.ValidationError( [ { "detail": "`data` object must include `type` key.", "source": {"pointer": "/data"}, } ] ) if item["type"] != self.opts.type_: raise IncorrectTypeError(actual=item["type"], expected=self.opts.type_) payload = self.dict_class() if "id" in item: payload["id"] = item["id"] if "meta" in item: payload[_RESOURCE_META_LOAD_FROM] = item["meta"] if self.document_meta: payload[_DOCUMENT_META_LOAD_FROM] = self.document_meta for key, value in item.get("attributes", {}).items(): payload[key] = value for key, value in item.get("relationships", {}).items(): # Fold included data related to this relationship into the item, so # that we can deserialize the whole objects instead of just IDs. if self.included_data: included_data = [] inner_data = value.get("data", []) # Data may be ``None`` (for empty relationships), but we only # need to process it when it's present. if inner_data: if not is_collection(inner_data): included_data = next( self._extract_from_included(inner_data), None ) else: for data in inner_data: included_data.extend(self._extract_from_included(data)) if included_data: value["data"] = included_data payload[key] = value return payload @ma.pre_load(pass_many=True) def unwrap_request(self, data, many, **kwargs): if "data" not in data: raise ma.ValidationError( [ { "detail": "Object must include `data` key.", "source": {"pointer": "/"}, } ] ) data = data["data"] if many: if not is_collection(data): raise ma.ValidationError( [ { "detail": "`data` expected to be a collection.", "source": {"pointer": "/data"}, } ] ) return [self.unwrap_item(each) for each in data] return self.unwrap_item(data) def on_bind_field(self, field_name, field_obj): """Schema hook override. When binding fields, set ``data_key`` to the inflected form of field_name.""" if not field_obj.data_key: field_obj.data_key = self.inflect(field_name) return None def _do_load(self, data, many=None, **kwargs): """Override `marshmallow.Schema._do_load` for custom JSON API handling. Specifically, we do this to format errors as JSON API Error objects, and to support loading of included data. """ many = self.many if many is None else bool(many) # Store this on the instance so we have access to the included data # when processing relationships (``included`` is outside of the # ``data``). self.included_data = data.get("included", {}) self.document_meta = data.get("meta", {}) try: result = super()._do_load(data, many=many, **kwargs) except ValidationError as err: # strict mode error_messages = err.messages if "_schema" in error_messages: error_messages = error_messages["_schema"] formatted_messages = self.format_errors(error_messages, many=many) err.messages = formatted_messages raise err return result def _extract_from_included(self, data): """Extract included data matching the items in ``data``. For each item in ``data``, extract the full data from the included data. """ return ( item for item in self.included_data if item["type"] == data["type"] and str(item["id"]) == str(data["id"]) ) def inflect(self, text): """Inflect ``text`` if the ``inflect`` class Meta option is defined, otherwise do nothing. """ return self.opts.inflect(text) if self.opts.inflect else text ### Overridable hooks ### def format_errors(self, errors, many): """Format validation errors as JSON Error objects.""" if not errors: return {} if isinstance(errors, (list, tuple)): return {"errors": errors} formatted_errors = [] if many: for index, i_errors in errors.items(): formatted_errors.extend(self._get_formatted_errors(i_errors, index)) else: formatted_errors.extend(self._get_formatted_errors(errors)) return {"errors": formatted_errors} def _get_formatted_errors(self, errors, index=None): return itertools.chain( *( [ self.format_error(field_name, message, index=index) for message in field_errors ] for field_name, field_errors in itertools.chain( *(self._process_nested_errors(k, v) for k, v in errors.items()) ) ) ) def _process_nested_errors(self, name, data): if not isinstance(data, dict): return [(name, data)] return itertools.chain( *(self._process_nested_errors(f"{name}/{k}", v) for k, v in data.items()) ) def format_error(self, field_name, message, index=None): """Override-able hook to format a single error message as an Error object. See: http://jsonapi.org/format/#error-objects """ pointer = ["/data"] if index is not None: pointer.append(str(index)) relationship = isinstance( self.declared_fields.get(field_name), BaseRelationship ) if relationship: pointer.append("relationships") elif field_name != "id": # JSONAPI identifier is a special field that exists above the attribute object. pointer.append("attributes") pointer.append(self.inflect(field_name)) if relationship: pointer.append("data") return {"detail": message, "source": {"pointer": "/".join(pointer)}} def format_item(self, item): """Format a single datum as a Resource object. See: http://jsonapi.org/format/#document-resource-objects """ # http://jsonapi.org/format/#document-top-level # Primary data MUST be either... a single resource object, a single resource # identifier object, or null, for requests that target single resources if not item: return None ret = self.dict_class() ret[TYPE] = self.opts.type_ # Get the schema attributes so we can confirm `dump-to` values exist attributes = { (self.fields[field].data_key or field): field for field in self.fields } for field_name, value in item.items(): attribute = attributes[field_name] if attribute == ID: ret[ID] = value elif isinstance(self.fields[attribute], DocumentMeta): if not self.document_meta: self.document_meta = self.dict_class() self.document_meta.update(value) elif isinstance(self.fields[attribute], ResourceMeta): if "meta" not in ret: ret["meta"] = self.dict_class() ret["meta"].update(value) elif isinstance(self.fields[attribute], BaseRelationship): if value: if "relationships" not in ret: ret["relationships"] = self.dict_class() ret["relationships"][self.inflect(field_name)] = value else: if "attributes" not in ret: ret["attributes"] = self.dict_class() ret["attributes"][self.inflect(field_name)] = value links = self.get_resource_links(item) if links: ret["links"] = links return ret def format_items(self, data, many): """Format data as a Resource object or list of Resource objects. See: http://jsonapi.org/format/#document-resource-objects """ if many: return [self.format_item(item) for item in data] else: return self.format_item(data) def get_top_level_links(self, data, many): """Hook for adding links to the root of the response data.""" self_link = None if many: if self.opts.self_url_many: self_link = self.generate_url(self.opts.self_url_many) else: if self.opts.self_url: self_link = data.get("links", {}).get("self", None) return {"self": self_link} def get_resource_links(self, item): """Hook for adding links to a resource object.""" if self.opts.self_url: ret = self.dict_class() kwargs = resolve_params(item, self.opts.self_url_kwargs or {}) ret["self"] = self.generate_url(self.opts.self_url, **kwargs) return ret return None def wrap_response(self, data, many): """Wrap data and links according to the JSON API""" ret = {"data": data} # self_url_many is still valid when there isn't any data, but self_url # may only be included if there is data in the ret if many or data: top_level_links = self.get_top_level_links(data, many) if top_level_links["self"]: ret["links"] = top_level_links return ret def generate_url(self, link, **kwargs): """Generate URL with any kwargs interpolated.""" return link.format_map(kwargs) if link else None ================================================ FILE: marshmallow_jsonapi/utils.py ================================================ """Utility functions. This module should be considered private API. """ import re from marshmallow.utils import get_value, missing _tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*") def tpl(val): """Return value within ``< >`` if possible, else return ``None``.""" match = _tpl_pattern.match(val) if match: return match.groups()[0] return None def resolve_params(obj, params, default=missing): """Given a dictionary of keyword arguments, return the same dictionary except with values enclosed in `< >` resolved to attributes on `obj`. """ param_values = {} for name, attr_tpl in params.items(): attr_name = tpl(str(attr_tpl)) if attr_name: attribute_value = get_value(obj, attr_name, default=default) if attribute_value is not missing: param_values[name] = attribute_value else: raise AttributeError( "{attr_name!r} is not a valid " "attribute of {obj!r}".format(attr_name=attr_name, obj=obj) ) else: param_values[name] = attr_tpl return param_values ================================================ FILE: setup.cfg ================================================ [metadata] license_files = LICENSE [bdist_wheel] # This flag says that the code is written to work on both Python 2 and Python # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. universal=1 [flake8] ignore = E203, E266, E501, W503 max-line-length = 110 max-complexity = 18 select = B,C,E,F,W,T4,B9 ================================================ FILE: setup.py ================================================ import re from setuptools import setup, find_packages INSTALL_REQUIRES = ("marshmallow>=2.15.2",) EXTRAS_REQUIRE = { "tests": ["pytest", "mock", "faker==4.18.0", "Flask==1.1.2"], "lint": ["flake8==3.9.0", "flake8-bugbear==20.11.1", "pre-commit~=2.0"], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] def find_version(fname): """Attempts to find the version number in the file names fname. Raises RuntimeError if not found. """ version = "" with open(fname) as fp: reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') for line in fp: m = reg.match(line) if m: version = m.group(1) break if not version: raise RuntimeError("Cannot find version information") return version def read(fname): with open(fname) as fp: content = fp.read() return content setup( name="marshmallow-jsonapi", version=find_version("marshmallow_jsonapi/__init__.py"), description="JSON API 1.0 (https://jsonapi.org) formatting with marshmallow", long_description=read("README.rst"), author="Steven Loria", author_email="sloria1@gmail.com", url="https://github.com/marshmallow-code/marshmallow-jsonapi", packages=find_packages(exclude=("test*",)), package_dir={"marshmallow-jsonapi": "marshmallow-jsonapi"}, include_package_data=True, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, python_requires=">=3.6", license="MIT", zip_safe=False, keywords=( "marshmallow-jsonapi marshmallow marshalling serialization " "jsonapi deserialization validation" ), classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", ], test_suite="tests", project_urls={ "Bug Reports": "https://github.com/marshmallow-code/marshmallow-jsonapi/issues", "Funding": "https://opencollective.com/marshmallow", }, ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/base.py ================================================ from hashlib import md5 from faker import Factory from marshmallow import validate from marshmallow_jsonapi import Schema, fields fake = Factory.create() class Bunch: def __init__(self, **kwargs): for key, val in kwargs.items(): setattr(self, key, val) class Post(Bunch): pass class Author(Bunch): pass class Comment(Bunch): pass class Keyword(Bunch): pass class AuthorSchema(Schema): id = fields.Str() first_name = fields.Str(required=True) last_name = fields.Str(required=True) password = fields.Str(load_only=True, validate=validate.Length(6)) twitter = fields.Str() def get_top_level_links(self, data, many): if many: self_link = "/authors/" else: self_link = "/authors/{}".format(data["id"]) return {"self": self_link} class Meta: type_ = "people" class KeywordSchema(Schema): id = fields.Str() keyword = fields.Str(required=True) def get_attribute(self, attr, obj, default): if obj == "id": return md5( super(Schema, self) .get_attribute(attr, "keyword", default) .encode("utf-8") ).hexdigest() else: return super(Schema, self).get_attribute(attr, obj, default) class Meta: type_ = "keywords" strict = True class CommentSchema(Schema): id = fields.Str() body = fields.Str(required=True) author = fields.Relationship( "http://test.test/comments/{id}/author/", related_url_kwargs={"id": ""}, schema=AuthorSchema, many=False, ) class Meta: type_ = "comments" strict = True class ArticleSchema(Schema): id = fields.Integer() body = fields.String() author = fields.Relationship( dump_only=False, include_resource_linkage=True, many=False, type_="people" ) comments = fields.Relationship( dump_only=False, include_resource_linkage=True, many=True, type_="comments" ) class Meta: type_ = "articles" strict = True class PostSchema(Schema): id = fields.Str() post_title = fields.Str(attribute="title", dump_to="title", data_key="title") author = fields.Relationship( "http://test.test/posts/{id}/author/", related_url_kwargs={"id": ""}, schema=AuthorSchema, many=False, type_="people", ) post_comments = fields.Relationship( "http://test.test/posts/{id}/comments/", related_url_kwargs={"id": ""}, attribute="comments", load_from="post-comments", dump_to="post-comments", data_key="post-comments", schema="CommentSchema", many=True, type_="comments", ) post_keywords = fields.Relationship( "http://test.test/posts/{id}/keywords/", related_url_kwargs={"id": ""}, attribute="keywords", dump_to="post-keywords", data_key="post-keywords", schema="KeywordSchema", many=True, type_="keywords", ) class Meta: type_ = "posts" strict = True class PolygonSchema(Schema): id = fields.Integer(as_string=True) sides = fields.Integer() # This is an attribute that uses the 'meta' key: /data/attributes/meta meta = fields.String() # This is the document's top level meta object: /meta document_meta = fields.DocumentMeta() # This is the resource object's meta object: /data/meta resource_meta = fields.ResourceMeta() class Meta: type_ = "shapes" strict = True ================================================ FILE: tests/conftest.py ================================================ import pytest from tests.base import Author, Post, Comment, Keyword, fake def make_author(): return Author( id=fake.random_int(), first_name=fake.first_name(), last_name=fake.last_name(), twitter=fake.domain_word(), ) def make_post(with_comments=True, with_author=True, with_keywords=True): comments = [make_comment() for _ in range(2)] if with_comments else [] keywords = [make_keyword() for _ in range(3)] if with_keywords else [] author = make_author() if with_author else None return Post( id=fake.random_int(), title=fake.catch_phrase(), author=author, author_id=author.id if with_author else None, comments=comments, keywords=keywords, ) def make_comment(with_author=True): author = make_author() if with_author else None return Comment(id=fake.random_int(), body=fake.bs(), author=author) def make_keyword(): return Keyword(keyword=fake.domain_word()) @pytest.fixture() def author(): return make_author() @pytest.fixture() def authors(): return [make_author() for _ in range(3)] @pytest.fixture() def comments(): return [make_comment() for _ in range(3)] @pytest.fixture() def post(): return make_post() @pytest.fixture() def post_with_null_comment(): return make_post(with_comments=False) @pytest.fixture() def post_with_null_author(): return make_post(with_author=False) @pytest.fixture() def posts(): return [make_post() for _ in range(3)] ================================================ FILE: tests/test_fields.py ================================================ import pytest from hashlib import md5 from marshmallow import ValidationError, missing as missing_ from marshmallow.fields import Int from marshmallow_jsonapi import Schema from marshmallow_jsonapi.fields import Str, DocumentMeta, ResourceMeta, Relationship class TestGenericRelationshipField: def test_serialize_relationship_link(self, post): field = Relationship( "http://example.com/posts/{id}/comments", related_url_kwargs={"id": ""} ) result = field.serialize("comments", post) assert field.serialize("comments", post) related = result["links"]["related"] assert related == f"http://example.com/posts/{post.id}/comments" def test_serialize_self_link(self, post): field = Relationship( self_url="http://example.com/posts/{id}/relationships/comments", self_url_kwargs={"id": ""}, ) result = field.serialize("comments", post) related = result["links"]["self"] assert "related" not in result["links"] assert related == "http://example.com/posts/{id}/relationships/comments".format( id=post.id ) def test_include_resource_linkage_requires_type(self): with pytest.raises(ValueError) as excinfo: Relationship( related_url="/posts/{post_id}", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, ) assert ( excinfo.value.args[0] == "include_resource_linkage=True requires the type_ argument." ) def test_include_resource_linkage_single(self, post): field = Relationship( related_url="/posts/{post_id}/author/", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, type_="people", ) result = field.serialize("author", post) assert "data" in result assert result["data"] assert result["data"]["id"] == str(post.author.id) def test_include_resource_linkage_single_with_schema(self, post): field = Relationship( related_url="/posts/{post_id}/author/", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, type_="people", schema="PostSchema", ) result = field.serialize("author", post) assert "data" in result assert result["data"] assert result["data"]["id"] == str(post.author.id) def test_include_resource_linkage_single_foreign_key(self, post): field = Relationship( related_url="/posts/{post_id}/author/", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, type_="people", ) result = field.serialize("author_id", post) assert result["data"]["id"] == str(post.author_id) def test_include_resource_linkage_single_foreign_key_with_schema(self, post): field = Relationship( related_url="/posts/{post_id}/author/", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, type_="people", schema="PostSchema", ) result = field.serialize("author_id", post) assert result["data"]["id"] == str(post.author_id) def test_include_resource_linkage_id_field_from_string(self): field = Relationship( include_resource_linkage=True, type_="authors", id_field="name" ) result = field.serialize("author", {"author": {"name": "Ray Bradbury"}}) assert "data" in result assert result["data"] assert result["data"]["id"] == "Ray Bradbury" def test_include_resource_linkage_id_field_from_schema(self): class AuthorSchema(Schema): id = Str(attribute="name") class Meta: type_ = "authors" strict = True field = Relationship( include_resource_linkage=True, type_="authors", schema=AuthorSchema ) result = field.serialize("author", {"author": {"name": "Ray Bradbury"}}) assert "data" in result assert result["data"] assert result["data"]["id"] == "Ray Bradbury" def test_include_resource_linkage_many(self, post): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", ) result = field.serialize("comments", post) assert "data" in result ids = [each["id"] for each in result["data"]] assert ids == [str(each.id) for each in post.comments] def test_include_resource_linkage_many_with_schema(self, post): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", schema="CommentSchema", ) result = field.serialize("comments", post) assert "data" in result ids = [each["id"] for each in result["data"]] assert ids == [str(each.id) for each in post.comments] def test_include_resource_linkage_many_with_schema_overriding_get_attribute( self, post ): field = Relationship( related_url="/posts/{post_id}/keywords", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="keywords", schema="KeywordSchema", ) result = field.serialize("keywords", post) assert "data" in result ids = [each["id"] for each in result["data"]] assert ids == [ md5(each.keyword.encode("utf-8")).hexdigest() for each in post.keywords ] def test_deserialize_data_single(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=False, include_resource_linkage=True, type_="comments", ) value = {"data": {"type": "comments", "id": "1"}} result = field.deserialize(value) assert result == "1" def test_deserialize_data_many(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", ) value = {"data": [{"type": "comments", "id": "1"}]} result = field.deserialize(value) assert result == ["1"] def test_deserialize_data_missing_id(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=False, include_resource_linkage=True, type_="comments", ) with pytest.raises(ValidationError) as excinfo: value = {"data": {"type": "comments"}} field.deserialize(value) assert excinfo.value.args[0] == ["Must have an `id` field"] def test_deserialize_data_missing_type(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=False, include_resource_linkage=True, type_="comments", ) with pytest.raises(ValidationError) as excinfo: value = {"data": {"id": "1"}} field.deserialize(value) assert excinfo.value.args[0] == ["Must have a `type` field"] def test_deserialize_data_incorrect_type(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=False, include_resource_linkage=True, type_="comments", ) with pytest.raises(ValidationError) as excinfo: value = {"data": {"type": "posts", "id": "1"}} field.deserialize(value) assert excinfo.value.args[0] == ["Invalid `type` specified"] def test_deserialize_null_data_value(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, allow_none=True, many=False, include_resource_linkage=False, type_="comments", ) result = field.deserialize({"data": None}) assert result is None def test_deserialize_null_value_disallow_none(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, allow_none=False, many=False, include_resource_linkage=False, type_="comments", ) with pytest.raises(ValidationError) as excinfo: field.deserialize({"data": None}) assert excinfo.value.args[0] == "Field may not be null." def test_deserialize_empty_data_list(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=False, type_="comments", ) result = field.deserialize({"data": []}) assert result == [] def test_deserialize_empty_data(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=False, include_resource_linkage=False, type_="comments", ) with pytest.raises(ValidationError) as excinfo: field.deserialize({"data": {}}) assert excinfo.value.args[0] == [ "Must have an `id` field", "Must have a `type` field", ] def test_deserialize_required_missing(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, required=True, many=False, include_resource_linkage=True, type_="comments", ) with pytest.raises(ValidationError) as excinfo: field.deserialize(missing_) assert excinfo.value.args[0] == "Missing data for required field." def test_deserialize_required_empty(self): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, required=True, many=False, include_resource_linkage=False, type_="comments", ) with pytest.raises(ValidationError) as excinfo: field.deserialize({}) assert excinfo.value.args[0] == "Must include a `data` key" def test_deserialize_many_non_list_relationship(self): field = Relationship(many=True, include_resource_linkage=True, type_="comments") with pytest.raises(ValidationError) as excinfo: field.deserialize({"data": "1"}) assert excinfo.value.args[0] == "Relationship is list-like" def test_deserialize_non_many_list_relationship(self): field = Relationship( many=False, include_resource_linkage=True, type_="comments" ) with pytest.raises(ValidationError) as excinfo: field.deserialize({"data": ["1"]}) assert excinfo.value.args[0] == "Relationship is not list-like" def test_include_null_data_single(self, post_with_null_author): field = Relationship( related_url="posts/{post_id}/author", related_url_kwargs={"post_id": ""}, include_resource_linkage=True, type_="people", ) result = field.serialize("author", post_with_null_author) assert result and result["links"]["related"] assert result["data"] is None def test_include_null_data_many(self, post_with_null_comment): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=True, type_="comments", ) result = field.serialize("comments", post_with_null_comment) assert result and result["links"]["related"] assert result["data"] == [] def test_exclude_data(self, post_with_null_comment): field = Relationship( related_url="/posts/{post_id}/comments", related_url_kwargs={"post_id": ""}, many=True, include_resource_linkage=False, type_="comments", ) result = field.serialize("comments", post_with_null_comment) assert result and result["links"]["related"] assert "data" not in result def test_empty_relationship_with_alternative_identifier_field( self, post_with_null_author ): field = Relationship( related_url="/authors/{author_id}", related_url_kwargs={"author_id": ""}, default=None, ) result = field.serialize("author", post_with_null_author) assert not result def test_resource_linkage_id_type_from_schema(self): class AuthorSchema(Schema): id = Int(attribute="author_id", as_string=True) class Meta: type_ = "authors" strict = True field = Relationship( include_resource_linkage=True, type_="authors", schema=AuthorSchema ) result = field.deserialize({"data": {"type": "authors", "id": "1"}}) assert result == 1 def test_resource_linkage_id_of_invalid_type(self): class AuthorSchema(Schema): id = Int(attribute="author_id", as_string=True) class Meta: type_ = "authors" strict = True field = Relationship( include_resource_linkage=True, type_="authors", schema=AuthorSchema ) with pytest.raises(ValidationError) as excinfo: field.deserialize({"data": {"type": "authors", "id": "not_a_number"}}) assert excinfo.value.args[0] == "Not a valid integer." class TestDocumentMetaField: def test_serialize(self): field = DocumentMeta() result = field.serialize( "document_meta", {"document_meta": {"page": {"offset": 1}}} ) assert result == {"page": {"offset": 1}} def test_serialize_incorrect_type(self): field = DocumentMeta() with pytest.raises(ValidationError) as excinfo: field.serialize("document_meta", {"document_meta": 1}) assert excinfo.value.args[0] == "Not a valid mapping type." def test_deserialize(self): field = DocumentMeta() value = {"page": {"offset": 1}} result = field.deserialize(value) assert result == value def test_deserialize_incorrect_type(self): field = DocumentMeta() value = 1 with pytest.raises(ValidationError) as excinfo: field.deserialize(value) assert excinfo.value.args[0] == "Not a valid mapping type." class TestResourceMetaField: def test_serialize(self): field = ResourceMeta() result = field.serialize("resource_meta", {"resource_meta": {"active": True}}) assert result == {"active": True} def test_serialize_incorrect_type(self): field = ResourceMeta() with pytest.raises(ValidationError) as excinfo: field.serialize("resource_meta", {"resource_meta": True}) assert excinfo.value.args[0] == "Not a valid mapping type." def test_deserialize(self): field = ResourceMeta() value = {"active": True} result = field.deserialize(value) assert result == value def test_deserialize_incorrect_type(self): field = ResourceMeta() value = True with pytest.raises(ValidationError) as excinfo: field.deserialize(value) assert excinfo.value.args[0] == "Not a valid mapping type." ================================================ FILE: tests/test_flask.py ================================================ from flask import Flask, url_for import pytest from werkzeug.routing import BuildError from marshmallow_jsonapi import fields from marshmallow_jsonapi.flask import Relationship, Schema @pytest.fixture() def app(): app_ = Flask("testapp") app_.config["DEBUG"] = True app_.config["TESTING"] = True @app_.route("/posts/") def posts(): return "All posts" @app_.route("/posts//") def post_detail(post_id): return f"Detail for post {post_id}" @app_.route("/posts//comments/") def posts_comments(post_id): return f"Comments for post {post_id}" @app_.route("/authors/") def author_detail(author_id): return f"Detail for author {author_id}" ctx = app_.test_request_context() ctx.push() yield app_ ctx.pop() class TestSchema: class PostFlaskSchema(Schema): id = fields.Int() title = fields.Str() class Meta: type_ = "posts" self_view = "post_detail" self_view_kwargs = {"post_id": ""} self_view_many = "posts" class PostAuthorFlaskSchema(Schema): id = fields.Int() title = fields.Str() field = Relationship( related_view="author_detail", related_view_kwargs={"author_id": ""}, default=None, ) class Meta: type_ = "posts" self_view = "post_detail" self_view_kwargs = {"post_id": ""} self_view_many = "posts" def test_schema_requires_view_options(self): with pytest.raises(ValueError): class InvalidFlaskMetaSchema(Schema): id = fields.Int() class Meta: type_ = "posts" self_url = "/posts/{id}" self_url_kwargs = {"post_id": ""} def test_non_existing_view(self, app, post): class InvalidFlaskMetaSchema(Schema): id = fields.Int() class Meta: type_ = "posts" self_view = "wrong_view" self_view_kwargs = {"post_id": ""} with pytest.raises(BuildError): InvalidFlaskMetaSchema().dump(post) def test_self_link_single(self, app, post): data = self.PostFlaskSchema().dump(post) assert "links" in data assert data["links"]["self"] == f"/posts/{post.id}/" def test_self_link_many(self, app, posts): data = self.PostFlaskSchema(many=True).dump(posts) assert "links" in data assert data["links"]["self"] == "/posts/" assert "links" in data["data"][0] assert data["data"][0]["links"]["self"] == f"/posts/{posts[0].id}/" def test_schema_with_empty_relationship(self, app, post_with_null_author): data = self.PostAuthorFlaskSchema().dump(post_with_null_author) assert "relationships" not in data class TestRelationshipField: def test_serialize_basic(self, app, post): field = Relationship( related_view="posts_comments", related_view_kwargs={"post_id": ""} ) result = field.serialize("comments", post) assert "links" in result assert "related" in result["links"] related = result["links"]["related"] assert related == url_for("posts_comments", post_id=post.id) def test_serialize_external(self, app, post): field = Relationship( related_view="posts_comments", related_view_kwargs={"post_id": "", "_external": True}, ) result = field.serialize("comments", post) related = result["links"]["related"] assert related == url_for("posts_comments", post_id=post.id, _external=True) def test_include_resource_linkage_requires_type(self, app, post): with pytest.raises(ValueError) as excinfo: Relationship( related_view="posts_comments", related_view_kwargs={"post_id": ""}, include_resource_linkage=True, ) assert ( excinfo.value.args[0] == "include_resource_linkage=True requires the type_ argument." ) def test_serialize_self_link(self, app, post): field = Relationship( self_view="posts_comments", self_view_kwargs={"post_id": ""} ) result = field.serialize("comments", post) assert "links" in result assert "self" in result["links"] related = result["links"]["self"] assert related == url_for("posts_comments", post_id=post.id) def test_empty_relationship(self, app, post_with_null_author): field = Relationship( related_view="author_detail", related_view_kwargs={"author_id": ""} ) result = field.serialize("author", post_with_null_author) assert not result def test_non_existing_view(self, app, post): field = Relationship( related_view="non_existing_view", related_view_kwargs={"author_id": ""}, ) with pytest.raises(BuildError): field.serialize("author", post) def test_empty_relationship_with_alternative_identifier_field( self, app, post_with_null_author ): field = Relationship( related_view="author_detail", related_view_kwargs={"author_id": ""}, default=None, ) result = field.serialize("author", post_with_null_author) assert not result ================================================ FILE: tests/test_options.py ================================================ import pytest from marshmallow import validate, ValidationError from marshmallow_jsonapi import Schema, fields from tests.base import AuthorSchema, CommentSchema from tests.test_schema import make_serialized_author, get_error_by_field def dasherize(text): return text.replace("_", "-") class AuthorSchemaWithInflection(Schema): id = fields.Int(dump_only=True) first_name = fields.Str(required=True, validate=validate.Length(min=2)) last_name = fields.Str(required=True) class Meta: type_ = "people" inflect = dasherize strict = True class AuthorSchemaWithOverrideInflection(Schema): id = fields.Str(dump_only=True) # data_key and load_from takes precedence over inflected attribute first_name = fields.Str(data_key="firstName", load_from="firstName") last_name = fields.Str() class Meta: type_ = "people" inflect = dasherize strict = True class TestInflection: @pytest.fixture() def schema(self): return AuthorSchemaWithInflection() def test_dump(self, schema, author): data = schema.dump(author) assert data["data"]["id"] == author.id assert data["data"]["type"] == "people" attribs = data["data"]["attributes"] assert "first-name" in attribs assert "last-name" in attribs assert attribs["first-name"] == author.first_name assert attribs["last-name"] == author.last_name def test_validate_with_inflection(self, schema): errors = schema.validate(make_serialized_author({"first-name": "d"})) lname_err = get_error_by_field(errors, "last-name") assert lname_err assert lname_err["detail"] == "Missing data for required field." fname_err = get_error_by_field(errors, "first-name") assert fname_err assert fname_err["detail"] == "Shorter than minimum length 2." def test_load_with_inflection(self, schema): # invalid with pytest.raises(ValidationError) as excinfo: schema.load(make_serialized_author({"first-name": "d"})) errors = excinfo.value.messages fname_err = get_error_by_field(errors, "first-name") assert fname_err assert fname_err["detail"] == "Shorter than minimum length 2." # valid data = schema.load( make_serialized_author({"first-name": "Nevets", "last-name": "Longoria"}) ) assert data["first_name"] == "Nevets" def test_load_with_inflection_and_load_from_override(self): schema = AuthorSchemaWithOverrideInflection() data = schema.load( make_serialized_author({"firstName": "Steve", "last-name": "Loria"}) ) assert data["first_name"] == "Steve" assert data["last_name"] == "Loria" def test_load_bulk_id_fields(self): request = {"data": [{"id": "1", "type": "people"}]} result = AuthorSchema(only=("id",), many=True).load(request) assert type(result) is list response = result[0] assert response["id"] == request["data"][0]["id"] def test_relationship_keys_get_inflected(self, post): class PostSchema(Schema): id = fields.Int() post_title = fields.Str(attribute="title") post_comments = fields.Relationship( "http://test.test/posts/{id}/comments/", related_url_kwargs={"id": ""}, attribute="comments", ) class Meta: type_ = "posts" inflect = dasherize strict = True data = PostSchema().dump(post) assert "post-title" in data["data"]["attributes"] assert "post-comments" in data["data"]["relationships"] related_href = data["data"]["relationships"]["post-comments"]["links"][ "related" ] assert related_href == f"http://test.test/posts/{post.id}/comments/" class AuthorAutoSelfLinkSchema(Schema): id = fields.Int(dump_only=True) first_name = fields.Str(required=True) last_name = fields.Str(required=True) password = fields.Str(load_only=True, validate=validate.Length(6)) twitter = fields.Str() class Meta: type_ = "people" self_url = "/authors/{id}" self_url_kwargs = {"id": ""} self_url_many = "/authors/" class AuthorAutoSelfLinkFirstLastSchema(AuthorAutoSelfLinkSchema): class Meta: type_ = "people" self_url = "http://example.com/authors/{first_name} {last_name}" self_url_kwargs = {"first_name": "", "last_name": ""} self_url_many = "http://example.com/authors/" class TestAutoSelfUrls: def test_self_url_kwargs_requires_self_url(self, author): class InvalidSelfLinkSchema(Schema): id = fields.Int() class Meta: type_ = "people" self_url_kwargs = {"id": ""} with pytest.raises(ValueError): InvalidSelfLinkSchema().dump(author) def test_self_link_single(self, author): data = AuthorAutoSelfLinkSchema().dump(author) assert "links" in data assert data["links"]["self"] == f"/authors/{author.id}" def test_self_link_many(self, authors): data = AuthorAutoSelfLinkSchema(many=True).dump(authors) assert "links" in data assert data["links"]["self"] == "/authors/" assert "links" in data["data"][0] assert data["data"][0]["links"]["self"] == f"/authors/{authors[0].id}" def test_without_self_link(self, comments): data = CommentSchema(many=True).dump(comments) assert "data" in data assert type(data["data"]) is list first = data["data"][0] assert first["id"] == str(comments[0].id) assert first["type"] == "comments" assert "links" not in data ================================================ FILE: tests/test_schema.py ================================================ import pytest import marshmallow as ma from marshmallow import ValidationError, INCLUDE from marshmallow_jsonapi import Schema, fields from marshmallow_jsonapi.exceptions import IncorrectTypeError from tests.base import ( AuthorSchema, CommentSchema, PostSchema, PolygonSchema, ArticleSchema, ) def make_serialized_author(attributes): return {"data": {"type": "people", "attributes": attributes}} def make_serialized_authors(items): return {"data": [{"type": "people", "attributes": each} for each in items]} def test_type_is_required(): class BadSchema(Schema): id = fields.Str() class Meta: pass with pytest.raises(ValueError) as excinfo: BadSchema() assert excinfo.value.args[0] == "Must specify type_ class Meta option" def test_id_field_is_required(): class BadSchema(Schema): class Meta: type_ = "users" with pytest.raises(ValueError) as excinfo: BadSchema() assert excinfo.value.args[0] == "Must have an `id` field" class TestResponseFormatting: def test_dump_single(self, author): data = AuthorSchema().dump(author) assert "data" in data assert type(data["data"]) is dict assert data["data"]["id"] == str(author.id) assert data["data"]["type"] == "people" attribs = data["data"]["attributes"] assert attribs["first_name"] == author.first_name assert attribs["last_name"] == author.last_name def test_dump_many(self, authors): data = AuthorSchema(many=True).dump(authors) assert "data" in data assert type(data["data"]) is list first = data["data"][0] assert first["id"] == str(authors[0].id) assert first["type"] == "people" attribs = first["attributes"] assert attribs["first_name"] == authors[0].first_name assert attribs["last_name"] == authors[0].last_name def test_self_link_single(self, author): data = AuthorSchema().dump(author) assert "links" in data assert data["links"]["self"] == f"/authors/{author.id}" def test_self_link_many(self, authors): data = AuthorSchema(many=True).dump(authors) assert "links" in data assert data["links"]["self"] == "/authors/" def test_dump_to(self, post): data = PostSchema().dump(post) assert "data" in data assert "attributes" in data["data"] assert "title" in data["data"]["attributes"] assert "relationships" in data["data"] assert "post-comments" in data["data"]["relationships"] def test_dump_none(self): data = AuthorSchema().dump(None) assert "data" in data assert data["data"] is None assert "links" not in data def test_schema_with_relationship_processes_none(self): data = CommentSchema().dump(None) assert data == {"data": None} def test_dump_empty_list(self): data = AuthorSchema(many=True).dump([]) assert "data" in data assert type(data["data"]) is list assert len(data["data"]) == 0 assert "links" in data assert data["links"]["self"] == "/authors/" class TestCompoundDocuments: def test_include_data_with_many(self, post): data = PostSchema(include_data=("post_comments", "post_comments.author")).dump( post ) assert "included" in data assert len(data["included"]) == 4 first_comment = [i for i in data["included"] if i["type"] == "comments"][0] assert "attributes" in first_comment assert "body" in first_comment["attributes"] def test_include_data_with_single(self, post): data = PostSchema(include_data=("author",)).dump(post) assert "included" in data assert len(data["included"]) == 1 author = data["included"][0] assert "attributes" in author assert "first_name" in author["attributes"] def test_include_data_with_all_relations(self, post): data = PostSchema( include_data=("author", "post_comments", "post_comments.author") ).dump(post) assert "included" in data assert len(data["included"]) == 5 for included in data["included"]: assert included["id"] assert included["type"] in ("people", "comments") expected_comments_author_ids = { str(comment.author.id) for comment in post.comments } included_comments_author_ids = { i["id"] for i in data["included"] if i["type"] == "people" and i["id"] != str(post.author.id) } assert included_comments_author_ids == expected_comments_author_ids def test_include_no_data(self, post): data = PostSchema(include_data=()).dump(post) assert "included" not in data def test_include_self_referential_relationship(self): class RefSchema(Schema): id = fields.Int() data = fields.Str() parent = fields.Relationship(schema="self", many=False) class Meta: type_ = "refs" obj = {"id": 1, "data": "data1", "parent": {"id": 2, "data": "data2"}} data = RefSchema(include_data=("parent",)).dump(obj) assert "included" in data assert data["included"][0]["attributes"]["data"] == "data2" def test_include_self_referential_relationship_many(self): class RefSchema(Schema): id = fields.Str() data = fields.Str() children = fields.Relationship(schema="self", many=True) class Meta: type_ = "refs" obj = { "id": "1", "data": "data1", "children": [{"id": "2", "data": "data2"}, {"id": "3", "data": "data3"}], } data = RefSchema(include_data=("children",)).dump(obj) assert "included" in data assert len(data["included"]) == 2 for child in data["included"]: assert child["attributes"]["data"] == "data%s" % child["id"] def test_include_self_referential_relationship_many_deep(self): class RefSchema(Schema): id = fields.Str() data = fields.Str() children = fields.Relationship(schema="self", type_="refs", many=True) class Meta: type_ = "refs" obj = { "id": "1", "data": "data1", "children": [ {"id": "2", "data": "data2", "children": []}, { "id": "3", "data": "data3", "children": [ {"id": "4", "data": "data4", "children": []}, {"id": "5", "data": "data5", "children": []}, ], }, ], } data = RefSchema(include_data=("children",)).dump(obj) assert "included" in data assert len(data["included"]) == 4 for child in data["included"]: assert child["attributes"]["data"] == "data%s" % child["id"] def test_include_data_with_many_and_schema_as_class(self, post): class PostClassSchema(PostSchema): post_comments = fields.Relationship( "http://test.test/posts/{id}/comments/", related_url_kwargs={"id": ""}, attribute="comments", dump_to="post-comments", schema=CommentSchema, many=True, ) class Meta(PostSchema.Meta): pass data = PostClassSchema(include_data=("post_comments",)).dump(post) assert "included" in data assert len(data["included"]) == 2 first_comment = data["included"][0] assert "attributes" in first_comment assert "body" in first_comment["attributes"] def test_include_data_with_nested_only_arg(self, post): data = PostSchema( only=( "id", "post_comments.id", "post_comments.author.id", "post_comments.author.twitter", ), include_data=("post_comments", "post_comments.author"), ).dump(post) assert "included" in data assert len(data["included"]) == 4 first_author = [i for i in data["included"] if i["type"] == "people"][0] assert "twitter" in first_author["attributes"] for attribute in ("first_name", "last_name"): assert attribute not in first_author["attributes"] def test_include_data_with_nested_exclude_arg(self, post): data = PostSchema( exclude=("post_comments.author.twitter",), include_data=("post_comments", "post_comments.author"), ).dump(post) assert "included" in data assert len(data["included"]) == 4 first_author = [i for i in data["included"] if i["type"] == "people"][0] assert "twitter" not in first_author["attributes"] for attribute in ("first_name", "last_name"): assert attribute in first_author["attributes"] def test_include_data_load(self, post): serialized = PostSchema( include_data=("author", "post_comments", "post_comments.author") ).dump(post) loaded = PostSchema().load(serialized) assert "author" in loaded assert loaded["author"]["id"] == str(post.author.id) assert loaded["author"]["first_name"] == post.author.first_name assert "comments" in loaded assert len(loaded["comments"]) == len(post.comments) for comment in loaded["comments"]: assert "body" in comment assert comment["id"] in [str(c.id) for c in post.comments] def test_include_data_load_null(self, post_with_null_author): serialized = PostSchema(include_data=("author", "post_comments")).dump( post_with_null_author ) with pytest.raises(ValidationError) as excinfo: PostSchema().load(serialized) err = excinfo.value assert "author" in err.args[0] def test_include_data_load_without_schema_loads_only_ids(self, post): class PostInnerSchemalessSchema(Schema): id = fields.Str() comments = fields.Relationship( "http://test.test/posts/{id}/comments/", related_url_kwargs={"id": ""}, data_key="post-comments", load_from="post-comments", many=True, type_="comments", ) class Meta: type_ = "posts" strict = True serialized = PostSchema(include_data=("author", "post_comments")).dump(post) loaded = PostInnerSchemalessSchema(unknown=INCLUDE).load(serialized) assert "comments" in loaded assert len(loaded["comments"]) == len(post.comments) for comment_id in loaded["comments"]: assert int(comment_id) in [c.id for c in post.comments] def test_include_data_with_schema_context(self, post): class ContextTestSchema(Schema): id = fields.Str() from_context = fields.Method("get_from_context") def get_from_context(self, obj): return self.context["some_value"] class Meta: type_ = "people" class PostContextTestSchema(PostSchema): author = fields.Relationship( "http://test.test/posts/{id}/author/", related_url_kwargs={"id": ""}, schema=ContextTestSchema, many=False, ) class Meta(PostSchema.Meta): pass serialized = PostContextTestSchema( include_data=("author",), context={"some_value": "Hello World"} ).dump(post) for included in serialized["included"]: if included["type"] == "people": assert "from_context" in included["attributes"] assert included["attributes"]["from_context"] == "Hello World" def get_error_by_field(errors, field): for err in errors["errors"]: # Relationship error pointers won't match with this. if err["source"]["pointer"].endswith("/" + field): return err return None class TestErrorFormatting: def test_validate(self): author = make_serialized_author({"first_name": "Dan", "password": "short"}) errors = AuthorSchema().validate(author) assert "errors" in errors assert len(errors["errors"]) == 2 password_err = get_error_by_field(errors, "password") assert password_err assert password_err["detail"] == "Shorter than minimum length 6." lname_err = get_error_by_field(errors, "last_name") assert lname_err assert lname_err["detail"] == "Missing data for required field." def test_errors_in_strict_mode(self): author = make_serialized_author({"first_name": "Dan", "password": "short"}) with pytest.raises(ValidationError) as excinfo: AuthorSchema().load(author) errors = excinfo.value.messages assert "errors" in errors assert len(errors["errors"]) == 2 password_err = get_error_by_field(errors, "password") assert password_err assert password_err["detail"] == "Shorter than minimum length 6." lname_err = get_error_by_field(errors, "last_name") assert lname_err assert lname_err["detail"] == "Missing data for required field." def test_no_type_raises_error(self): author = { "data": {"attributes": {"first_name": "Dan", "password": "supersecure"}} } with pytest.raises(ValidationError) as excinfo: AuthorSchema().load(author) expected = { "errors": [ { "detail": "`data` object must include `type` key.", "source": {"pointer": "/data"}, } ] } assert excinfo.value.messages == expected errors = AuthorSchema().validate(author) assert errors == expected def test_validate_no_data_raises_error(self): author = {"meta": {"this": "that"}} with pytest.raises(ValidationError) as excinfo: AuthorSchema().load(author) errors = excinfo.value.messages expected = { "errors": [ { "detail": "Object must include `data` key.", "source": {"pointer": "/"}, } ] } assert errors == expected def test_validate_type(self): author = { "data": { "type": "invalid", "attributes": {"first_name": "Dan", "password": "supersecure"}, } } with pytest.raises(IncorrectTypeError) as excinfo: AuthorSchema().validate(author) assert excinfo.value.args[0] == 'Invalid type. Expected "people".' assert excinfo.value.messages == { "errors": [ { "detail": 'Invalid type. Expected "people".', "source": {"pointer": "/data/type"}, } ] } def test_validate_id(self): """the pointer for id should be at the data object, not attributes""" author = { "data": { "type": "people", "id": 123, "attributes": {"first_name": "Rob", "password": "correcthorses"}, } } try: errors = AuthorSchema().validate(author) except ValidationError as err: errors = err.messages assert "errors" in errors assert len(errors["errors"]) == 2 lname_err = get_error_by_field(errors, "last_name") assert lname_err assert lname_err["source"]["pointer"] == "/data/attributes/last_name" assert lname_err["detail"] == "Missing data for required field." id_err = get_error_by_field(errors, "id") assert id_err assert id_err["source"]["pointer"] == "/data/id" assert id_err["detail"] == "Not a valid string." def test_load(self): with pytest.raises(ValidationError) as excinfo: AuthorSchema().load( make_serialized_author({"first_name": "Dan", "password": "short"}) ) errors = excinfo.value.messages assert "errors" in errors assert len(errors["errors"]) == 2 password_err = get_error_by_field(errors, "password") assert password_err assert password_err["detail"] == "Shorter than minimum length 6." lname_err = get_error_by_field(errors, "last_name") assert lname_err assert lname_err["detail"] == "Missing data for required field." def test_errors_is_empty_if_valid(self): errors = AuthorSchema().validate( make_serialized_author( { "first_name": "Dan", "last_name": "Gebhardt", "password": "supersecret", } ) ) assert errors == {} def test_errors_many(self): authors = make_serialized_authors( [ {"first_name": "Dan", "last_name": "Gebhardt", "password": "bad"}, { "first_name": "Dan", "last_name": "Gebhardt", "password": "supersecret", }, ] ) try: errors = AuthorSchema(many=True).validate(authors)["errors"] except ValidationError as err: errors = err.messages["errors"] assert len(errors) == 1 err = errors[0] assert "source" in err assert err["source"]["pointer"] == "/data/0/attributes/password" def test_errors_many_not_list(self): authors = make_serialized_author( {"first_name": "Dan", "last_name": "Gebhardt", "password": "bad"} ) try: errors = AuthorSchema(many=True).validate(authors)["errors"] except ValidationError as err: errors = err.messages["errors"] assert len(errors) == 1 err = errors[0] assert "source" in err assert err["source"]["pointer"] == "/data" assert err["detail"] == "`data` expected to be a collection." def test_many_id_errors(self): """the pointer for id should be at the data object, not attributes""" author = { "data": [ { "type": "people", "id": "invalid", "attributes": {"first_name": "Rob", "password": "correcthorses"}, }, { "type": "people", "id": 37, "attributes": { "first_name": "Dan", "last_name": "Gebhardt", "password": "supersecret", }, }, ] } errors = AuthorSchema(many=True).validate(author) assert "errors" in errors assert len(errors["errors"]) == 2 lname_err = get_error_by_field(errors, "last_name") assert lname_err assert lname_err["source"]["pointer"] == "/data/0/attributes/last_name" assert lname_err["detail"] == "Missing data for required field." id_err = get_error_by_field(errors, "id") assert id_err assert id_err["source"]["pointer"] == "/data/1/id" assert id_err["detail"] == "Not a valid string." def test_nested_fields_error(self): min_size = 10 class ThirdLevel(ma.Schema): number = fields.Int(required=True, validate=ma.validate.Range(min=min_size)) class SecondLevel(ma.Schema): foo = fields.Str(required=True) third = fields.Nested(ThirdLevel) class FirstLevel(Schema): class Meta: type_ = "first" id = fields.Int() second = fields.Nested(SecondLevel) schema = FirstLevel() result = schema.validate( { "data": { "type": "first", "attributes": {"second": {"third": {"number": 5}}}, } } ) def sort_func(d): return d["source"]["pointer"] expected_errors = sorted( [ { "source": {"pointer": "/data/attributes/second/third/number"}, "detail": f"Must be greater than or equal to {min_size}.", }, { "source": {"pointer": "/data/attributes/second/foo"}, "detail": ma.fields.Field.default_error_messages["required"], }, ], key=sort_func, ) errors = sorted(result["errors"], key=sort_func) assert errors == expected_errors class TestMeta: shape = { "id": 1, "sides": 3, "meta": "triangle", "resource_meta": {"concave": False}, "document_meta": {"page": 1}, } shapes = [ { "id": 1, "sides": 3, "meta": "triangle", "resource_meta": {"concave": False}, "document_meta": {"page": 1}, }, { "id": 2, "sides": 4, "meta": "quadrilateral", "resource_meta": {"concave": True}, "document_meta": {"page": 1}, }, ] def test_dump_single(self): serialized = PolygonSchema().dump(self.shape) assert "meta" in serialized assert serialized["meta"] == self.shape["document_meta"] assert serialized["data"]["attributes"]["meta"] == self.shape["meta"] assert serialized["data"]["meta"] == self.shape["resource_meta"] def test_dump_many(self): serialized = PolygonSchema(many=True).dump(self.shapes) assert "meta" in serialized assert serialized["meta"] == self.shapes[0]["document_meta"] first = serialized["data"][0] assert first["attributes"]["meta"] == self.shapes[0]["meta"] assert first["meta"] == self.shapes[0]["resource_meta"] second = serialized["data"][1] assert second["attributes"]["meta"] == self.shapes[1]["meta"] assert second["meta"] == self.shapes[1]["resource_meta"] def test_load_single(self): serialized = PolygonSchema().dump(self.shape) loaded = PolygonSchema().load(serialized) assert loaded["meta"] == self.shape["meta"] assert loaded["resource_meta"] == self.shape["resource_meta"] assert loaded["document_meta"] == self.shape["document_meta"] def test_load_many(self): serialized = PolygonSchema(many=True).dump(self.shapes) loaded = PolygonSchema(many=True).load(serialized) first = loaded[0] assert first["meta"] == self.shapes[0]["meta"] assert first["resource_meta"] == self.shapes[0]["resource_meta"] assert first["document_meta"] == self.shapes[0]["document_meta"] second = loaded[1] assert second["meta"] == self.shapes[1]["meta"] assert second["resource_meta"] == self.shapes[1]["resource_meta"] assert second["document_meta"] == self.shapes[1]["document_meta"] def assert_relationship_error(pointer, errors): """Walk through the dictionary and determine if a specific relationship pointer exists """ pointer = f"/data/relationships/{pointer}/data" for error in errors: if pointer == error["source"]["pointer"]: return True return False class TestRelationshipLoading: article = { "data": { "id": "1", "type": "articles", "attributes": {"body": "Test"}, "relationships": { "author": {"data": {"type": "people", "id": "1"}}, "comments": {"data": [{"type": "comments", "id": "1"}]}, }, } } def test_deserializing_relationship_fields(self): data = ArticleSchema().load(self.article) assert data["body"] == "Test" assert data["author"] == "1" assert data["comments"] == ["1"] def test_deserializing_nested_relationship_fields(self): class RelationshipWithSchemaCommentSchema(Schema): id = fields.Str() body = fields.Str(required=True) author = fields.Relationship( schema=AuthorSchema, many=False, type_="people" ) class Meta: type_ = "comments" strict = True class RelationshipWithSchemaArticleSchema(Schema): id = fields.Integer() body = fields.String() comments = fields.Relationship( schema=RelationshipWithSchemaCommentSchema, many=True, type_="comments" ) author = fields.Relationship( dump_only=False, include_resource_linkage=True, many=False, type_="people", ) class Meta: type_ = "articles" strict = True article = self.article.copy() article["included"] = [ { "id": "1", "type": "comments", "attributes": {"body": "Test comment"}, "relationships": {"author": {"data": {"type": "people", "id": "2"}}}, }, { "id": "2", "type": "people", "attributes": {"first_name": "Marshmallow Jr", "last_name": "JsonAPI"}, }, ] included_author = filter( lambda item: item["type"] == "people", article["included"] ) included_author = list(included_author)[0] data = RelationshipWithSchemaArticleSchema().load(article) author = data["comments"][0]["author"] assert isinstance(author, dict) assert author["first_name"] == included_author["attributes"]["first_name"] def test_deserializing_relationship_errors(self): data = self.article data["data"]["relationships"]["author"]["data"] = {} data["data"]["relationships"]["comments"]["data"] = [{}] with pytest.raises(ValidationError) as excinfo: ArticleSchema().load(data) errors = excinfo.value.messages assert assert_relationship_error("author", errors["errors"]) assert assert_relationship_error("comments", errors["errors"]) def test_deserializing_missing_required_relationship(self): class ArticleSchemaRequiredRelationships(Schema): id = fields.Integer() body = fields.String() author = fields.Relationship( dump_only=False, include_resource_linkage=True, many=False, type_="people", required=True, ) comments = fields.Relationship( dump_only=False, include_resource_linkage=True, many=True, type_="comments", required=True, ) class Meta: type_ = "articles" strict = True article = self.article.copy() article["data"]["relationships"] = {} with pytest.raises(ValidationError) as excinfo: ArticleSchemaRequiredRelationships().load(article) errors = excinfo.value.messages assert assert_relationship_error("author", errors["errors"]) assert assert_relationship_error("comments", errors["errors"]) def test_deserializing_relationship_with_missing_param(self): class ArticleMissingParamSchema(Schema): id = fields.Integer() body = fields.String() author = fields.Relationship( dump_only=False, include_resource_linkage=True, many=False, type_="people", missing="1", ) comments = fields.Relationship( dump_only=False, include_resource_linkage=True, many=True, type_="comments", missing=["2", "3"], ) class Meta: type_ = "articles" strict = True article = self.article.copy() article["data"]["relationships"] = {} data = ArticleMissingParamSchema().load(article) assert "author" in data assert data["author"] == "1" assert "comments" in data assert data["comments"] == ["2", "3"] ================================================ FILE: tests/test_utils.py ================================================ import pytest from marshmallow_jsonapi import utils @pytest.mark.parametrize( "tag,val", [ ("", "id"), ("", "author.last_name"), ("", "comment.author.first_name"), ("True", None), ("", None), ], ) def test_tpl(tag, val): assert utils.tpl(tag) == val ================================================ FILE: tox.ini ================================================ [tox] envlist= lint py{36,37,38,39}-marshmallow3 py39-marshmallowdev docs [testenv] extras = tests deps = marshmallow3: marshmallow>=3.0.0<4.0.0 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz commands = pytest {posargs} [testenv:lint] deps = pre-commit~=2.0 skip_install = true commands = pre-commit run --all-files [testenv:docs] deps = -rdocs/requirements.txt extras = commands = sphinx-build docs/ docs/_build {posargs} ; Below tasks are for development only (not run in CI) [testenv:watch-docs] deps = -rdocs/requirements.txt sphinx-autobuild extras = commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch marshmallow_jsonapi --delay 2 [testenv:watch-readme] deps = restview skip_install = true commands = restview README.rst