dev e7273b4c3e6b cached
39 files
158.0 KB
39.0k tokens
256 symbols
1 requests
Download .txt
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 <https://github.com/sloria>`_

Contributors (chronological)
============================

- Jotham Apaloo `@jo-tham <https://github.com/jo-tham>`_
- Anders Steinlein `@asteinlein <https://github.com/asteinlein>`_
- `@floqqi <https://github.com/floqqi>`_
- Colton Allen `@cmanallen <https://github.com/cmanallen>`_
- Dominik Steinberger `@ZeeD26 <https://github.com/ZeeD26>`_
- Tim Mundt `@Tim-Erwin <https://github.com/Tim-Erwin>`_
- Brandon Wood `@woodb <https://github.com/woodb>`_
- Frazer McLean `@RazerM <https://github.com/RazerM>`_
- J Rob Gant `@rgant <https://github.com/rgant>`_
- Dan Poland `@danpoland <https://github.com/danpoland>`_
- Pierre CHAISY `@akira-dev <https://github.com/akira-dev>`_
- `@mrhanky17 <https://github.com/mrhanky17>`_
- Mark Hall `@scmmmh <https://github.com/scmmmh>`_
- Scott Werner `@scottwernervt <https://github.com/scottwernervt>`_
- Michael Dodsworth `@mdodsworth <https://github.com/mdodsworth>`_
- Mathieu Alorent `@kumy <https://github.com/kumy>`_
- Grant Harris `@grantHarris <https://github.com/grantHarris>`_
- Robert Sawicki `@ww3pl <https://github.com/ww3pl>`_
- `@aberres <https://github.com/aberres>`_
- George Alton `@georgealton <https://github.com/georgealton>`_
- Areeb Jamal `@iamareebjamal <https://github.com/iamareebjamal>`_
- Suren Khorenyan `@mahenzon <https://github.com/mahenzon>`_
- Karthikeyan Singaravelan `@tirkarthi <https://github.com/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 <http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
::

    $ 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 <https://dev.azure.com/sloria/sloria/_build/latest?definitionId=7&branchName=dev>`_ 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 <https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_. 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 <http://jsonapi.org/>`_) formatting with `marshmallow <https://marshmallow.readthedocs.io>`_.

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": "<author.id>"}
        )

        comments = fields.Relationship(
            "/posts/{post_id}/comments",
            related_url_kwargs={"post_id": "<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 <https://github.com/marshmallow-code/marshmallow-jsonapi/blob/master/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 <target>' where <target> 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 <changelog>`)

JSON API 1.0 (`https://jsonapi.org <http://jsonapi.org/>`_) formatting with `marshmallow <https://marshmallow.readthedocs.io>`_.

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": "<author.id>"},
        )

        comments = fields.Relationship(
            related_url="/posts/{post_id}/comments",
            related_url_kwargs={"post_id": "<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 <https://github.com/marshmallow-code/marshmallow-jsonapi>`_
- `marshmallow-jsonapi @ PyPI <https://pypi.python.org/pypi/marshmallow-jsonapi>`_
- `Issue Tracker <https://github.com/marshmallow-code/marshmallow-jsonapi/issues>`_


================================================
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 ^<target^>` where ^<target^> 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 <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": "<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 <http://jsonapi.org/format/#document-structure>`_ 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 <marshmallow_json.fields.Relationship>` field is used to serialize `relationship objects <http://jsonapi.org/format/#document-resource-object-relationships>`_. 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": "<id>"},
            related_url="/authors/{author_id}",
            related_url_kwargs={"author_id": "<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 <http://jsonapi.org/format/#document-resource-object-linkage>`_ 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": "<id>"},
            related_url="/authors/{author_id}",
            related_url_kwargs={"author_id": "<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 <http://jsonapi.org/format/#document-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": "<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": "<id>"},
            related_url="/authors/{author_id}",
            related_url_kwargs={"author_id": "<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": "<id>"},
            related_url="/comments/{author_id}",
            related_url_kwargs={"author_id": "<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" <http://jsonapi.org/format/#document-meta>`_.

.. 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 <http://jsonapi.org/format/#document-resource-objects>`_.

.. 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 <http://jsonapi.org/format/#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 <http://jsonapi.org/recommendations/#naming>`_ 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": "<id>"},
            related_view="author_detail",
            related_view_kwargs={"author_id": "<author.id>"},
        )

        comments = Relationship(
            related_view="post_comments",
            related_view_kwargs={"post_id": "<id>"},
            many=True,
            include_resource_linkage=True,
            type_="comments",
        )

        class Meta:
            type_ = "posts"
            self_view = "post_detail"
            self_view_kwargs = {"post_detail": "<id>"}
            self_view_many = "posts_list"

See `here <https://github.com/marshmallow-code/marshmallow-jsonapi/blob/dev/examples/flask_example.py>`_ 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": "<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": "<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": "<author.id>", "_external": True},
        include_data=True,
        type_="people",
    )

    comments = Relationship(
        related_view="posts_comments",
        related_view_kwargs={"post_id": "<id>", "_external": True},
        many=True,
        include_data=True,
        type_="comments",
    )

    class Meta:
        type_ = "posts"
        self_view = "posts_detail"
        self_view_kwargs = {"post_id": "<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/<int:post_id>")
def posts_detail(post_id):
    post = db["posts"][post_id - 1]
    data = PostSchema().dump(post)
    return J(data)


@app.route("/posts/<int:post_id>/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/<int:author_id>")
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/<int:comment_id>")
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': '<author.id>'},
        )

        comments = Relationship(
            related_url='/posts/{post_id}/comments/',
            related_url_kwargs={'post_id': '<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': '<author.id>'},
        )

        comments = Relationship(
            related_view='posts_comments',
            related_view_kwargs={'post_id': '<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': '<author.id>'},
            )

            comments = fields.HyperlinkRelated(
                '/posts/{post_id}/comments',
                url_kwargs={'post_id': '<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": "<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": "<id>"},
        schema=AuthorSchema,
        many=False,
        type_="people",
    )

    post_comments = fields.Relationship(
        "http://test.test/posts/{id}/comments/",
        related_url_kwargs={"id": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<author.last_name>"},
            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/<post_id>/")
    def post_detail(post_id):
        return f"Detail for post {post_id}"

    @app_.route("/posts/<post_id>/comments/")
    def posts_comments(post_id):
        return f"Comments for post {post_id}"

    @app_.route("/authors/<int:author_id>")
    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": "<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": "<author.last_name>"},
            default=None,
        )

        class Meta:
            type_ = "posts"
            self_view = "post_detail"
            self_view_kwargs = {"post_id": "<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": "<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": "<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": "<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": "<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": "<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": "<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": "<author>"}
        )
        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": "<author>"},
        )
        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": "<author.last_name>"},
            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": "<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": "<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": "<first_name>", "last_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": "<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": "<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": "<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": "<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>", "id"),
        ("<author.last_name>", "author.last_name"),
        ("<comment.author.first_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
Download .txt
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
Download .txt
SYMBOL INDEX (256 symbols across 14 files)

FILE: examples/flask_example.py
  class Model (line 6) | class Model:
    method __init__ (line 7) | def __init__(self, **kwargs):
  class Comment (line 12) | class Comment(Model):
  class Author (line 16) | class Author(Model):
  class Post (line 20) | class Post(Model):
  class CommentSchema (line 49) | class CommentSchema(Schema):
    class Meta (line 53) | class Meta:
  class AuthorSchema (line 60) | class AuthorSchema(Schema):
    class Meta (line 67) | class Meta:
  class PostSchema (line 74) | class PostSchema(Schema):
    class Meta (line 93) | class Meta:
  function J (line 106) | def J(*args, **kwargs):
  function posts_list (line 116) | def posts_list():
  function posts_detail (line 123) | def posts_detail(post_id):
  function posts_comments (line 130) | def posts_comments(post_id):
  function authors_list (line 138) | def authors_list():
  function author_detail (line 145) | def author_detail(author_id):
  function author_create (line 152) | def author_create():
  function comments_list (line 167) | def comments_list():
  function comment_detail (line 174) | def comment_detail(comment_id):

FILE: marshmallow_jsonapi/exceptions.py
  class JSONAPIError (line 4) | class JSONAPIError(Exception):
  class IncorrectTypeError (line 10) | class IncorrectTypeError(JSONAPIError, ValueError):
    method __init__ (line 16) | def __init__(self, message=None, actual=None, expected=None):
    method messages (line 27) | def messages(self):

FILE: marshmallow_jsonapi/fields.py
  class BaseRelationship (line 25) | class BaseRelationship(Field):
  function _stringify (line 37) | def _stringify(value):
  class Relationship (line 43) | class Relationship(BaseRelationship):
    method __init__ (line 81) | def __init__(
    method id_field (line 112) | def id_field(self):
    method schema (line 122) | def schema(self):
    method get_related_url (line 153) | def get_related_url(self, obj):
    method get_self_url (line 163) | def get_self_url(self, obj):
    method get_resource_linkage (line 173) | def get_resource_linkage(self, value):
    method extract_value (line 186) | def extract_value(self, data):
    method deserialize (line 215) | def deserialize(self, value, attr=None, data=None, **kwargs):
    method _deserialize (line 232) | def _deserialize(self, value, attr, obj, **kwargs):
    method serialize (line 246) | def serialize(self, attr, obj, accessor=None):
    method _serialize (line 251) | def _serialize(self, value, attr, obj):
    method _serialize_included (line 279) | def _serialize_included(self, value):
    method _get_id (line 286) | def _get_id(self, value):
  class DocumentMeta (line 293) | class DocumentMeta(Field):
    method __init__ (line 312) | def __init__(self, **kwargs):
    method _deserialize (line 316) | def _deserialize(self, value, attr, data, **kwargs):
    method _serialize (line 322) | def _serialize(self, value, *args, **kwargs):
  class ResourceMeta (line 329) | class ResourceMeta(Field):
    method __init__ (line 348) | def __init__(self, **kwargs):
    method _deserialize (line 352) | def _deserialize(self, value, attr, data, **kwargs):
    method _serialize (line 358) | def _serialize(self, value, *args, **kwargs):

FILE: marshmallow_jsonapi/flask.py
  class SchemaOpts (line 14) | class SchemaOpts(DefaultOpts):
    method __init__ (line 17) | def __init__(self, meta, *args, **kwargs):
  class Schema (line 49) | class Schema(DefaultSchema):
    class Meta (line 54) | class Meta:
    method generate_url (line 69) | def generate_url(self, view_name, **kwargs):
  class Relationship (line 74) | class Relationship(GenericRelationship):
    method __init__ (line 105) | def __init__(
    method get_url (line 120) | def get_url(self, obj, view_name, view_kwargs):
    method get_related_url (line 134) | def get_related_url(self, obj):
    method get_self_url (line 137) | def get_self_url(self, obj):

FILE: marshmallow_jsonapi/schema.py
  class SchemaOpts (line 16) | class SchemaOpts(ma.SchemaOpts):
    method __init__ (line 17) | def __init__(self, meta, *args, **kwargs):
  class Schema (line 26) | class Schema(ma.Schema):
    class Meta (line 60) | class Meta:
    method __init__ (line 76) | def __init__(self, *args, **kwargs):
    method check_relations (line 98) | def check_relations(self, relations):
    method format_json_api_response (line 122) | def format_json_api_response(self, data, many, **kwargs):
    method render_included_data (line 133) | def render_included_data(self, data):
    method render_meta_document (line 139) | def render_meta_document(self, data):
    method unwrap_item (line 145) | def unwrap_item(self, item):
    method unwrap_request (line 192) | def unwrap_request(self, data, many, **kwargs):
    method on_bind_field (line 217) | def on_bind_field(self, field_name, field_obj):
    method _do_load (line 223) | def _do_load(self, data, many=None, **kwargs):
    method _extract_from_included (line 248) | def _extract_from_included(self, data):
    method inflect (line 260) | def inflect(self, text):
    method format_errors (line 268) | def format_errors(self, errors, many):
    method _get_formatted_errors (line 284) | def _get_formatted_errors(self, errors, index=None):
    method _process_nested_errors (line 297) | def _process_nested_errors(self, name, data):
    method format_error (line 305) | def format_error(self, field_name, message, index=None):
    method format_item (line 331) | def format_item(self, item):
    method format_items (line 377) | def format_items(self, data, many):
    method get_top_level_links (line 387) | def get_top_level_links(self, data, many):
    method get_resource_links (line 400) | def get_resource_links(self, item):
    method wrap_response (line 409) | def wrap_response(self, data, many):
    method generate_url (line 420) | def generate_url(self, link, **kwargs):

FILE: marshmallow_jsonapi/utils.py
  function tpl (line 13) | def tpl(val):
  function resolve_params (line 21) | def resolve_params(obj, params, default=missing):

FILE: setup.py
  function find_version (line 12) | def find_version(fname):
  function read (line 29) | def read(fname):

FILE: tests/base.py
  class Bunch (line 11) | class Bunch:
    method __init__ (line 12) | def __init__(self, **kwargs):
  class Post (line 17) | class Post(Bunch):
  class Author (line 21) | class Author(Bunch):
  class Comment (line 25) | class Comment(Bunch):
  class Keyword (line 29) | class Keyword(Bunch):
  class AuthorSchema (line 33) | class AuthorSchema(Schema):
    method get_top_level_links (line 40) | def get_top_level_links(self, data, many):
    class Meta (line 47) | class Meta:
  class KeywordSchema (line 51) | class KeywordSchema(Schema):
    method get_attribute (line 55) | def get_attribute(self, attr, obj, default):
    class Meta (line 65) | class Meta:
  class CommentSchema (line 70) | class CommentSchema(Schema):
    class Meta (line 81) | class Meta:
  class ArticleSchema (line 86) | class ArticleSchema(Schema):
    class Meta (line 96) | class Meta:
  class PostSchema (line 101) | class PostSchema(Schema):
    class Meta (line 136) | class Meta:
  class PolygonSchema (line 141) | class PolygonSchema(Schema):
    class Meta (line 151) | class Meta:

FILE: tests/conftest.py
  function make_author (line 6) | def make_author():
  function make_post (line 15) | def make_post(with_comments=True, with_author=True, with_keywords=True):
  function make_comment (line 29) | def make_comment(with_author=True):
  function make_keyword (line 34) | def make_keyword():
  function author (line 39) | def author():
  function authors (line 44) | def authors():
  function comments (line 49) | def comments():
  function post (line 54) | def post():
  function post_with_null_comment (line 59) | def post_with_null_comment():
  function post_with_null_author (line 64) | def post_with_null_author():
  function posts (line 69) | def posts():

FILE: tests/test_fields.py
  class TestGenericRelationshipField (line 11) | class TestGenericRelationshipField:
    method test_serialize_relationship_link (line 12) | def test_serialize_relationship_link(self, post):
    method test_serialize_self_link (line 21) | def test_serialize_self_link(self, post):
    method test_include_resource_linkage_requires_type (line 33) | def test_include_resource_linkage_requires_type(self):
    method test_include_resource_linkage_single (line 45) | def test_include_resource_linkage_single(self, post):
    method test_include_resource_linkage_single_with_schema (line 57) | def test_include_resource_linkage_single_with_schema(self, post):
    method test_include_resource_linkage_single_foreign_key (line 70) | def test_include_resource_linkage_single_foreign_key(self, post):
    method test_include_resource_linkage_single_foreign_key_with_schema (line 80) | def test_include_resource_linkage_single_foreign_key_with_schema(self,...
    method test_include_resource_linkage_id_field_from_string (line 91) | def test_include_resource_linkage_id_field_from_string(self):
    method test_include_resource_linkage_id_field_from_schema (line 100) | def test_include_resource_linkage_id_field_from_schema(self):
    method test_include_resource_linkage_many (line 116) | def test_include_resource_linkage_many(self, post):
    method test_include_resource_linkage_many_with_schema (line 129) | def test_include_resource_linkage_many_with_schema(self, post):
    method test_include_resource_linkage_many_with_schema_overriding_get_attribute (line 143) | def test_include_resource_linkage_many_with_schema_overriding_get_attr...
    method test_deserialize_data_single (line 161) | def test_deserialize_data_single(self):
    method test_deserialize_data_many (line 173) | def test_deserialize_data_many(self):
    method test_deserialize_data_missing_id (line 185) | def test_deserialize_data_missing_id(self):
    method test_deserialize_data_missing_type (line 198) | def test_deserialize_data_missing_type(self):
    method test_deserialize_data_incorrect_type (line 211) | def test_deserialize_data_incorrect_type(self):
    method test_deserialize_null_data_value (line 224) | def test_deserialize_null_data_value(self):
    method test_deserialize_null_value_disallow_none (line 236) | def test_deserialize_null_value_disallow_none(self):
    method test_deserialize_empty_data_list (line 249) | def test_deserialize_empty_data_list(self):
    method test_deserialize_empty_data (line 260) | def test_deserialize_empty_data(self):
    method test_deserialize_required_missing (line 275) | def test_deserialize_required_missing(self):
    method test_deserialize_required_empty (line 288) | def test_deserialize_required_empty(self):
    method test_deserialize_many_non_list_relationship (line 301) | def test_deserialize_many_non_list_relationship(self):
    method test_deserialize_non_many_list_relationship (line 307) | def test_deserialize_non_many_list_relationship(self):
    method test_include_null_data_single (line 315) | def test_include_null_data_single(self, post_with_null_author):
    method test_include_null_data_many (line 326) | def test_include_null_data_many(self, post_with_null_comment):
    method test_exclude_data (line 338) | def test_exclude_data(self, post_with_null_comment):
    method test_empty_relationship_with_alternative_identifier_field (line 350) | def test_empty_relationship_with_alternative_identifier_field(
    method test_resource_linkage_id_type_from_schema (line 362) | def test_resource_linkage_id_type_from_schema(self):
    method test_resource_linkage_id_of_invalid_type (line 378) | def test_resource_linkage_id_of_invalid_type(self):
  class TestDocumentMetaField (line 395) | class TestDocumentMetaField:
    method test_serialize (line 396) | def test_serialize(self):
    method test_serialize_incorrect_type (line 403) | def test_serialize_incorrect_type(self):
    method test_deserialize (line 409) | def test_deserialize(self):
    method test_deserialize_incorrect_type (line 415) | def test_deserialize_incorrect_type(self):
  class TestResourceMetaField (line 423) | class TestResourceMetaField:
    method test_serialize (line 424) | def test_serialize(self):
    method test_serialize_incorrect_type (line 429) | def test_serialize_incorrect_type(self):
    method test_deserialize (line 435) | def test_deserialize(self):
    method test_deserialize_incorrect_type (line 441) | def test_deserialize_incorrect_type(self):

FILE: tests/test_flask.py
  function app (line 10) | def app():
  class TestSchema (line 37) | class TestSchema:
    class PostFlaskSchema (line 38) | class PostFlaskSchema(Schema):
      class Meta (line 42) | class Meta:
    class PostAuthorFlaskSchema (line 48) | class PostAuthorFlaskSchema(Schema):
      class Meta (line 58) | class Meta:
    method test_schema_requires_view_options (line 64) | def test_schema_requires_view_options(self):
    method test_non_existing_view (line 75) | def test_non_existing_view(self, app, post):
    method test_self_link_single (line 87) | def test_self_link_single(self, app, post):
    method test_self_link_many (line 92) | def test_self_link_many(self, app, posts):
    method test_schema_with_empty_relationship (line 100) | def test_schema_with_empty_relationship(self, app, post_with_null_auth...
  class TestRelationshipField (line 105) | class TestRelationshipField:
    method test_serialize_basic (line 106) | def test_serialize_basic(self, app, post):
    method test_serialize_external (line 116) | def test_serialize_external(self, app, post):
    method test_include_resource_linkage_requires_type (line 125) | def test_include_resource_linkage_requires_type(self, app, post):
    method test_serialize_self_link (line 137) | def test_serialize_self_link(self, app, post):
    method test_empty_relationship (line 147) | def test_empty_relationship(self, app, post_with_null_author):
    method test_non_existing_view (line 155) | def test_non_existing_view(self, app, post):
    method test_empty_relationship_with_alternative_identifier_field (line 163) | def test_empty_relationship_with_alternative_identifier_field(

FILE: tests/test_options.py
  function dasherize (line 9) | def dasherize(text):
  class AuthorSchemaWithInflection (line 13) | class AuthorSchemaWithInflection(Schema):
    class Meta (line 18) | class Meta:
  class AuthorSchemaWithOverrideInflection (line 24) | class AuthorSchemaWithOverrideInflection(Schema):
    class Meta (line 30) | class Meta:
  class TestInflection (line 36) | class TestInflection:
    method schema (line 38) | def schema(self):
    method test_dump (line 41) | def test_dump(self, schema, author):
    method test_validate_with_inflection (line 54) | def test_validate_with_inflection(self, schema):
    method test_load_with_inflection (line 64) | def test_load_with_inflection(self, schema):
    method test_load_with_inflection_and_load_from_override (line 80) | def test_load_with_inflection_and_load_from_override(self):
    method test_load_bulk_id_fields (line 89) | def test_load_bulk_id_fields(self):
    method test_relationship_keys_get_inflected (line 98) | def test_relationship_keys_get_inflected(self, post):
  class AuthorAutoSelfLinkSchema (line 123) | class AuthorAutoSelfLinkSchema(Schema):
    class Meta (line 130) | class Meta:
  class AuthorAutoSelfLinkFirstLastSchema (line 137) | class AuthorAutoSelfLinkFirstLastSchema(AuthorAutoSelfLinkSchema):
    class Meta (line 138) | class Meta:
  class TestAutoSelfUrls (line 145) | class TestAutoSelfUrls:
    method test_self_url_kwargs_requires_self_url (line 146) | def test_self_url_kwargs_requires_self_url(self, author):
    method test_self_link_single (line 157) | def test_self_link_single(self, author):
    method test_self_link_many (line 162) | def test_self_link_many(self, authors):
    method test_without_self_link (line 170) | def test_without_self_link(self, comments):

FILE: tests/test_schema.py
  function make_serialized_author (line 16) | def make_serialized_author(attributes):
  function make_serialized_authors (line 20) | def make_serialized_authors(items):
  function test_type_is_required (line 24) | def test_type_is_required():
  function test_id_field_is_required (line 36) | def test_id_field_is_required():
  class TestResponseFormatting (line 46) | class TestResponseFormatting:
    method test_dump_single (line 47) | def test_dump_single(self, author):
    method test_dump_many (line 60) | def test_dump_many(self, authors):
    method test_self_link_single (line 74) | def test_self_link_single(self, author):
    method test_self_link_many (line 79) | def test_self_link_many(self, authors):
    method test_dump_to (line 84) | def test_dump_to(self, post):
    method test_dump_none (line 92) | def test_dump_none(self):
    method test_schema_with_relationship_processes_none (line 99) | def test_schema_with_relationship_processes_none(self):
    method test_dump_empty_list (line 103) | def test_dump_empty_list(self):
  class TestCompoundDocuments (line 113) | class TestCompoundDocuments:
    method test_include_data_with_many (line 114) | def test_include_data_with_many(self, post):
    method test_include_data_with_single (line 125) | def test_include_data_with_single(self, post):
    method test_include_data_with_all_relations (line 133) | def test_include_data_with_all_relations(self, post):
    method test_include_no_data (line 153) | def test_include_no_data(self, post):
    method test_include_self_referential_relationship (line 157) | def test_include_self_referential_relationship(self):
    method test_include_self_referential_relationship_many (line 171) | def test_include_self_referential_relationship_many(self):
    method test_include_self_referential_relationship_many_deep (line 191) | def test_include_self_referential_relationship_many_deep(self):
    method test_include_data_with_many_and_schema_as_class (line 221) | def test_include_data_with_many_and_schema_as_class(self, post):
    method test_include_data_with_nested_only_arg (line 242) | def test_include_data_with_nested_only_arg(self, post):
    method test_include_data_with_nested_exclude_arg (line 261) | def test_include_data_with_nested_exclude_arg(self, post):
    method test_include_data_load (line 275) | def test_include_data_load(self, post):
    method test_include_data_load_null (line 292) | def test_include_data_load_null(self, post_with_null_author):
    method test_include_data_load_without_schema_loads_only_ids (line 302) | def test_include_data_load_without_schema_loads_only_ids(self, post):
    method test_include_data_with_schema_context (line 326) | def test_include_data_with_schema_context(self, post):
  function get_error_by_field (line 358) | def get_error_by_field(errors, field):
  class TestErrorFormatting (line 366) | class TestErrorFormatting:
    method test_validate (line 367) | def test_validate(self):
    method test_errors_in_strict_mode (line 381) | def test_errors_in_strict_mode(self):
    method test_no_type_raises_error (line 396) | def test_no_type_raises_error(self):
    method test_validate_no_data_raises_error (line 416) | def test_validate_no_data_raises_error(self):
    method test_validate_type (line 435) | def test_validate_type(self):
    method test_validate_id (line 454) | def test_validate_id(self):
    method test_load (line 480) | def test_load(self):
    method test_errors_is_empty_if_valid (line 497) | def test_errors_is_empty_if_valid(self):
    method test_errors_many (line 509) | def test_errors_many(self):
    method test_errors_many_not_list (line 531) | def test_errors_many_not_list(self):
    method test_many_id_errors (line 547) | def test_many_id_errors(self):
    method test_nested_fields_error (line 581) | def test_nested_fields_error(self):
  class TestMeta (line 630) | class TestMeta:
    method test_dump_single (line 656) | def test_dump_single(self):
    method test_dump_many (line 663) | def test_dump_many(self):
    method test_load_single (line 676) | def test_load_single(self):
    method test_load_many (line 684) | def test_load_many(self):
  function assert_relationship_error (line 699) | def assert_relationship_error(pointer, errors):
  class TestRelationshipLoading (line 710) | class TestRelationshipLoading:
    method test_deserializing_relationship_fields (line 723) | def test_deserializing_relationship_fields(self):
    method test_deserializing_nested_relationship_fields (line 729) | def test_deserializing_nested_relationship_fields(self):
    method test_deserializing_relationship_errors (line 784) | def test_deserializing_relationship_errors(self):
    method test_deserializing_missing_required_relationship (line 795) | def test_deserializing_missing_required_relationship(self):
    method test_deserializing_relationship_with_missing_param (line 828) | def test_deserializing_relationship_with_missing_param(self):

FILE: tests/test_utils.py
  function test_tpl (line 16) | def test_tpl(tag, val):
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (173K chars).
[
  {
    "path": ".gitignore",
    "chars": 459,
    "preview": "*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed."
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 497,
    "preview": "repos:\n- repo: https://github.com/asottile/pyupgrade\n  rev: v2.31.0\n  hooks:\n  - id: pyupgrade\n    args: [--py36-plus]\n-"
  },
  {
    "path": ".readthdocs.yml",
    "chars": 138,
    "preview": "version: 2\nsphinx:\n  configuration: docs/conf.py\nformats: all\npython:\n  version: 3.7\n  install:\n    - requirements: docs"
  },
  {
    "path": "AUTHORS.rst",
    "chars": 1479,
    "preview": "*******\nAuthors\n*******\n\nLead\n====\n\n- Steven Loria `@sloria <https://github.com/sloria>`_\n\nContributors (chronological)\n"
  },
  {
    "path": "CHANGELOG.rst",
    "chars": 10941,
    "preview": "*********\nChangelog\n*********\n\n0.24.0 (2020-12-27)\n===================\n\nDeprecations/Removals:\n\n* Drop support for marsh"
  },
  {
    "path": "CONTRIBUTING.rst",
    "chars": 3226,
    "preview": "Contributing Guidelines\n=======================\n\nQuestions, Feature Requests, Bug Reports, and Feedback…\n---------------"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "Copyright 2015-2020 Steven Loria and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "MANIFEST.in",
    "chars": 304,
    "preview": "include *.rst LICENSE\nrecursive-include tests *\nrecursive-include docs *\nrecursive-include examples *\nrecursive-exclude "
  },
  {
    "path": "README.rst",
    "chars": 3390,
    "preview": "*******************\nmarshmallow-jsonapi\n*******************\n\n.. image:: https://badgen.net/pypi/v/marshmallow-jsonapi\n  "
  },
  {
    "path": "azure-pipelines.yml",
    "chars": 824,
    "preview": "trigger:\n  branches:\n    include: [dev, test-me-*]\n  tags:\n    include: [\"*\"]\n\n# Run builds nightly to catch incompatibi"
  },
  {
    "path": "docs/Makefile",
    "chars": 6778,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/api_reference.rst",
    "chars": 420,
    "preview": ".. _api:\n\n*************\nAPI Reference\n*************\n\nCore\n====\n\n.. automodule:: marshmallow_jsonapi\n    :members:\n\nField"
  },
  {
    "path": "docs/authors.rst",
    "chars": 28,
    "preview": ".. include:: ../AUTHORS.rst\n"
  },
  {
    "path": "docs/changelog.rst",
    "chars": 46,
    "preview": ".. _changelog:\n\n.. include:: ../CHANGELOG.rst\n"
  },
  {
    "path": "docs/conf.py",
    "chars": 1058,
    "preview": "import datetime as dt\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(\"..\"))\nimport marshmallow_jsonapi  # noqa"
  },
  {
    "path": "docs/contributing.rst",
    "chars": 33,
    "preview": ".. include:: ../CONTRIBUTING.rst\n"
  },
  {
    "path": "docs/index.rst",
    "chars": 2452,
    "preview": "*******************\nmarshmallow-jsonapi\n*******************\n\nRelease v\\ |version|. (:ref:`Changelog <changelog>`)\n\nJSON "
  },
  {
    "path": "docs/license.rst",
    "chars": 56,
    "preview": "*******\nLicense\n*******\n\n.. literalinclude:: ../LICENSE\n"
  },
  {
    "path": "docs/make.bat",
    "chars": 6467,
    "preview": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUI"
  },
  {
    "path": "docs/quickstart.rst",
    "chars": 16427,
    "preview": "**********\nQuickstart\n**********\n\n.. note:: The following guide assumes some familiarity with the marshmallow API. To le"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 94,
    "preview": "marshmallow>=2.0.0rc1\nFlask==1.1.2\nsphinx==3.5.3\nsphinx-rtd-theme==0.5.0\nsphinx-issues>=0.2.0\n"
  },
  {
    "path": "examples/flask_example.py",
    "chars": 4164,
    "preview": "from flask import Flask, request, jsonify\n\n### MODELS ###\n\n\nclass Model:\n    def __init__(self, **kwargs):\n        for k"
  },
  {
    "path": "marshmallow_jsonapi/__init__.py",
    "chars": 98,
    "preview": "from .schema import Schema, SchemaOpts\n\n__version__ = \"0.24.0\"\n__all__ = (\"Schema\", \"SchemaOpts\")\n"
  },
  {
    "path": "marshmallow_jsonapi/exceptions.py",
    "chars": 914,
    "preview": "\"\"\"Exception classes.\"\"\"\n\n\nclass JSONAPIError(Exception):\n    \"\"\"Base class for all exceptions in this package.\"\"\"\n\n    "
  },
  {
    "path": "marshmallow_jsonapi/fields.py",
    "chars": 12804,
    "preview": "\"\"\"Includes all the fields classes from `marshmallow.fields` as well as\nfields for serializing JSON API-formatted hyperl"
  },
  {
    "path": "marshmallow_jsonapi/flask.py",
    "chars": 5107,
    "preview": "\"\"\"Flask integration that avoids the need to hard-code URLs for links.\n\nThis includes a Flask-specific schema with custo"
  },
  {
    "path": "marshmallow_jsonapi/schema.py",
    "chars": 15217,
    "preview": "import itertools\n\nimport marshmallow as ma\nfrom marshmallow.exceptions import ValidationError\nfrom marshmallow.utils imp"
  },
  {
    "path": "marshmallow_jsonapi/utils.py",
    "chars": 1166,
    "preview": "\"\"\"Utility functions.\n\nThis module should be considered private API.\n\"\"\"\nimport re\n\nfrom marshmallow.utils import get_va"
  },
  {
    "path": "setup.cfg",
    "chars": 398,
    "preview": "[metadata]\nlicense_files = LICENSE\n\n[bdist_wheel]\n# This flag says that the code is written to work on both Python 2 and"
  },
  {
    "path": "setup.py",
    "chars": 2303,
    "preview": "import re\nfrom setuptools import setup, find_packages\n\nINSTALL_REQUIRES = (\"marshmallow>=2.15.2\",)\nEXTRAS_REQUIRE = {\n  "
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/base.py",
    "chars": 3658,
    "preview": "from hashlib import md5\n\nfrom faker import Factory\nfrom marshmallow import validate\n\nfrom marshmallow_jsonapi import Sch"
  },
  {
    "path": "tests/conftest.py",
    "chars": 1521,
    "preview": "import pytest\n\nfrom tests.base import Author, Post, Comment, Keyword, fake\n\n\ndef make_author():\n    return Author(\n     "
  },
  {
    "path": "tests/test_fields.py",
    "chars": 16428,
    "preview": "import pytest\n\nfrom hashlib import md5\nfrom marshmallow import ValidationError, missing as missing_\nfrom marshmallow.fie"
  },
  {
    "path": "tests/test_flask.py",
    "chars": 5608,
    "preview": "from flask import Flask, url_for\nimport pytest\nfrom werkzeug.routing import BuildError\n\nfrom marshmallow_jsonapi import "
  },
  {
    "path": "tests/test_options.py",
    "chars": 5917,
    "preview": "import pytest\nfrom marshmallow import validate, ValidationError\n\nfrom marshmallow_jsonapi import Schema, fields\nfrom tes"
  },
  {
    "path": "tests/test_schema.py",
    "chars": 29121,
    "preview": "import pytest\nimport marshmallow as ma\nfrom marshmallow import ValidationError, INCLUDE\n\nfrom marshmallow_jsonapi import"
  },
  {
    "path": "tests/test_utils.py",
    "chars": 358,
    "preview": "import pytest\n\nfrom marshmallow_jsonapi import utils\n\n\n@pytest.mark.parametrize(\n    \"tag,val\",\n    [\n        (\"<id>\", \""
  },
  {
    "path": "tox.ini",
    "chars": 826,
    "preview": "[tox]\nenvlist=\n    lint\n    py{36,37,38,39}-marshmallow3\n    py39-marshmallowdev\n    docs\n\n[testenv]\nextras = tests\ndeps"
  }
]

About this extraction

This page contains the full source code of the marshmallow-code/marshmallow-jsonapi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (158.0 KB), approximately 39.0k tokens, and a symbol index with 256 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!