Import upstream version 7.0.1+git20210322.e92f875, md5 9d8156895adfb14b6bfb66f8de2ef5ac
Kali Janitor
3 years ago
0 | # Byte-compiled / optimized / DLL files | |
1 | __pycache__/ | |
2 | *.py[cod] | |
3 | *$py.class | |
4 | ||
5 | # C extensions | |
6 | *.so | |
7 | ||
8 | # Distribution / packaging | |
9 | .Python | |
10 | build/ | |
11 | develop-eggs/ | |
12 | dist/ | |
13 | downloads/ | |
14 | eggs/ | |
15 | .eggs/ | |
16 | lib/ | |
17 | lib64/ | |
18 | parts/ | |
19 | sdist/ | |
20 | var/ | |
21 | wheels/ | |
22 | *.egg-info/ | |
23 | .installed.cfg | |
24 | *.egg | |
25 | MANIFEST | |
26 | ||
27 | # PyInstaller | |
28 | # Usually these files are written by a python script from a template | |
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. | |
30 | *.manifest | |
31 | *.spec | |
32 | ||
33 | # Installer logs | |
34 | pip-log.txt | |
35 | pip-delete-this-directory.txt | |
36 | ||
37 | # Unit test / coverage reports | |
38 | htmlcov/ | |
39 | .tox/ | |
40 | .coverage | |
41 | .coverage.* | |
42 | .cache | |
43 | nosetests.xml | |
44 | coverage.xml | |
45 | *.cover | |
46 | .hypothesis/ | |
47 | .pytest_cache/ | |
48 | ||
49 | # Translations | |
50 | *.mo | |
51 | *.pot | |
52 | ||
53 | # Django stuff: | |
54 | *.log | |
55 | local_settings.py | |
56 | db.sqlite3 | |
57 | ||
58 | # Flask stuff: | |
59 | instance/ | |
60 | .webassets-cache | |
61 | ||
62 | # Scrapy stuff: | |
63 | .scrapy | |
64 | ||
65 | # Sphinx documentation | |
66 | docs/_build/ | |
67 | README.html | |
68 | ||
69 | # PyBuilder | |
70 | target/ | |
71 | ||
72 | # Jupyter Notebook | |
73 | .ipynb_checkpoints | |
74 | ||
75 | # pyenv | |
76 | .python-version | |
77 | ||
78 | # celery beat schedule file | |
79 | celerybeat-schedule | |
80 | ||
81 | # SageMath parsed files | |
82 | *.sage.py | |
83 | ||
84 | # Environments | |
85 | .env | |
86 | .venv | |
87 | env/ | |
88 | venv/ | |
89 | ENV/ | |
90 | env.bak/ | |
91 | venv.bak/ | |
92 | ||
93 | # Spyder project settings | |
94 | .spyderproject | |
95 | .spyproject | |
96 | ||
97 | # Rope project settings | |
98 | .ropeproject | |
99 | ||
100 | # mkdocs documentation | |
101 | /site | |
102 | ||
103 | # mypy | |
104 | .mypy_cache/ |
0 | repos: | |
1 | - repo: https://github.com/asottile/pyupgrade | |
2 | rev: v2.7.3 | |
3 | hooks: | |
4 | - id: pyupgrade | |
5 | args: ["--py36-plus"] | |
6 | - repo: https://github.com/psf/black | |
7 | rev: 20.8b1 | |
8 | hooks: | |
9 | - id: black | |
10 | - repo: https://gitlab.com/pycqa/flake8 | |
11 | rev: 3.8.4 | |
12 | hooks: | |
13 | - id: flake8 | |
14 | additional_dependencies: [flake8-bugbear==20.1.0] | |
15 | - repo: https://github.com/asottile/blacken-docs | |
16 | rev: v1.8.0 | |
17 | hooks: | |
18 | - id: blacken-docs | |
19 | additional_dependencies: [black==20.8b1] | |
20 | args: ["--target-version", "py35"] | |
21 | - repo: https://github.com/pre-commit/mirrors-mypy | |
22 | rev: v0.790 | |
23 | hooks: | |
24 | - id: mypy | |
25 | language_version: python3 | |
26 | files: ^src/webargs/ |
0 | version: 2 | |
1 | sphinx: | |
2 | configuration: docs/conf.py | |
3 | formats: all | |
4 | python: | |
5 | version: 3.8 | |
6 | install: | |
7 | - method: pip | |
8 | path: . | |
9 | extra_requirements: | |
10 | - docs |
50 | 50 | * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_ |
51 | 51 | * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_ |
52 | 52 | * Jason Williams `@jaswilli <https://github.com/jaswilli>`_ |
53 | * Grey Li `@greyli <https://github.com/greyli>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | ||
3 | 8.0.0 (Unreleased) | |
4 | ****************** | |
5 | ||
6 | Features: | |
7 | ||
8 | * Add `Parser.pre_load` as a method for allowing users to modify data before | |
9 | schema loading, but without redefining location loaders. See advanced docs on | |
10 | `Parser pre_load` for usage information | |
11 | ||
12 | * ``unknown`` defaults to `None` for body locations (`json`, `form` and | |
13 | `json_or_form`) (:issue:`580`). | |
14 | ||
15 | * Detection of fields as "multi-value" for unpacking lists from multi-dict | |
16 | types is now extensible with the ``is_multiple`` attribute. If a field sets | |
17 | ``is_multiple = True`` it will be detected as a multi-value field. | |
18 | (:issue:`563`) | |
19 | ||
20 | * If ``is_multiple`` is not set or is set to ``None``, webargs will check if the | |
21 | field is an instance of ``List`` or ``Tuple``. | |
22 | ||
23 | * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be | |
24 | used to set fields which should be detected as ``is_multiple=True`` even when | |
25 | the attribute is not set. | |
26 | ||
27 | See docs on "Multi-Field Detection" for more details. | |
2 | 28 | |
3 | 29 | 7.0.1 (2020-12-14) |
4 | 30 | ****************** |
0 | For the code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html |
0 | webargs includes some code from third-party libraries. | |
1 | ||
2 | ||
3 | Flask-Restful License | |
4 | ===================== | |
5 | ||
6 | Copyright (c) 2013, Twilio, Inc. | |
7 | All rights reserved. | |
8 | ||
9 | Redistribution and use in source and binary forms, with or without | |
10 | modification, are permitted provided that the following conditions are met: | |
11 | ||
12 | - Redistributions of source code must retain the above copyright notice, this | |
13 | list of conditions and the following disclaimer. | |
14 | - Redistributions in binary form must reproduce the above copyright notice, | |
15 | this list of conditions and the following disclaimer in the documentation | |
16 | and/or other materials provided with the distribution. | |
17 | - Neither the name of the Twilio, Inc. nor the names of its contributors may be | |
18 | used to endorse or promote products derived from this software without | |
19 | specific prior written permission. | |
20 | ||
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
31 | ||
32 | Werkzeug License | |
33 | ================ | |
34 | ||
35 | Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details. | |
36 | ||
37 | Redistribution and use in source and binary forms, with or without | |
38 | modification, are permitted provided that the following conditions are | |
39 | met: | |
40 | ||
41 | * Redistributions of source code must retain the above copyright | |
42 | notice, this list of conditions and the following disclaimer. | |
43 | ||
44 | * Redistributions in binary form must reproduce the above | |
45 | copyright notice, this list of conditions and the following | |
46 | disclaimer in the documentation and/or other materials provided | |
47 | with the distribution. | |
48 | ||
49 | * The names of the contributors may not be used to endorse or | |
50 | promote products derived from this software without specific | |
51 | prior written permission. | |
52 | ||
53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
54 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
55 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
56 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
57 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
58 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
59 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
60 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
61 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
62 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
63 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
0 | Metadata-Version: 2.1 | |
1 | Name: webargs | |
2 | Version: 7.0.1 | |
3 | Summary: Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. | |
4 | Home-page: https://github.com/marshmallow-code/webargs | |
5 | Author: Steven Loria | |
6 | Author-email: [email protected] | |
7 | License: MIT | |
8 | Project-URL: Changelog, https://webargs.readthedocs.io/en/latest/changelog.html | |
9 | Project-URL: Issues, https://github.com/marshmallow-code/webargs/issues | |
10 | Project-URL: Funding, https://opencollective.com/marshmallow | |
11 | Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-marshmallow&utm_medium=pypi | |
12 | Description: ******* | |
13 | webargs | |
14 | ******* | |
15 | ||
16 | .. image:: https://badgen.net/pypi/v/webargs | |
17 | :target: https://pypi.org/project/webargs/ | |
18 | :alt: PyPI version | |
19 | ||
20 | .. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.webargs?branchName=dev | |
21 | :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=6&branchName=dev | |
22 | :alt: Build status | |
23 | ||
24 | .. image:: https://readthedocs.org/projects/webargs/badge/ | |
25 | :target: https://webargs.readthedocs.io/ | |
26 | :alt: Documentation | |
27 | ||
28 | .. image:: https://badgen.net/badge/marshmallow/3 | |
29 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html | |
30 | :alt: marshmallow 3 compatible | |
31 | ||
32 | .. image:: https://badgen.net/badge/code%20style/black/000 | |
33 | :target: https://github.com/ambv/black | |
34 | :alt: code style: black | |
35 | ||
36 | Homepage: https://webargs.readthedocs.io/ | |
37 | ||
38 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. | |
39 | ||
40 | .. code-block:: python | |
41 | ||
42 | from flask import Flask | |
43 | from webargs import fields | |
44 | from webargs.flaskparser import use_args | |
45 | ||
46 | app = Flask(__name__) | |
47 | ||
48 | ||
49 | @app.route("/") | |
50 | @use_args({"name": fields.Str(required=True)}, location="query") | |
51 | def index(args): | |
52 | return "Hello " + args["name"] | |
53 | ||
54 | ||
55 | if __name__ == "__main__": | |
56 | app.run() | |
57 | ||
58 | # curl http://localhost:5000/\?name\='World' | |
59 | # Hello World | |
60 | ||
61 | Install | |
62 | ======= | |
63 | ||
64 | :: | |
65 | ||
66 | pip install -U webargs | |
67 | ||
68 | webargs supports Python >= 3.6. | |
69 | ||
70 | ||
71 | Documentation | |
72 | ============= | |
73 | ||
74 | Full documentation is available at https://webargs.readthedocs.io/. | |
75 | ||
76 | Support webargs | |
77 | =============== | |
78 | ||
79 | webargs is maintained by a group of | |
80 | `volunteers <https://webargs.readthedocs.io/en/latest/authors.html>`_. | |
81 | If you'd like to support the future of the project, please consider | |
82 | contributing to our Open Collective: | |
83 | ||
84 | .. image:: https://opencollective.com/marshmallow/donate/button.png | |
85 | :target: https://opencollective.com/marshmallow | |
86 | :width: 200 | |
87 | :alt: Donate to our collective | |
88 | ||
89 | Professional Support | |
90 | ==================== | |
91 | ||
92 | Professionally-supported webargs is available through the | |
93 | `Tidelift Subscription <https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme>`_. | |
94 | ||
95 | Tidelift gives software development teams a single source for purchasing and maintaining their software, | |
96 | with professional-grade assurances from the experts who know it best, | |
97 | while seamlessly integrating with existing tools. [`Get professional support`_] | |
98 | ||
99 | .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme | |
100 | ||
101 | .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png | |
102 | :target: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme | |
103 | :alt: Get supported marshmallow with Tidelift | |
104 | ||
105 | Security Contact Information | |
106 | ============================ | |
107 | ||
108 | To report a security vulnerability, please use the | |
109 | `Tidelift security contact <https://tidelift.com/security>`_. | |
110 | Tidelift will coordinate the fix and disclosure. | |
111 | ||
112 | Project Links | |
113 | ============= | |
114 | ||
115 | - Docs: https://webargs.readthedocs.io/ | |
116 | - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html | |
117 | - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html | |
118 | - PyPI: https://pypi.python.org/pypi/webargs | |
119 | - Issues: https://github.com/marshmallow-code/webargs/issues | |
120 | ||
121 | ||
122 | License | |
123 | ======= | |
124 | ||
125 | MIT licensed. See the `LICENSE <https://github.com/marshmallow-code/webargs/blob/dev/LICENSE>`_ file for more details. | |
126 | ||
127 | Keywords: webargs,http,flask,django,bottle,tornado,aiohttp,request,arguments,validation,parameters,rest,api,marshmallow | |
128 | Platform: UNKNOWN | |
129 | Classifier: Development Status :: 5 - Production/Stable | |
130 | Classifier: Intended Audience :: Developers | |
131 | Classifier: License :: OSI Approved :: MIT License | |
132 | Classifier: Natural Language :: English | |
133 | Classifier: Programming Language :: Python :: 3 | |
134 | Classifier: Programming Language :: Python :: 3.6 | |
135 | Classifier: Programming Language :: Python :: 3.7 | |
136 | Classifier: Programming Language :: Python :: 3.8 | |
137 | Classifier: Programming Language :: Python :: 3.9 | |
138 | Classifier: Programming Language :: Python :: 3 :: Only | |
139 | Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content | |
140 | Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application | |
141 | Requires-Python: >=3.6 | |
142 | Provides-Extra: dev | |
143 | Provides-Extra: docs | |
144 | Provides-Extra: frameworks | |
145 | Provides-Extra: lint | |
146 | Provides-Extra: tests |
0 | trigger: | |
1 | branches: | |
2 | include: [dev, test-me-*] | |
3 | tags: | |
4 | include: ['*'] | |
5 | ||
6 | # Run builds nightly to catch incompatibilities with new marshmallow releases | |
7 | schedules: | |
8 | - cron: "0 0 * * *" | |
9 | displayName: Daily midnight build | |
10 | branches: | |
11 | include: | |
12 | - dev | |
13 | always: "true" | |
14 | ||
15 | resources: | |
16 | repositories: | |
17 | - repository: sloria | |
18 | type: github | |
19 | endpoint: github | |
20 | name: sloria/azure-pipeline-templates | |
21 | ref: refs/heads/sloria | |
22 | ||
23 | jobs: | |
24 | - template: job--python-tox.yml@sloria | |
25 | parameters: | |
26 | toxenvs: | |
27 | - lint | |
28 | - mypy | |
29 | - py36 | |
30 | - py36-mindeps | |
31 | - py37 | |
32 | - py38 | |
33 | - py39 | |
34 | - py39-marshmallowdev | |
35 | - docs | |
36 | os: linux | |
37 | # Build wheels | |
38 | - template: job--pypi-release.yml@sloria | |
39 | parameters: | |
40 | python: "3.9" | |
41 | distributions: "sdist bdist_wheel" | |
42 | dependsOn: | |
43 | - tox_linux |
0 | # Makefile for Sphinx documentation | |
1 | # | |
2 | ||
3 | # You can set these variables from the command line. | |
4 | SPHINXOPTS = | |
5 | SPHINXBUILD = sphinx-build | |
6 | PAPER = | |
7 | BUILDDIR = _build | |
8 | ||
9 | # User-friendly check for sphinx-build | |
10 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) | |
11 | $(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/) | |
12 | endif | |
13 | ||
14 | # Internal variables. | |
15 | PAPEROPT_a4 = -D latex_paper_size=a4 | |
16 | PAPEROPT_letter = -D latex_paper_size=letter | |
17 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
18 | # the i18n builder cannot share the environment and doctrees with the others | |
19 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
20 | ||
21 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext | |
22 | ||
23 | help: | |
24 | @echo "Please use \`make <target>' where <target> is one of" | |
25 | @echo " html to make standalone HTML files" | |
26 | @echo " dirhtml to make HTML files named index.html in directories" | |
27 | @echo " singlehtml to make a single large HTML file" | |
28 | @echo " pickle to make pickle files" | |
29 | @echo " json to make JSON files" | |
30 | @echo " htmlhelp to make HTML files and a HTML help project" | |
31 | @echo " qthelp to make HTML files and a qthelp project" | |
32 | @echo " devhelp to make HTML files and a Devhelp project" | |
33 | @echo " epub to make an epub" | |
34 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | |
35 | @echo " latexpdf to make LaTeX files and run them through pdflatex" | |
36 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | |
37 | @echo " text to make text files" | |
38 | @echo " man to make manual pages" | |
39 | @echo " texinfo to make Texinfo files" | |
40 | @echo " info to make Texinfo files and run them through makeinfo" | |
41 | @echo " gettext to make PO message catalogs" | |
42 | @echo " changes to make an overview of all changed/added/deprecated items" | |
43 | @echo " xml to make Docutils-native XML files" | |
44 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" | |
45 | @echo " linkcheck to check all external links for integrity" | |
46 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" | |
47 | ||
48 | clean: | |
49 | rm -rf $(BUILDDIR)/* | |
50 | ||
51 | html: | |
52 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | |
53 | @echo | |
54 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | |
55 | ||
56 | dirhtml: | |
57 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | |
58 | @echo | |
59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | |
60 | ||
61 | singlehtml: | |
62 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | |
63 | @echo | |
64 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | |
65 | ||
66 | pickle: | |
67 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | |
68 | @echo | |
69 | @echo "Build finished; now you can process the pickle files." | |
70 | ||
71 | json: | |
72 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | |
73 | @echo | |
74 | @echo "Build finished; now you can process the JSON files." | |
75 | ||
76 | htmlhelp: | |
77 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | |
78 | @echo | |
79 | @echo "Build finished; now you can run HTML Help Workshop with the" \ | |
80 | ".hhp project file in $(BUILDDIR)/htmlhelp." | |
81 | ||
82 | qthelp: | |
83 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | |
84 | @echo | |
85 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ | |
86 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | |
87 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" | |
88 | @echo "To view the help file:" | |
89 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" | |
90 | ||
91 | devhelp: | |
92 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | |
93 | @echo | |
94 | @echo "Build finished." | |
95 | @echo "To view the help file:" | |
96 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" | |
97 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" | |
98 | @echo "# devhelp" | |
99 | ||
100 | epub: | |
101 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | |
102 | @echo | |
103 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." | |
104 | ||
105 | latex: | |
106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
107 | @echo | |
108 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | |
109 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ | |
110 | "(use \`make latexpdf' here to do that automatically)." | |
111 | ||
112 | latexpdf: | |
113 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
114 | @echo "Running LaTeX files through pdflatex..." | |
115 | $(MAKE) -C $(BUILDDIR)/latex all-pdf | |
116 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
117 | ||
118 | latexpdfja: | |
119 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
120 | @echo "Running LaTeX files through platex and dvipdfmx..." | |
121 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | |
122 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
123 | ||
124 | text: | |
125 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | |
126 | @echo | |
127 | @echo "Build finished. The text files are in $(BUILDDIR)/text." | |
128 | ||
129 | man: | |
130 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | |
131 | @echo | |
132 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." | |
133 | ||
134 | texinfo: | |
135 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
136 | @echo | |
137 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | |
138 | @echo "Run \`make' in that directory to run these through makeinfo" \ | |
139 | "(use \`make info' here to do that automatically)." | |
140 | ||
141 | info: | |
142 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
143 | @echo "Running Texinfo files through makeinfo..." | |
144 | make -C $(BUILDDIR)/texinfo info | |
145 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | |
146 | ||
147 | gettext: | |
148 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | |
149 | @echo | |
150 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | |
151 | ||
152 | changes: | |
153 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | |
154 | @echo | |
155 | @echo "The overview file is in $(BUILDDIR)/changes." | |
156 | ||
157 | linkcheck: | |
158 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | |
159 | @echo | |
160 | @echo "Link check complete; look for any errors in the above output " \ | |
161 | "or in $(BUILDDIR)/linkcheck/output.txt." | |
162 | ||
163 | doctest: | |
164 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | |
165 | @echo "Testing of doctests in the sources finished, look at the " \ | |
166 | "results in $(BUILDDIR)/doctest/output.txt." | |
167 | ||
168 | xml: | |
169 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | |
170 | @echo | |
171 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." | |
172 | ||
173 | pseudoxml: | |
174 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | |
175 | @echo | |
176 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."⏎ |
0 | {% if donate_url %} | |
1 | <div class="globaltoc"> | |
2 | <ul> | |
3 | <li class="toctree-l1"> | |
4 | <a href="{{ donate_url }}">Donate</a> | |
5 | </li> | |
6 | </ul> | |
7 | </div> | |
8 | {% endif %} |
0 | <div class="sponsors"> | |
1 | {% if tidelift_url %} | |
2 | <div class="sponsor"> | |
3 | <a class="image" href="{{ tidelift_url }}"> | |
4 | <img src="https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png" alt="Sponsor"></a> | |
5 | <div class="text">Professionally-supported {{ project }} is available with the | |
6 | <a href="{{ tidelift_url }}">Tidelift Subscription</a>. | |
7 | </div> | |
8 | </div> | |
9 | {% endif %} | |
10 | </div> |
0 | Advanced Usage | |
1 | ============== | |
2 | ||
3 | This section includes guides for advanced usage patterns. | |
4 | ||
5 | Custom Location Handlers | |
6 | ------------------------ | |
7 | ||
8 | To add your own custom location handler, write a function that receives a request, and a :class:`Schema <marshmallow.Schema>`, then decorate that function with :func:`Parser.location_loader <webargs.core.Parser.location_loader>`. | |
9 | ||
10 | ||
11 | .. code-block:: python | |
12 | ||
13 | from webargs import fields | |
14 | from webargs.flaskparser import parser | |
15 | ||
16 | ||
17 | @parser.location_loader("data") | |
18 | def load_data(request, schema): | |
19 | return request.data | |
20 | ||
21 | ||
22 | # Now 'data' can be specified as a location | |
23 | @parser.use_args({"per_page": fields.Int()}, location="data") | |
24 | def posts(args): | |
25 | return "displaying {} posts".format(args["per_page"]) | |
26 | ||
27 | ||
28 | .. NOTE:: | |
29 | ||
30 | The schema is passed so that it can be used to wrap multidict types and | |
31 | unpack List fields correctly. If you are writing a loader for a multidict | |
32 | type, consider looking at | |
33 | :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` for an | |
34 | example of how to do this. | |
35 | ||
36 | "meta" Locations | |
37 | ~~~~~~~~~~~~~~~~ | |
38 | ||
39 | You can define your own locations which mix data from several existing | |
40 | locations. | |
41 | ||
42 | The `json_or_form` location does this -- first trying to load data as JSON and | |
43 | then falling back to a form body -- and its implementation is quite simple: | |
44 | ||
45 | ||
46 | .. code-block:: python | |
47 | ||
48 | def load_json_or_form(self, req, schema): | |
49 | """Load data from a request, accepting either JSON or form-encoded | |
50 | data. | |
51 | ||
52 | The data will first be loaded as JSON, and, if that fails, it will be | |
53 | loaded as a form post. | |
54 | """ | |
55 | data = self.load_json(req, schema) | |
56 | if data is not missing: | |
57 | return data | |
58 | return self.load_form(req, schema) | |
59 | ||
60 | ||
61 | You can imagine your own locations with custom behaviors like this. | |
62 | For example, to mix query parameters and form body data, you might write the | |
63 | following: | |
64 | ||
65 | .. code-block:: python | |
66 | ||
67 | from webargs import fields | |
68 | from webargs.multidictproxy import MultiDictProxy | |
69 | from webargs.flaskparser import parser | |
70 | ||
71 | ||
72 | @parser.location_loader("query_and_form") | |
73 | def load_data(request, schema): | |
74 | # relies on the Flask (werkzeug) MultiDict type's implementation of | |
75 | # these methods, but when you're extending webargs, you may know things | |
76 | # about your framework of choice | |
77 | newdata = request.args.copy() | |
78 | newdata.update(request.form) | |
79 | return MultiDictProxy(newdata, schema) | |
80 | ||
81 | ||
82 | # Now 'query_and_form' means you can send these values in either location, | |
83 | # and they will be *mixed* together into a new dict to pass to your schema | |
84 | @parser.use_args({"favorite_food": fields.String()}, location="query_and_form") | |
85 | def set_favorite_food(args): | |
86 | ... # do stuff | |
87 | return "your favorite food is now set to {}".format(args["favorite_food"]) | |
88 | ||
89 | marshmallow Integration | |
90 | ----------------------- | |
91 | ||
92 | When you need more flexibility in defining input schemas, you can pass a marshmallow `Schema <marshmallow.Schema>` instead of a dictionary to `Parser.parse <webargs.core.Parser.parse>`, `Parser.use_args <webargs.core.Parser.use_args>`, and `Parser.use_kwargs <webargs.core.Parser.use_kwargs>`. | |
93 | ||
94 | ||
95 | .. code-block:: python | |
96 | ||
97 | from marshmallow import Schema, fields | |
98 | from webargs.flaskparser import use_args | |
99 | ||
100 | ||
101 | class UserSchema(Schema): | |
102 | id = fields.Int(dump_only=True) # read-only (won't be parsed by webargs) | |
103 | username = fields.Str(required=True) | |
104 | password = fields.Str(load_only=True) # write-only | |
105 | first_name = fields.Str(missing="") | |
106 | last_name = fields.Str(missing="") | |
107 | date_registered = fields.DateTime(dump_only=True) | |
108 | ||
109 | ||
110 | @use_args(UserSchema()) | |
111 | def profile_view(args): | |
112 | username = args["userame"] | |
113 | # ... | |
114 | ||
115 | ||
116 | @use_kwargs(UserSchema()) | |
117 | def profile_update(username, password, first_name, last_name): | |
118 | update_profile(username, password, first_name, last_name) | |
119 | # ... | |
120 | ||
121 | ||
122 | # You can add additional parameters | |
123 | @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query") | |
124 | @use_args(UserSchema()) | |
125 | def profile_posts(args, posts_per_page): | |
126 | username = args["username"] | |
127 | # ... | |
128 | ||
129 | .. _advanced_setting_unknown: | |
130 | ||
131 | Setting `unknown` | |
132 | ----------------- | |
133 | ||
134 | webargs supports several ways of setting and passing the `unknown` parameter | |
135 | for `handling unknown fields <https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields>`_. | |
136 | ||
137 | You can pass `unknown=...` as a parameter to any of | |
138 | `Parser.parse <webargs.core.Parser.parse>`, | |
139 | `Parser.use_args <webargs.core.Parser.use_args>`, and | |
140 | `Parser.use_kwargs <webargs.core.Parser.use_kwargs>`. | |
141 | ||
142 | ||
143 | .. note:: | |
144 | ||
145 | The `unknown` value is passed to the schema's `load()` call. It therefore | |
146 | only applies to the top layer when nesting is used. To control `unknown` at | |
147 | multiple layers of a nested schema, you must use other mechanisms, like | |
148 | the `unknown` argument to `fields.Nested`. | |
149 | ||
150 | Default `unknown` | |
151 | +++++++++++++++++ | |
152 | ||
153 | By default, webargs will pass `unknown=marshmallow.EXCLUDE` except when the | |
154 | location is `json`, `form`, `json_or_form`, `path`, or `path`. In those cases, | |
155 | it uses `unknown=marshmallow.RAISE` instead. | |
156 | ||
157 | You can change these defaults by overriding `DEFAULT_UNKNOWN_BY_LOCATION`. | |
158 | This is a mapping of locations to values to pass. | |
159 | ||
160 | For example, | |
161 | ||
162 | .. code-block:: python | |
163 | ||
164 | from flask import Flask | |
165 | from marshmallow import EXCLUDE, fields | |
166 | from webargs.flaskparser import FlaskParser | |
167 | ||
168 | app = Flask(__name__) | |
169 | ||
170 | ||
171 | class Parser(FlaskParser): | |
172 | DEFAULT_UNKNOWN_BY_LOCATION = {"query": EXCLUDE} | |
173 | ||
174 | ||
175 | parser = Parser() | |
176 | ||
177 | ||
178 | # location is "query", which is listed in DEFAULT_UNKNOWN_BY_LOCATION, | |
179 | # so EXCLUDE will be used | |
180 | @app.route("/", methods=["GET"]) | |
181 | @parser.use_args({"foo": fields.Int()}, location="query") | |
182 | def get(self, args): | |
183 | return f"foo x 2 = {args['foo'] * 2}" | |
184 | ||
185 | ||
186 | # location is "json", which is not in DEFAULT_UNKNOWN_BY_LOCATION, | |
187 | # so no value will be passed for `unknown` | |
188 | @app.route("/", methods=["POST"]) | |
189 | @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") | |
190 | def post(self, args): | |
191 | return f"foo x bar = {args['foo'] * args['bar']}" | |
192 | ||
193 | ||
194 | You can also define a default at parser instantiation, which will take | |
195 | precedence over these defaults, as in | |
196 | ||
197 | .. code-block:: python | |
198 | ||
199 | from marshmallow import INCLUDE | |
200 | ||
201 | parser = Parser(unknown=INCLUDE) | |
202 | ||
203 | # because `unknown` is set on the parser, `DEFAULT_UNKNOWN_BY_LOCATION` has | |
204 | # effect and `INCLUDE` will always be used | |
205 | @app.route("/", methods=["POST"]) | |
206 | @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") | |
207 | def post(self, args): | |
208 | unexpected_args = [k for k in args.keys() if k not in ("foo", "bar")] | |
209 | return f"foo x bar = {args['foo'] * args['bar']}; unexpected args={unexpected_args}" | |
210 | ||
211 | Using Schema-Specfied `unknown` | |
212 | +++++++++++++++++++++++++++++++ | |
213 | ||
214 | If you wish to use the value of `unknown` specified by a schema, simply pass | |
215 | ``unknown=None``. This will disable webargs' automatic passing of values for | |
216 | ``unknown``. For example, | |
217 | ||
218 | .. code-block:: python | |
219 | ||
220 | from flask import Flask | |
221 | from marshmallow import Schema, fields, EXCLUDE, missing | |
222 | from webargs.flaskparser import use_args | |
223 | ||
224 | ||
225 | class RectangleSchema(Schema): | |
226 | length = fields.Float() | |
227 | width = fields.Float() | |
228 | ||
229 | class Meta: | |
230 | unknown = EXCLUDE | |
231 | ||
232 | ||
233 | app = Flask(__name__) | |
234 | ||
235 | # because unknown=None was passed, no value is passed during schema loading | |
236 | # as a result, the schema's behavior (EXCLUDE) is used | |
237 | @app.route("/", methods=["POST"]) | |
238 | @use_args(RectangleSchema(), location="json", unknown=None) | |
239 | def get(self, args): | |
240 | return f"area = {args['length'] * args['width']}" | |
241 | ||
242 | ||
243 | You can also set ``unknown=None`` when instantiating a parser to make this | |
244 | behavior the default for a parser. | |
245 | ||
246 | ||
247 | When to avoid `use_kwargs` | |
248 | -------------------------- | |
249 | ||
250 | Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. | |
251 | If your schema has a `post_load <marshmallow.decorators.post_load>` method | |
252 | that returns a non-dictionary, | |
253 | you should use `use_args <webargs.core.Parser.use_args>` instead. | |
254 | ||
255 | .. code-block:: python | |
256 | ||
257 | from marshmallow import Schema, fields, post_load | |
258 | from webargs.flaskparser import use_args | |
259 | ||
260 | ||
261 | class Rectangle: | |
262 | def __init__(self, length, width): | |
263 | self.length = length | |
264 | self.width = width | |
265 | ||
266 | ||
267 | class RectangleSchema(Schema): | |
268 | length = fields.Float() | |
269 | width = fields.Float() | |
270 | ||
271 | @post_load | |
272 | def make_object(self, data, **kwargs): | |
273 | return Rectangle(**data) | |
274 | ||
275 | ||
276 | @use_args(RectangleSchema) | |
277 | def post(self, rect: Rectangle): | |
278 | return f"Area: {rect.length * rect.width}" | |
279 | ||
280 | Packages such as `marshmallow-sqlalchemy <https://github.com/marshmallow-code/marshmallow-sqlalchemy>`_ and `marshmallow-dataclass <https://github.com/lovasoa/marshmallow_dataclass>`_ generate schemas that deserialize to non-dictionary objects. | |
281 | Therefore, `use_args <webargs.core.Parser.use_args>` should be used with those schemas. | |
282 | ||
283 | ||
284 | Schema Factories | |
285 | ---------------- | |
286 | ||
287 | If you need to parametrize a schema based on a given request, you can use a "Schema factory": a callable that receives the current `request` and returns a `marshmallow.Schema` instance. | |
288 | ||
289 | Consider the following use cases: | |
290 | ||
291 | - Filtering via a query parameter by passing ``only`` to the Schema. | |
292 | - Handle partial updates for PATCH requests using marshmallow's `partial loading <https://marshmallow.readthedocs.io/en/latest/quickstart.html#partial-loading>`_ API. | |
293 | ||
294 | .. code-block:: python | |
295 | ||
296 | from flask import Flask | |
297 | from marshmallow import Schema, fields | |
298 | from webargs.flaskparser import use_args | |
299 | ||
300 | app = Flask(__name__) | |
301 | ||
302 | ||
303 | class UserSchema(Schema): | |
304 | id = fields.Int(dump_only=True) | |
305 | username = fields.Str(required=True) | |
306 | password = fields.Str(load_only=True) | |
307 | first_name = fields.Str(missing="") | |
308 | last_name = fields.Str(missing="") | |
309 | date_registered = fields.DateTime(dump_only=True) | |
310 | ||
311 | ||
312 | def make_user_schema(request): | |
313 | # Filter based on 'fields' query parameter | |
314 | fields = request.args.get("fields", None) | |
315 | only = fields.split(",") if fields else None | |
316 | # Respect partial updates for PATCH requests | |
317 | partial = request.method == "PATCH" | |
318 | # Add current request to the schema's context | |
319 | return UserSchema(only=only, partial=partial, context={"request": request}) | |
320 | ||
321 | ||
322 | # Pass the factory to .parse, .use_args, or .use_kwargs | |
323 | @app.route("/profile/", methods=["GET", "POST", "PATCH"]) | |
324 | @use_args(make_user_schema) | |
325 | def profile_view(args): | |
326 | username = args.get("username") | |
327 | # ... | |
328 | ||
329 | ||
330 | ||
331 | Reducing Boilerplate | |
332 | ++++++++++++++++++++ | |
333 | ||
334 | We can reduce boilerplate and improve [re]usability with a simple helper function: | |
335 | ||
336 | .. code-block:: python | |
337 | ||
338 | from webargs.flaskparser import use_args | |
339 | ||
340 | ||
341 | def use_args_with(schema_cls, schema_kwargs=None, **kwargs): | |
342 | schema_kwargs = schema_kwargs or {} | |
343 | ||
344 | def factory(request): | |
345 | # Filter based on 'fields' query parameter | |
346 | only = request.args.get("fields", None) | |
347 | # Respect partial updates for PATCH requests | |
348 | partial = request.method == "PATCH" | |
349 | return schema_cls( | |
350 | only=only, partial=partial, context={"request": request}, **schema_kwargs | |
351 | ) | |
352 | ||
353 | return use_args(factory, **kwargs) | |
354 | ||
355 | ||
356 | Now we can attach input schemas to our view functions like so: | |
357 | ||
358 | .. code-block:: python | |
359 | ||
360 | @use_args_with(UserSchema) | |
361 | def profile_view(args): | |
362 | # ... | |
363 | get_profile(**args) | |
364 | ||
365 | ||
366 | Custom Fields | |
367 | ------------- | |
368 | ||
369 | See the "Custom Fields" section of the marshmallow docs for a detailed guide on defining custom fields which you can pass to webargs parsers: https://marshmallow.readthedocs.io/en/latest/custom_fields.html. | |
370 | ||
371 | Using ``Method`` and ``Function`` Fields with webargs | |
372 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
373 | ||
374 | Using the :class:`Method <marshmallow.fields.Method>` and :class:`Function <marshmallow.fields.Function>` fields requires that you pass the ``deserialize`` parameter. | |
375 | ||
376 | ||
377 | .. code-block:: python | |
378 | ||
379 | @use_args({"cube": fields.Function(deserialize=lambda x: int(x) ** 3)}) | |
380 | def math_view(args): | |
381 | cube = args["cube"] | |
382 | # ... | |
383 | ||
384 | .. _custom-loaders: | |
385 | ||
386 | Custom Parsers | |
387 | -------------- | |
388 | ||
389 | To add your own parser, extend :class:`Parser <webargs.core.Parser>` and implement the `load_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. | |
390 | ||
391 | ||
392 | .. code-block:: python | |
393 | ||
394 | import re | |
395 | ||
396 | from webargs import core | |
397 | from webargs.flaskparser import FlaskParser | |
398 | ||
399 | ||
400 | class NestedQueryFlaskParser(FlaskParser): | |
401 | """Parses nested query args | |
402 | ||
403 | This parser handles nested query args. It expects nested levels | |
404 | delimited by a period and then deserializes the query args into a | |
405 | nested dict. | |
406 | ||
407 | For example, the URL query params `?name.first=John&name.last=Boone` | |
408 | will yield the following dict: | |
409 | ||
410 | { | |
411 | 'name': { | |
412 | 'first': 'John', | |
413 | 'last': 'Boone', | |
414 | } | |
415 | } | |
416 | """ | |
417 | ||
418 | def load_querystring(self, req, schema): | |
419 | return _structure_dict(req.args) | |
420 | ||
421 | ||
422 | def _structure_dict(dict_): | |
423 | def structure_dict_pair(r, key, value): | |
424 | m = re.match(r"(\w+)\.(.*)", key) | |
425 | if m: | |
426 | if r.get(m.group(1)) is None: | |
427 | r[m.group(1)] = {} | |
428 | structure_dict_pair(r[m.group(1)], m.group(2), value) | |
429 | else: | |
430 | r[key] = value | |
431 | ||
432 | r = {} | |
433 | for k, v in dict_.items(): | |
434 | structure_dict_pair(r, k, v) | |
435 | return r | |
436 | ||
437 | Returning HTTP 400 Responses | |
438 | ---------------------------- | |
439 | ||
440 | If you'd prefer validation errors to return status code ``400`` instead | |
441 | of ``422``, you can override ``DEFAULT_VALIDATION_STATUS`` on a :class:`Parser <webargs.core.Parser>`. | |
442 | ||
443 | Sublcass the parser for your framework to do so. For example, using Falcon: | |
444 | ||
445 | .. code-block:: python | |
446 | ||
447 | from webargs.falconparser import FalconParser | |
448 | ||
449 | ||
450 | class Parser(FalconParser): | |
451 | DEFAULT_VALIDATION_STATUS = 400 | |
452 | ||
453 | ||
454 | parser = Parser() | |
455 | use_args = parser.use_args | |
456 | use_kwargs = parser.use_kwargs | |
457 | ||
458 | Bulk-type Arguments | |
459 | ------------------- | |
460 | ||
461 | In order to parse a JSON array of objects, pass ``many=True`` to your input ``Schema`` . | |
462 | ||
463 | For example, you might implement JSON PATCH according to `RFC 6902 <https://tools.ietf.org/html/rfc6902>`_ like so: | |
464 | ||
465 | ||
466 | .. code-block:: python | |
467 | ||
468 | from webargs import fields | |
469 | from webargs.flaskparser import use_args | |
470 | from marshmallow import Schema, validate | |
471 | ||
472 | ||
473 | class PatchSchema(Schema): | |
474 | op = fields.Str( | |
475 | required=True, | |
476 | validate=validate.OneOf(["add", "remove", "replace", "move", "copy"]), | |
477 | ) | |
478 | path = fields.Str(required=True) | |
479 | value = fields.Str(required=True) | |
480 | ||
481 | ||
482 | @app.route("/profile/", methods=["patch"]) | |
483 | @use_args(PatchSchema(many=True)) | |
484 | def patch_blog(args): | |
485 | """Implements JSON Patch for the user profile | |
486 | ||
487 | Example JSON body: | |
488 | ||
489 | [ | |
490 | {"op": "replace", "path": "/email", "value": "[email protected]"} | |
491 | ] | |
492 | """ | |
493 | # ... | |
494 | ||
495 | Mixing Locations | |
496 | ---------------- | |
497 | ||
498 | Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.use_args>` call: | |
499 | ||
500 | .. code-block:: python | |
501 | ||
502 | # "json" is the default, used explicitly below | |
503 | @app.route("/stacked", methods=["POST"]) | |
504 | @use_args({"page": fields.Int(), "q": fields.Str()}, location="query") | |
505 | @use_args({"name": fields.Str()}, location="json") | |
506 | def viewfunc(query_parsed, json_parsed): | |
507 | page = query_parsed["page"] | |
508 | name = json_parsed["name"] | |
509 | # ... | |
510 | ||
511 | To reduce boilerplate, you could create shortcuts, like so: | |
512 | ||
513 | .. code-block:: python | |
514 | ||
515 | import functools | |
516 | ||
517 | query = functools.partial(use_args, location="query") | |
518 | body = functools.partial(use_args, location="json") | |
519 | ||
520 | ||
521 | @query({"page": fields.Int(), "q": fields.Int()}) | |
522 | @body({"name": fields.Str()}) | |
523 | def viewfunc(query_parsed, json_parsed): | |
524 | page = query_parsed["page"] | |
525 | name = json_parsed["name"] | |
526 | # ... | |
527 | ||
528 | Next Steps | |
529 | ---------- | |
530 | ||
531 | - See the :doc:`Framework Support <framework_support>` page for framework-specific guides. | |
532 | - For example applications, check out the `examples <https://github.com/marshmallow-code/webargs/tree/dev/examples>`_ directory. |
0 | API | |
1 | === | |
2 | ||
3 | .. module:: webargs | |
4 | ||
5 | webargs.core | |
6 | ------------ | |
7 | ||
8 | .. automodule:: webargs.core | |
9 | :inherited-members: | |
10 | ||
11 | ||
12 | webargs.fields | |
13 | -------------- | |
14 | ||
15 | .. automodule:: webargs.fields | |
16 | :members: Nested, DelimitedList | |
17 | ||
18 | ||
19 | webargs.multidictproxy | |
20 | ---------------------- | |
21 | ||
22 | .. automodule:: webargs.multidictproxy | |
23 | :members: | |
24 | ||
25 | ||
26 | webargs.asyncparser | |
27 | ------------------- | |
28 | ||
29 | .. automodule:: webargs.asyncparser | |
30 | :inherited-members: | |
31 | ||
32 | webargs.flaskparser | |
33 | ------------------- | |
34 | ||
35 | .. automodule:: webargs.flaskparser | |
36 | :members: | |
37 | ||
38 | webargs.djangoparser | |
39 | -------------------- | |
40 | ||
41 | .. automodule:: webargs.djangoparser | |
42 | :members: | |
43 | ||
44 | webargs.bottleparser | |
45 | -------------------- | |
46 | ||
47 | .. automodule:: webargs.bottleparser | |
48 | :members: | |
49 | ||
50 | webargs.tornadoparser | |
51 | --------------------- | |
52 | ||
53 | .. automodule:: webargs.tornadoparser | |
54 | :members: | |
55 | ||
56 | webargs.pyramidparser | |
57 | --------------------- | |
58 | ||
59 | .. automodule:: webargs.pyramidparser | |
60 | :members: | |
61 | ||
62 | webargs.falconparser | |
63 | --------------------- | |
64 | ||
65 | .. automodule:: webargs.falconparser | |
66 | :members: | |
67 | ||
68 | webargs.aiohttpparser | |
69 | --------------------- | |
70 | ||
71 | .. automodule:: webargs.aiohttpparser | |
72 | :members: |
0 | import datetime as dt | |
1 | import sys | |
2 | import os | |
3 | import sphinx_typlog_theme | |
4 | ||
5 | # If extensions (or modules to document with autodoc) are in another directory, | |
6 | # add these directories to sys.path here. If the directory is relative to the | |
7 | # documentation root, use os.path.abspath to make it absolute, like shown here. | |
8 | sys.path.insert(0, os.path.abspath(os.path.join("..", "src"))) | |
9 | import webargs # noqa | |
10 | ||
11 | extensions = [ | |
12 | "sphinx.ext.autodoc", | |
13 | "sphinx.ext.viewcode", | |
14 | "sphinx.ext.intersphinx", | |
15 | "sphinx_issues", | |
16 | ] | |
17 | ||
18 | primary_domain = "py" | |
19 | default_role = "py:obj" | |
20 | ||
21 | github_user = "marshmallow-code" | |
22 | github_repo = "webargs" | |
23 | ||
24 | issues_github_path = f"{github_user}/{github_repo}" | |
25 | ||
26 | intersphinx_mapping = { | |
27 | "python": ("http://python.readthedocs.io/en/latest/", None), | |
28 | "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None), | |
29 | } | |
30 | ||
31 | # The master toctree document. | |
32 | master_doc = "index" | |
33 | ||
34 | language = "en" | |
35 | ||
36 | html_domain_indices = False | |
37 | source_suffix = ".rst" | |
38 | project = "webargs" | |
39 | copyright = f"2014-{dt.datetime.utcnow():%Y}, Steven Loria and contributors" | |
40 | version = release = webargs.__version__ | |
41 | templates_path = ["_templates"] | |
42 | exclude_patterns = ["_build"] | |
43 | ||
44 | # THEME | |
45 | ||
46 | # Add any paths that contain custom themes here, relative to this directory. | |
47 | html_theme = "sphinx_typlog_theme" | |
48 | html_theme_path = [sphinx_typlog_theme.get_path()] | |
49 | ||
50 | html_theme_options = { | |
51 | "color": "#268bd2", | |
52 | "logo_name": "webargs", | |
53 | "description": "Declarative parsing and validation of HTTP request objects.", | |
54 | "github_user": github_user, | |
55 | "github_repo": github_repo, | |
56 | } | |
57 | ||
58 | html_context = { | |
59 | "tidelift_url": ( | |
60 | "https://tidelift.com/subscription/pkg/pypi-webargs" | |
61 | "?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=docs" | |
62 | ), | |
63 | "donate_url": "https://opencollective.com/marshmallow", | |
64 | } | |
65 | ||
66 | html_sidebars = { | |
67 | "**": [ | |
68 | "logo.html", | |
69 | "github.html", | |
70 | "globaltoc.html", | |
71 | "donate.html", | |
72 | "searchbox.html", | |
73 | "sponsors.html", | |
74 | ] | |
75 | } |
0 | Ecosystem | |
1 | ========= | |
2 | ||
3 | A list of webargs-related libraries can be found at the GitHub wiki here: | |
4 | ||
5 | https://github.com/marshmallow-code/webargs/wiki/Ecosystem |
0 | .. _frameworks: | |
1 | ||
2 | Framework Support | |
3 | ================= | |
4 | ||
5 | This section includes notes for using webargs with specific web frameworks. | |
6 | ||
7 | Flask | |
8 | ----- | |
9 | ||
10 | Flask support is available via the :mod:`webargs.flaskparser` module. | |
11 | ||
12 | Decorator Usage | |
13 | +++++++++++++++ | |
14 | ||
15 | When using the :meth:`use_args <webargs.flaskparser.FlaskParser.use_args>` decorator, the arguments dictionary will be *before* any URL variable parameters. | |
16 | ||
17 | .. code-block:: python | |
18 | ||
19 | from webargs import fields | |
20 | from webargs.flaskparser import use_args | |
21 | ||
22 | ||
23 | @app.route("/user/<int:uid>") | |
24 | @use_args({"per_page": fields.Int()}, location="query") | |
25 | def user_detail(args, uid): | |
26 | return ("The user page for user {uid}, showing {per_page} posts.").format( | |
27 | uid=uid, per_page=args["per_page"] | |
28 | ) | |
29 | ||
30 | Error Handling | |
31 | ++++++++++++++ | |
32 | ||
33 | Webargs uses Flask's ``abort`` function to raise an ``HTTPException`` when a validation error occurs. | |
34 | If you use the ``Flask.errorhandler`` method to handle errors, you can access validation messages from the ``messages`` attribute of | |
35 | the attached ``ValidationError``. | |
36 | ||
37 | Here is an example error handler that returns validation messages to the client as JSON. | |
38 | ||
39 | .. code-block:: python | |
40 | ||
41 | from flask import jsonify | |
42 | ||
43 | ||
44 | # Return validation errors as JSON | |
45 | @app.errorhandler(422) | |
46 | @app.errorhandler(400) | |
47 | def handle_error(err): | |
48 | headers = err.data.get("headers", None) | |
49 | messages = err.data.get("messages", ["Invalid request."]) | |
50 | if headers: | |
51 | return jsonify({"errors": messages}), err.code, headers | |
52 | else: | |
53 | return jsonify({"errors": messages}), err.code | |
54 | ||
55 | URL Matches | |
56 | +++++++++++ | |
57 | ||
58 | The `FlaskParser` supports parsing values from a request's ``view_args``. | |
59 | ||
60 | .. code-block:: python | |
61 | ||
62 | from webargs.flaskparser import use_args | |
63 | ||
64 | ||
65 | @app.route("/greeting/<name>/") | |
66 | @use_args({"name": fields.Str()}, location="view_args") | |
67 | def greeting(args, **kwargs): | |
68 | return "Hello {}".format(args["name"]) | |
69 | ||
70 | ||
71 | Django | |
72 | ------ | |
73 | ||
74 | Django support is available via the :mod:`webargs.djangoparser` module. | |
75 | ||
76 | Webargs can parse Django request arguments in both function-based and class-based views. | |
77 | ||
78 | Decorator Usage | |
79 | +++++++++++++++ | |
80 | ||
81 | When using the :meth:`use_args <webargs.djangoparser.DjangoParser.use_args>` decorator, the arguments dictionary will positioned after the ``request`` argument. | |
82 | ||
83 | **Function-based Views** | |
84 | ||
85 | .. code-block:: python | |
86 | ||
87 | from django.http import HttpResponse | |
88 | from webargs import Arg | |
89 | from webargs.djangoparser import use_args | |
90 | ||
91 | account_args = { | |
92 | "username": fields.Str(required=True), | |
93 | "password": fields.Str(required=True), | |
94 | } | |
95 | ||
96 | ||
97 | @use_args(account_args, location="form") | |
98 | def login_user(request, args): | |
99 | if request.method == "POST": | |
100 | login(args["username"], args["password"]) | |
101 | return HttpResponse("Login page") | |
102 | ||
103 | **Class-based Views** | |
104 | ||
105 | .. code-block:: python | |
106 | ||
107 | from django.views.generic import View | |
108 | from django.shortcuts import render_to_response | |
109 | from webargs import fields | |
110 | from webargs.djangoparser import use_args | |
111 | ||
112 | blog_args = {"title": fields.Str(), "author": fields.Str()} | |
113 | ||
114 | ||
115 | class BlogPostView(View): | |
116 | @use_args(blog_args, location="query") | |
117 | def get(self, request, args): | |
118 | blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"]) | |
119 | return render_to_response("post_template.html", {"post": blog_post}) | |
120 | ||
121 | Error Handling | |
122 | ++++++++++++++ | |
123 | ||
124 | The :class:`DjangoParser` does not override :meth:`handle_error <webargs.core.Parser.handle_error>`, so your Django views are responsible for catching any :exc:`ValidationErrors` raised by the parser and returning the appropriate `HTTPResponse`. | |
125 | ||
126 | .. code-block:: python | |
127 | ||
128 | from django.http import JsonResponse | |
129 | ||
130 | from webargs import fields, ValidationError, json | |
131 | ||
132 | argmap = {"name": fields.Str(required=True)} | |
133 | ||
134 | ||
135 | def index(request): | |
136 | try: | |
137 | args = parser.parse(argmap, request) | |
138 | except ValidationError as err: | |
139 | return JsonResponse(err.messages, status=422) | |
140 | except json.JSONDecodeError: | |
141 | return JsonResponse({"json": ["Invalid JSON body."]}, status=400) | |
142 | return JsonResponse({"message": "Hello {name}".format(name=name)}) | |
143 | ||
144 | Tornado | |
145 | ------- | |
146 | ||
147 | Tornado argument parsing is available via the :mod:`webargs.tornadoparser` module. | |
148 | ||
149 | The :class:`webargs.tornadoparser.TornadoParser` parses arguments from a :class:`tornado.httpserver.HTTPRequest` object. The :class:`TornadoParser <webargs.tornadoparser.TornadoParser>` can be used directly, or you can decorate handler methods with :meth:`use_args <webargs.tornadoparser.TornadoParser.use_args>` or :meth:`use_kwargs <webargs.tornadoparser.TornadoParser.use_kwargs>`. | |
150 | ||
151 | .. code-block:: python | |
152 | ||
153 | import tornado.ioloop | |
154 | import tornado.web | |
155 | ||
156 | from webargs import fields | |
157 | from webargs.tornadoparser import parser | |
158 | ||
159 | ||
160 | class HelloHandler(tornado.web.RequestHandler): | |
161 | ||
162 | hello_args = {"name": fields.Str()} | |
163 | ||
164 | def post(self, id): | |
165 | reqargs = parser.parse(self.hello_args, self.request) | |
166 | response = {"message": "Hello {}".format(reqargs["name"])} | |
167 | self.write(response) | |
168 | ||
169 | ||
170 | application = tornado.web.Application([(r"/hello/([0-9]+)", HelloHandler)], debug=True) | |
171 | ||
172 | if __name__ == "__main__": | |
173 | application.listen(8888) | |
174 | tornado.ioloop.IOLoop.instance().start() | |
175 | ||
176 | Decorator Usage | |
177 | +++++++++++++++ | |
178 | ||
179 | When using the :meth:`use_args <webargs.tornadoparser.TornadoParser.use_args>` decorator, the decorated method will have the dictionary of parsed arguments passed as a positional argument after ``self`` and any regex match groups from the URL spec. | |
180 | ||
181 | ||
182 | .. code-block:: python | |
183 | ||
184 | from webargs import fields | |
185 | from webargs.tornadoparser import use_args | |
186 | ||
187 | ||
188 | class HelloHandler(tornado.web.RequestHandler): | |
189 | @use_args({"name": fields.Str()}) | |
190 | def post(self, id, reqargs): | |
191 | response = {"message": "Hello {}".format(reqargs["name"])} | |
192 | self.write(response) | |
193 | ||
194 | ||
195 | application = tornado.web.Application([(r"/hello/([0-9]+)", HelloHandler)], debug=True) | |
196 | ||
197 | As with the other parser modules, :meth:`use_kwargs <webargs.tornadoparser.TornadoParser.use_kwargs>` will add keyword arguments to the view callable. | |
198 | ||
199 | Error Handling | |
200 | ++++++++++++++ | |
201 | ||
202 | A `HTTPError <webargs.tornadoparser.HTTPError>` will be raised in the event of a validation error. Your `RequestHandlers` are responsible for handling these errors. | |
203 | ||
204 | Here is how you could write the error messages to a JSON response. | |
205 | ||
206 | .. code-block:: python | |
207 | ||
208 | from tornado.web import RequestHandler | |
209 | ||
210 | ||
211 | class MyRequestHandler(RequestHandler): | |
212 | def write_error(self, status_code, **kwargs): | |
213 | """Write errors as JSON.""" | |
214 | self.set_header("Content-Type", "application/json") | |
215 | if "exc_info" in kwargs: | |
216 | etype, exc, traceback = kwargs["exc_info"] | |
217 | if hasattr(exc, "messages"): | |
218 | self.write({"errors": exc.messages}) | |
219 | if getattr(exc, "headers", None): | |
220 | for name, val in exc.headers.items(): | |
221 | self.set_header(name, val) | |
222 | self.finish() | |
223 | ||
224 | Pyramid | |
225 | ------- | |
226 | ||
227 | Pyramid support is available via the :mod:`webargs.pyramidparser` module. | |
228 | ||
229 | Decorator Usage | |
230 | +++++++++++++++ | |
231 | ||
232 | When using the :meth:`use_args <webargs.pyramidparser.PyramidParser.use_args>` decorator on a view callable, the arguments dictionary will be positioned after the `request` argument. | |
233 | ||
234 | .. code-block:: python | |
235 | ||
236 | from pyramid.response import Response | |
237 | from webargs import fields | |
238 | from webargs.pyramidparser import use_args | |
239 | ||
240 | ||
241 | @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query") | |
242 | def user_detail(request, args): | |
243 | uid = args["uid"] | |
244 | return Response( | |
245 | "The user page for user {uid}, showing {per_page} posts.".format( | |
246 | uid=uid, per_page=args["per_page"] | |
247 | ) | |
248 | ) | |
249 | ||
250 | As with the other parser modules, :meth:`use_kwargs <webargs.pyramidparser.PyramidParser.use_kwargs>` will add keyword arguments to the view callable. | |
251 | ||
252 | URL Matches | |
253 | +++++++++++ | |
254 | ||
255 | The `PyramidParser` supports parsing values from a request's matchdict. | |
256 | ||
257 | .. code-block:: python | |
258 | ||
259 | from pyramid.response import Response | |
260 | from webargs.pyramidparser import use_args | |
261 | ||
262 | ||
263 | @use_args({"mymatch": fields.Int()}, location="matchdict") | |
264 | def matched(request, args): | |
265 | return Response("The value for mymatch is {}".format(args["mymatch"])) | |
266 | ||
267 | Falcon | |
268 | ------ | |
269 | ||
270 | Falcon support is available via the :mod:`webargs.falconparser` module. | |
271 | ||
272 | Decorator Usage | |
273 | +++++++++++++++ | |
274 | ||
275 | When using the :meth:`use_args <webargs.falconparser.FalconParser.use_args>` decorator on a resource method, the arguments dictionary will be positioned directly after the request and response arguments. | |
276 | ||
277 | ||
278 | .. code-block:: python | |
279 | ||
280 | import falcon | |
281 | from webargs import fields | |
282 | from webargs.falconparser import use_args | |
283 | ||
284 | ||
285 | class BlogResource: | |
286 | request_args = {"title": fields.Str(required=True)} | |
287 | ||
288 | @use_args(request_args) | |
289 | def on_post(self, req, resp, args, post_id): | |
290 | content = args["title"] | |
291 | # ... | |
292 | ||
293 | ||
294 | api = application = falcon.API() | |
295 | api.add_route("/blogs/{post_id}") | |
296 | ||
297 | As with the other parser modules, :meth:`use_kwargs <webargs.falconparser.FalconParser.use_kwargs>` will add keyword arguments to your resource methods. | |
298 | ||
299 | Hook Usage | |
300 | ++++++++++ | |
301 | ||
302 | You can easily implement hooks by using `parser.parse <webargs.falconparser.FalconParser.parse>` directly. | |
303 | ||
304 | .. code-block:: python | |
305 | ||
306 | import falcon | |
307 | from webargs import fields | |
308 | from webargs.falconparser import parser | |
309 | ||
310 | ||
311 | def add_args(argmap, **kwargs): | |
312 | def hook(req, resp, resource, params): | |
313 | parsed_args = parser.parse(argmap, req=req, **kwargs) | |
314 | req.context["args"] = parsed_args | |
315 | ||
316 | return hook | |
317 | ||
318 | ||
319 | @falcon.before(add_args({"page": fields.Int()}, location="query")) | |
320 | class AuthorResource: | |
321 | def on_get(self, req, resp): | |
322 | args = req.context["args"] | |
323 | page = args.get("page") | |
324 | # ... | |
325 | ||
326 | aiohttp | |
327 | ------- | |
328 | ||
329 | aiohttp support is available via the :mod:`webargs.aiohttpparser` module. | |
330 | ||
331 | ||
332 | The `parse <webargs.aiohttpparser.AIOHTTPParser.parse>` method of `AIOHTTPParser <webargs.aiohttpparser.AIOHTTPParser>` is a `coroutine <asyncio.coroutine>`. | |
333 | ||
334 | ||
335 | .. code-block:: python | |
336 | ||
337 | import asyncio | |
338 | ||
339 | from aiohttp import web | |
340 | from webargs import fields | |
341 | from webargs.aiohttpparser import parser | |
342 | ||
343 | handler_args = {"name": fields.Str(missing="World")} | |
344 | ||
345 | ||
346 | async def handler(request): | |
347 | args = await parser.parse(handler_args, request) | |
348 | return web.Response(body="Hello, {}".format(args["name"]).encode("utf-8")) | |
349 | ||
350 | ||
351 | Decorator Usage | |
352 | +++++++++++++++ | |
353 | ||
354 | When using the :meth:`use_args <webargs.aiohttpparser.AIOHTTPParser.use_args>` decorator on a handler, the parsed arguments dictionary will be the last positional argument. | |
355 | ||
356 | .. code-block:: python | |
357 | ||
358 | import asyncio | |
359 | ||
360 | from aiohttp import web | |
361 | from webargs import fields | |
362 | from webargs.aiohttpparser import use_args | |
363 | ||
364 | ||
365 | @use_args({"content": fields.Str(required=True)}) | |
366 | async def create_comment(request, args): | |
367 | content = args["content"] | |
368 | # ... | |
369 | ||
370 | ||
371 | app = web.Application() | |
372 | app.router.add_route("POST", "/comments/", create_comment) | |
373 | ||
374 | As with the other parser modules, :meth:`use_kwargs <webargs.aiohttpparser.AIOHTTPParser.use_kwargs>` will add keyword arguments to your resource methods. | |
375 | ||
376 | ||
377 | Usage with coroutines | |
378 | +++++++++++++++++++++ | |
379 | ||
380 | The :meth:`use_args <webargs.aiohttpparser.AIOHTTPParser.use_args>` and :meth:`use_kwargs <webargs.aiohttpparser.AIOHTTPParser.use_kwargs>` decorators will work with both `async def` coroutines and generator-based coroutines decorated with `asyncio.coroutine`. | |
381 | ||
382 | .. code-block:: python | |
383 | ||
384 | import asyncio | |
385 | ||
386 | from aiohttp import web | |
387 | from webargs import fields | |
388 | from webargs.aiohttpparser import use_kwargs | |
389 | ||
390 | hello_args = {"name": fields.Str(missing="World")} | |
391 | ||
392 | # The following are equivalent | |
393 | ||
394 | ||
395 | @asyncio.coroutine | |
396 | @use_kwargs(hello_args) | |
397 | def hello(request, name): | |
398 | return web.Response(body="Hello, {}".format(name).encode("utf-8")) | |
399 | ||
400 | ||
401 | @use_kwargs(hello_args) | |
402 | async def hello(request, name): | |
403 | return web.Response(body="Hello, {}".format(name).encode("utf-8")) | |
404 | ||
405 | URL Matches | |
406 | +++++++++++ | |
407 | ||
408 | The `AIOHTTPParser <webargs.aiohttpparser.AIOHTTPParser>` supports parsing values from a request's ``match_info``. | |
409 | ||
410 | .. code-block:: python | |
411 | ||
412 | from aiohttp import web | |
413 | from webargs.aiohttpparser import use_args | |
414 | ||
415 | ||
416 | @parser.use_args({"slug": fields.Str()}, location="match_info") | |
417 | def article_detail(request, args): | |
418 | return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8")) | |
419 | ||
420 | ||
421 | app = web.Application() | |
422 | app.router.add_route("GET", "/articles/{slug}", article_detail) | |
423 | ||
424 | ||
425 | Bottle | |
426 | ------ | |
427 | ||
428 | Bottle support is available via the :mod:`webargs.bottleparser` module. | |
429 | ||
430 | Decorator Usage | |
431 | +++++++++++++++ | |
432 | ||
433 | The preferred way to apply decorators to Bottle routes is using the | |
434 | ``apply`` argument. | |
435 | ||
436 | .. code-block:: python | |
437 | ||
438 | from bottle import route | |
439 | ||
440 | user_args = {"name": fields.Str(missing="Friend")} | |
441 | ||
442 | ||
443 | @route("/users/<_id:int>", method="GET", apply=use_args(user_args)) | |
444 | def users(args, _id): | |
445 | """A welcome page.""" | |
446 | return {"message": "Welcome, {}!".format(args["name"]), "_id": _id} |
0 | ======= | |
1 | webargs | |
2 | ======= | |
3 | ||
4 | Release v\ |version|. (:doc:`Changelog <changelog>`) | |
5 | ||
6 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. | |
7 | ||
8 | Upgrading from an older version? | |
9 | -------------------------------- | |
10 | ||
11 | See the :doc:`Upgrading to Newer Releases <upgrading>` page for notes on getting your code up-to-date with the latest version. | |
12 | ||
13 | ||
14 | Usage and Simple Examples | |
15 | ------------------------- | |
16 | ||
17 | .. code-block:: python | |
18 | ||
19 | from flask import Flask | |
20 | from webargs import fields | |
21 | from webargs.flaskparser import use_args | |
22 | ||
23 | app = Flask(__name__) | |
24 | ||
25 | ||
26 | @app.route("/") | |
27 | @use_args({"name": fields.Str(required=True)}, location="query") | |
28 | def index(args): | |
29 | return "Hello " + args["name"] | |
30 | ||
31 | ||
32 | if __name__ == "__main__": | |
33 | app.run() | |
34 | ||
35 | # curl http://localhost:5000/\?name\='World' | |
36 | # Hello World | |
37 | ||
38 | By default Webargs will automatically parse JSON request bodies. But it also | |
39 | has support for: | |
40 | ||
41 | **Query Parameters** | |
42 | :: | |
43 | $ curl http://localhost:5000/\?name\='Freddie' | |
44 | Hello Freddie | |
45 | ||
46 | # pass location="query" to use_args | |
47 | ||
48 | **Form Data** | |
49 | :: | |
50 | ||
51 | $ curl -d 'name=Brian' http://localhost:5000/ | |
52 | Hello Brian | |
53 | ||
54 | # pass location="form" to use_args | |
55 | ||
56 | **JSON Data** | |
57 | :: | |
58 | ||
59 | $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ | |
60 | Hello Roger | |
61 | ||
62 | # pass location="json" (or omit location) to use_args | |
63 | ||
64 | and, optionally: | |
65 | ||
66 | - Headers | |
67 | - Cookies | |
68 | - Files | |
69 | - Paths | |
70 | ||
71 | Why Use It | |
72 | ---------- | |
73 | ||
74 | * **Simple, declarative syntax**. Define your arguments as a mapping rather than imperatively pulling values off of request objects. | |
75 | * **Code reusability**. If you have multiple views that have the same request parameters, you only need to define your parameters once. You can also reuse validation and pre-processing routines. | |
76 | * **Self-documentation**. Webargs makes it easy to understand the expected arguments and their types for your view functions. | |
77 | * **Automatic documentation**. The metadata that webargs provides can serve as an aid for automatically generating API documentation. | |
78 | * **Cross-framework compatibility**. Webargs provides a consistent request-parsing interface that will work across many Python web frameworks. | |
79 | * **marshmallow integration**. Webargs uses `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ under the hood. When you need more flexibility than dictionaries, you can use marshmallow `Schemas <marshmallow.Schema>` to define your request arguments. | |
80 | ||
81 | Get It Now | |
82 | ---------- | |
83 | ||
84 | :: | |
85 | ||
86 | pip install -U webargs | |
87 | ||
88 | Ready to get started? Go on to the :doc:`Quickstart tutorial <quickstart>` or check out some `examples <https://github.com/marshmallow-code/webargs/tree/dev/examples>`_. | |
89 | ||
90 | User Guide | |
91 | ---------- | |
92 | ||
93 | .. toctree:: | |
94 | :maxdepth: 2 | |
95 | ||
96 | install | |
97 | quickstart | |
98 | advanced | |
99 | framework_support | |
100 | ecosystem | |
101 | ||
102 | API Reference | |
103 | ------------- | |
104 | ||
105 | .. toctree:: | |
106 | :maxdepth: 2 | |
107 | ||
108 | api | |
109 | ||
110 | ||
111 | Project Info | |
112 | ------------ | |
113 | ||
114 | .. toctree:: | |
115 | :maxdepth: 1 | |
116 | ||
117 | license | |
118 | changelog | |
119 | upgrading | |
120 | authors | |
121 | contributing |
0 | Install | |
1 | ======= | |
2 | ||
3 | **webargs** requires Python >= 3.6. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 3.0.0. | |
4 | ||
5 | From the PyPI | |
6 | ------------- | |
7 | ||
8 | To install the latest version from the PyPI: | |
9 | ||
10 | :: | |
11 | ||
12 | $ pip install -U webargs | |
13 | ||
14 | ||
15 | Get the Bleeding Edge Version | |
16 | ----------------------------- | |
17 | ||
18 | To get the latest development version of webargs, run | |
19 | ||
20 | :: | |
21 | ||
22 | $ pip install -U git+https://github.com/marshmallow-code/webargs.git@dev |
0 | @ECHO OFF | |
1 | ||
2 | REM Command file for Sphinx documentation | |
3 | ||
4 | if "%SPHINXBUILD%" == "" ( | |
5 | set SPHINXBUILD=sphinx-build | |
6 | ) | |
7 | set BUILDDIR=_build | |
8 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . | |
9 | set I18NSPHINXOPTS=%SPHINXOPTS% . | |
10 | if NOT "%PAPER%" == "" ( | |
11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% | |
12 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% | |
13 | ) | |
14 | ||
15 | if "%1" == "" goto help | |
16 | ||
17 | if "%1" == "help" ( | |
18 | :help | |
19 | echo.Please use `make ^<target^>` where ^<target^> is one of | |
20 | echo. html to make standalone HTML files | |
21 | echo. dirhtml to make HTML files named index.html in directories | |
22 | echo. singlehtml to make a single large HTML file | |
23 | echo. pickle to make pickle files | |
24 | echo. json to make JSON files | |
25 | echo. htmlhelp to make HTML files and a HTML help project | |
26 | echo. qthelp to make HTML files and a qthelp project | |
27 | echo. devhelp to make HTML files and a Devhelp project | |
28 | echo. epub to make an epub | |
29 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter | |
30 | echo. text to make text files | |
31 | echo. man to make manual pages | |
32 | echo. texinfo to make Texinfo files | |
33 | echo. gettext to make PO message catalogs | |
34 | echo. changes to make an overview over all changed/added/deprecated items | |
35 | echo. xml to make Docutils-native XML files | |
36 | echo. pseudoxml to make pseudoxml-XML files for display purposes | |
37 | echo. linkcheck to check all external links for integrity | |
38 | echo. doctest to run all doctests embedded in the documentation if enabled | |
39 | goto end | |
40 | ) | |
41 | ||
42 | if "%1" == "clean" ( | |
43 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i | |
44 | del /q /s %BUILDDIR%\* | |
45 | goto end | |
46 | ) | |
47 | ||
48 | ||
49 | %SPHINXBUILD% 2> nul | |
50 | if errorlevel 9009 ( | |
51 | echo. | |
52 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx | |
53 | echo.installed, then set the SPHINXBUILD environment variable to point | |
54 | echo.to the full path of the 'sphinx-build' executable. Alternatively you | |
55 | echo.may add the Sphinx directory to PATH. | |
56 | echo. | |
57 | echo.If you don't have Sphinx installed, grab it from | |
58 | echo.http://sphinx-doc.org/ | |
59 | exit /b 1 | |
60 | ) | |
61 | ||
62 | if "%1" == "html" ( | |
63 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html | |
64 | if errorlevel 1 exit /b 1 | |
65 | echo. | |
66 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. | |
67 | goto end | |
68 | ) | |
69 | ||
70 | if "%1" == "dirhtml" ( | |
71 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml | |
72 | if errorlevel 1 exit /b 1 | |
73 | echo. | |
74 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. | |
75 | goto end | |
76 | ) | |
77 | ||
78 | if "%1" == "singlehtml" ( | |
79 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml | |
80 | if errorlevel 1 exit /b 1 | |
81 | echo. | |
82 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. | |
83 | goto end | |
84 | ) | |
85 | ||
86 | if "%1" == "pickle" ( | |
87 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle | |
88 | if errorlevel 1 exit /b 1 | |
89 | echo. | |
90 | echo.Build finished; now you can process the pickle files. | |
91 | goto end | |
92 | ) | |
93 | ||
94 | if "%1" == "json" ( | |
95 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json | |
96 | if errorlevel 1 exit /b 1 | |
97 | echo. | |
98 | echo.Build finished; now you can process the JSON files. | |
99 | goto end | |
100 | ) | |
101 | ||
102 | if "%1" == "htmlhelp" ( | |
103 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp | |
104 | if errorlevel 1 exit /b 1 | |
105 | echo. | |
106 | echo.Build finished; now you can run HTML Help Workshop with the ^ | |
107 | .hhp project file in %BUILDDIR%/htmlhelp. | |
108 | goto end | |
109 | ) | |
110 | ||
111 | if "%1" == "qthelp" ( | |
112 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp | |
113 | if errorlevel 1 exit /b 1 | |
114 | echo. | |
115 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ | |
116 | .qhcp project file in %BUILDDIR%/qthelp, like this: | |
117 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp | |
118 | echo.To view the help file: | |
119 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc | |
120 | goto end | |
121 | ) | |
122 | ||
123 | if "%1" == "devhelp" ( | |
124 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp | |
125 | if errorlevel 1 exit /b 1 | |
126 | echo. | |
127 | echo.Build finished. | |
128 | goto end | |
129 | ) | |
130 | ||
131 | if "%1" == "epub" ( | |
132 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub | |
133 | if errorlevel 1 exit /b 1 | |
134 | echo. | |
135 | echo.Build finished. The epub file is in %BUILDDIR%/epub. | |
136 | goto end | |
137 | ) | |
138 | ||
139 | if "%1" == "latex" ( | |
140 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | |
141 | if errorlevel 1 exit /b 1 | |
142 | echo. | |
143 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. | |
144 | goto end | |
145 | ) | |
146 | ||
147 | if "%1" == "latexpdf" ( | |
148 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | |
149 | cd %BUILDDIR%/latex | |
150 | make all-pdf | |
151 | cd %BUILDDIR%/.. | |
152 | echo. | |
153 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. | |
154 | goto end | |
155 | ) | |
156 | ||
157 | if "%1" == "latexpdfja" ( | |
158 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | |
159 | cd %BUILDDIR%/latex | |
160 | make all-pdf-ja | |
161 | cd %BUILDDIR%/.. | |
162 | echo. | |
163 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. | |
164 | goto end | |
165 | ) | |
166 | ||
167 | if "%1" == "text" ( | |
168 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text | |
169 | if errorlevel 1 exit /b 1 | |
170 | echo. | |
171 | echo.Build finished. The text files are in %BUILDDIR%/text. | |
172 | goto end | |
173 | ) | |
174 | ||
175 | if "%1" == "man" ( | |
176 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man | |
177 | if errorlevel 1 exit /b 1 | |
178 | echo. | |
179 | echo.Build finished. The manual pages are in %BUILDDIR%/man. | |
180 | goto end | |
181 | ) | |
182 | ||
183 | if "%1" == "texinfo" ( | |
184 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo | |
185 | if errorlevel 1 exit /b 1 | |
186 | echo. | |
187 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. | |
188 | goto end | |
189 | ) | |
190 | ||
191 | if "%1" == "gettext" ( | |
192 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale | |
193 | if errorlevel 1 exit /b 1 | |
194 | echo. | |
195 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. | |
196 | goto end | |
197 | ) | |
198 | ||
199 | if "%1" == "changes" ( | |
200 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes | |
201 | if errorlevel 1 exit /b 1 | |
202 | echo. | |
203 | echo.The overview file is in %BUILDDIR%/changes. | |
204 | goto end | |
205 | ) | |
206 | ||
207 | if "%1" == "linkcheck" ( | |
208 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck | |
209 | if errorlevel 1 exit /b 1 | |
210 | echo. | |
211 | echo.Link check complete; look for any errors in the above output ^ | |
212 | or in %BUILDDIR%/linkcheck/output.txt. | |
213 | goto end | |
214 | ) | |
215 | ||
216 | if "%1" == "doctest" ( | |
217 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest | |
218 | if errorlevel 1 exit /b 1 | |
219 | echo. | |
220 | echo.Testing of doctests in the sources finished, look at the ^ | |
221 | results in %BUILDDIR%/doctest/output.txt. | |
222 | goto end | |
223 | ) | |
224 | ||
225 | if "%1" == "xml" ( | |
226 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml | |
227 | if errorlevel 1 exit /b 1 | |
228 | echo. | |
229 | echo.Build finished. The XML files are in %BUILDDIR%/xml. | |
230 | goto end | |
231 | ) | |
232 | ||
233 | if "%1" == "pseudoxml" ( | |
234 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml | |
235 | if errorlevel 1 exit /b 1 | |
236 | echo. | |
237 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. | |
238 | goto end | |
239 | ) | |
240 | ||
241 | :end⏎ |
0 | Quickstart | |
1 | ========== | |
2 | ||
3 | Basic Usage | |
4 | ----------- | |
5 | ||
6 | Arguments are specified as a dictionary of name -> :class:`Field <marshmallow.fields.Field>` pairs. | |
7 | ||
8 | .. code-block:: python | |
9 | ||
10 | from webargs import fields, validate | |
11 | ||
12 | user_args = { | |
13 | # Required arguments | |
14 | "username": fields.Str(required=True), | |
15 | # Validation | |
16 | "password": fields.Str(validate=lambda p: len(p) >= 6), | |
17 | # OR use marshmallow's built-in validators | |
18 | "password": fields.Str(validate=validate.Length(min=6)), | |
19 | # Default value when argument is missing | |
20 | "display_per_page": fields.Int(missing=10), | |
21 | # Repeated parameter, e.g. "/?nickname=Fred&nickname=Freddie" | |
22 | "nickname": fields.List(fields.Str()), | |
23 | # Delimited list, e.g. "/?languages=python,javascript" | |
24 | "languages": fields.DelimitedList(fields.Str()), | |
25 | # When value is keyed on a variable-unsafe name | |
26 | # or you want to rename a key | |
27 | "user_type": fields.Str(data_key="user-type"), | |
28 | } | |
29 | ||
30 | .. note:: | |
31 | ||
32 | See the `marshmallow.fields` documentation for a full reference on available field types. | |
33 | ||
34 | To parse request arguments, use the :meth:`parse <webargs.core.Parser.parse>` method of a :class:`Parser <webargs.core.Parser>` object. | |
35 | ||
36 | .. code-block:: python | |
37 | ||
38 | from flask import request | |
39 | from webargs.flaskparser import parser | |
40 | ||
41 | ||
42 | @app.route("/register", methods=["POST"]) | |
43 | def register(): | |
44 | args = parser.parse(user_args, request) | |
45 | return register_user( | |
46 | args["username"], | |
47 | args["password"], | |
48 | fullname=args["fullname"], | |
49 | per_page=args["display_per_page"], | |
50 | ) | |
51 | ||
52 | ||
53 | Decorator API | |
54 | ------------- | |
55 | ||
56 | As an alternative to `Parser.parse`, you can decorate your view with :meth:`use_args <webargs.core.Parser.use_args>` or :meth:`use_kwargs <webargs.core.Parser.use_kwargs>`. The parsed arguments dictionary will be injected as a parameter of your view function or as keyword arguments, respectively. | |
57 | ||
58 | .. code-block:: python | |
59 | ||
60 | from webargs.flaskparser import use_args, use_kwargs | |
61 | ||
62 | ||
63 | @app.route("/register", methods=["POST"]) | |
64 | @use_args(user_args) # Injects args dictionary | |
65 | def register(args): | |
66 | return register_user( | |
67 | args["username"], | |
68 | args["password"], | |
69 | fullname=args["fullname"], | |
70 | per_page=args["display_per_page"], | |
71 | ) | |
72 | ||
73 | ||
74 | @app.route("/settings", methods=["POST"]) | |
75 | @use_kwargs(user_args) # Injects keyword arguments | |
76 | def user_settings(username, password, fullname, display_per_page, nickname): | |
77 | return render_template("settings.html", username=username, nickname=nickname) | |
78 | ||
79 | ||
80 | .. note:: | |
81 | ||
82 | When using `use_kwargs`, any missing values will be omitted from the arguments. | |
83 | Use ``**kwargs`` to handle optional arguments. | |
84 | ||
85 | .. code-block:: python | |
86 | ||
87 | from webargs import fields, missing | |
88 | ||
89 | ||
90 | @use_kwargs({"name": fields.Str(required=True), "nickname": fields.Str(required=False)}) | |
91 | def myview(name, **kwargs): | |
92 | if "nickname" not in kwargs: | |
93 | # ... | |
94 | pass | |
95 | ||
96 | Request "Locations" | |
97 | ------------------- | |
98 | ||
99 | By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so: | |
100 | ||
101 | .. code-block:: python | |
102 | ||
103 | @app.route("/register") | |
104 | @use_args(user_args, location="form") | |
105 | def register(args): | |
106 | return "registration page" | |
107 | ||
108 | Available locations include: | |
109 | ||
110 | - ``'querystring'`` (same as ``'query'``) | |
111 | - ``'json'`` | |
112 | - ``'form'`` | |
113 | - ``'headers'`` | |
114 | - ``'cookies'`` | |
115 | - ``'files'`` | |
116 | ||
117 | Validation | |
118 | ---------- | |
119 | ||
120 | Each :class:`Field <marshmallow.fields.Field>` object can be validated individually by passing the ``validate`` argument. | |
121 | ||
122 | .. code-block:: python | |
123 | ||
124 | from webargs import fields | |
125 | ||
126 | args = {"age": fields.Int(validate=lambda val: val > 0)} | |
127 | ||
128 | The validator may return either a `boolean` or raise a :exc:`ValidationError <webargs.core.ValidationError>`. | |
129 | ||
130 | .. code-block:: python | |
131 | ||
132 | from webargs import fields, ValidationError | |
133 | ||
134 | ||
135 | def must_exist_in_db(val): | |
136 | if not User.query.get(val): | |
137 | # Optionally pass a status_code | |
138 | raise ValidationError("User does not exist") | |
139 | ||
140 | ||
141 | args = {"id": fields.Int(validate=must_exist_in_db)} | |
142 | ||
143 | .. note:: | |
144 | ||
145 | If a validator returns ``None``, validation will pass. A validator must return ``False`` or raise a `ValidationError <webargs.core.ValidationError>` | |
146 | for validation to fail. | |
147 | ||
148 | ||
149 | There are a number of built-in validators from `marshmallow.validate <marshmallow.validate>` | |
150 | (re-exported as `webargs.validate`). | |
151 | ||
152 | .. code-block:: python | |
153 | ||
154 | from webargs import fields, validate | |
155 | ||
156 | args = { | |
157 | "name": fields.Str(required=True, validate=[validate.Length(min=1, max=9999)]), | |
158 | "age": fields.Int(validate=[validate.Range(min=1, max=999)]), | |
159 | } | |
160 | ||
161 | The full arguments dictionary can also be validated by passing ``validate`` to :meth:`Parser.parse <webargs.core.Parser.parse>`, :meth:`Parser.use_args <webargs.core.Parser.use_args>`, :meth:`Parser.use_kwargs <webargs.core.Parser.use_kwargs>`. | |
162 | ||
163 | ||
164 | .. code-block:: python | |
165 | ||
166 | from webargs import fields | |
167 | from webargs.flaskparser import parser | |
168 | ||
169 | argmap = {"age": fields.Int(), "years_employed": fields.Int()} | |
170 | ||
171 | # ... | |
172 | result = parser.parse( | |
173 | argmap, validate=lambda args: args["years_employed"] < args["age"] | |
174 | ) | |
175 | ||
176 | ||
177 | Error Handling | |
178 | -------------- | |
179 | ||
180 | Each parser has a default error handling method. To override the error handling callback, write a function that | |
181 | receives an error, the request, the `marshmallow.Schema` instance, status code, and headers. | |
182 | Then decorate that function with :func:`Parser.error_handler <webargs.core.Parser.error_handler>`. | |
183 | ||
184 | .. code-block:: python | |
185 | ||
186 | from webargs import flaskparser | |
187 | ||
188 | parser = flaskparser.FlaskParser() | |
189 | ||
190 | ||
191 | class CustomError(Exception): | |
192 | pass | |
193 | ||
194 | ||
195 | @parser.error_handler | |
196 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
197 | raise CustomError(error.messages) | |
198 | ||
199 | Parsing Lists in Query Strings | |
200 | ------------------------------ | |
201 | ||
202 | Use `fields.DelimitedList <webargs.fields.DelimitedList>` to parse comma-separated | |
203 | lists in query parameters, e.g. ``/?permissions=read,write`` | |
204 | ||
205 | .. code-block:: python | |
206 | ||
207 | from webargs import fields | |
208 | ||
209 | args = {"permissions": fields.DelimitedList(fields.Str())} | |
210 | ||
211 | If you expect repeated query parameters, e.g. ``/?repo=webargs&repo=marshmallow``, use | |
212 | `fields.List <marshmallow.fields.List>` instead. | |
213 | ||
214 | .. code-block:: python | |
215 | ||
216 | from webargs import fields | |
217 | ||
218 | args = {"repo": fields.List(fields.Str())} | |
219 | ||
220 | Nesting Fields | |
221 | -------------- | |
222 | ||
223 | :class:`Field <marshmallow.fields.Field>` dictionaries can be nested within each other. This can be useful for validating nested data. | |
224 | ||
225 | .. code-block:: python | |
226 | ||
227 | from webargs import fields | |
228 | ||
229 | args = { | |
230 | "name": fields.Nested( | |
231 | {"first": fields.Str(required=True), "last": fields.Str(required=True)} | |
232 | ) | |
233 | } | |
234 | ||
235 | .. note:: | |
236 | ||
237 | Of the default supported locations in webargs, only the ``json`` request location supports nested datastructures. You can, however, :ref:`implement your own data loader <custom-loaders>` to add nested field functionality to the other locations. | |
238 | ||
239 | Next Steps | |
240 | ---------- | |
241 | ||
242 | - Go on to :doc:`Advanced Usage <advanced>` to learn how to add custom location handlers, use marshmallow Schemas, and more. | |
243 | - See the :doc:`Framework Support <framework_support>` page for framework-specific guides. | |
244 | - For example applications, check out the `examples <https://github.com/marshmallow-code/webargs/tree/dev/examples>`_ directory. |
0 | Upgrading to Newer Releases | |
1 | =========================== | |
2 | ||
3 | This section documents migration paths to new releases. | |
4 | ||
5 | Upgrading to 7.0 | |
6 | ++++++++++++++++ | |
7 | ||
8 | `unknown` is Now Settable by the Parser | |
9 | --------------------------------------- | |
10 | ||
11 | As of 7.0, `Parsers` have multiple settings for controlling the value for | |
12 | `unknown` which is passed to `schema.load` when parsing. | |
13 | ||
14 | To set unknown behavior on a parser, see the advanced doc on this topic: | |
15 | :ref:`advanced_setting_unknown`. | |
16 | ||
17 | Importantly, by default, any schema setting for `unknown` will be overridden by | |
18 | the `unknown` settings for the parser. | |
19 | ||
20 | In order to use a schema's `unknown` value, set `unknown=None` on the parser. | |
21 | In 6.x versions of webargs, schema values for `unknown` are used, so the | |
22 | `unknown=None` setting is the best way to emulate this. | |
23 | ||
24 | To get identical behavior: | |
25 | ||
26 | .. code-block:: python | |
27 | ||
28 | # assuming you have a schema named MySchema | |
29 | ||
30 | # webargs 6.x | |
31 | @parser.use_args(MySchema) | |
32 | def foo(args): | |
33 | ... | |
34 | ||
35 | ||
36 | # webargs 7.x | |
37 | # as a parameter to use_args or parse | |
38 | @parser.use_args(MySchema, unknown=None) | |
39 | def foo(args): | |
40 | ... | |
41 | ||
42 | ||
43 | # webargs 7.x | |
44 | # as a parser setting | |
45 | # example with flaskparser, but any parser class works | |
46 | parser = FlaskParser(unknown=None) | |
47 | ||
48 | ||
49 | @parser.use_args(MySchema) | |
50 | def foo(args): | |
51 | ... | |
52 | ||
53 | Upgrading to 6.0 | |
54 | ++++++++++++++++ | |
55 | ||
56 | Multiple Locations Are No Longer Supported In A Single Call | |
57 | ----------------------------------------------------------- | |
58 | ||
59 | The default location is JSON/body. | |
60 | ||
61 | Under webargs 5.x, code often did not have to specify a location. | |
62 | ||
63 | Because webargs would parse data from multiple locations automatically, users | |
64 | did not need to specify where a parameter, call it `q`, was passed. | |
65 | `q` could be in a query parameter or in a JSON or form-post body. | |
66 | ||
67 | Now, webargs requires that users specify only one location for data loading per | |
68 | `use_args` call, and `"json"` is the default. If `q` is intended to be a query | |
69 | parameter, the developer must be explicit and rewrite like so: | |
70 | ||
71 | .. code-block:: python | |
72 | ||
73 | # webargs 5.x | |
74 | @parser.use_args({"q": ma.fields.String()}) | |
75 | def foo(args): | |
76 | return some_function(user_query=args.get("q")) | |
77 | ||
78 | ||
79 | # webargs 6.x | |
80 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
81 | def foo(args): | |
82 | return some_function(user_query=args.get("q")) | |
83 | ||
84 | This also means that another usage from 5.x is not supported. Code with | |
85 | multiple locations in a single `use_args`, `use_kwargs`, or `parse` call | |
86 | must be rewritten in multiple separate `use_args` or `use_kwargs` invocations, | |
87 | like so: | |
88 | ||
89 | .. code-block:: python | |
90 | ||
91 | # webargs 5.x | |
92 | @parser.use_kwargs( | |
93 | { | |
94 | "q1": ma.fields.Int(location="query"), | |
95 | "q2": ma.fields.Int(location="query"), | |
96 | "h1": ma.fields.Int(location="headers"), | |
97 | }, | |
98 | locations=("query", "headers"), | |
99 | ) | |
100 | def foo(q1, q2, h1): | |
101 | ... | |
102 | ||
103 | ||
104 | # webargs 6.x | |
105 | @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") | |
106 | @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers") | |
107 | def foo(q1, q2, h1): | |
108 | ... | |
109 | ||
110 | ||
111 | Fields No Longer Support location=... | |
112 | ------------------------------------- | |
113 | ||
114 | Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call | |
115 | cannot specify multiple locations, it is not necessary for a field to be able | |
116 | to specify its location. Rewrite code like so: | |
117 | ||
118 | .. code-block:: python | |
119 | ||
120 | # webargs 5.x | |
121 | @parser.use_args({"q": ma.fields.String(location="query")}) | |
122 | def foo(args): | |
123 | return some_function(user_query=args.get("q")) | |
124 | ||
125 | ||
126 | # webargs 6.x | |
127 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
128 | def foo(args): | |
129 | return some_function(user_query=args.get("q")) | |
130 | ||
131 | location_handler Has Been Replaced With location_loader | |
132 | ------------------------------------------------------- | |
133 | ||
134 | This is not just a name change. The expected signature of a `location_loader` | |
135 | is slightly different from the signature for a `location_handler`. | |
136 | ||
137 | Where previously a `location_handler` code took the incoming request data and | |
138 | details of a single field being loaded, a `location_loader` takes the request | |
139 | and the schema as a pair. It does not return a specific field's data, but data | |
140 | for the whole location. | |
141 | ||
142 | Rewrite code like this: | |
143 | ||
144 | .. code-block:: python | |
145 | ||
146 | # webargs 5.x | |
147 | @parser.location_handler("data") | |
148 | def load_data(request, name, field): | |
149 | return request.data.get(name) | |
150 | ||
151 | ||
152 | # webargs 6.x | |
153 | @parser.location_loader("data") | |
154 | def load_data(request, schema): | |
155 | return request.data | |
156 | ||
157 | Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified | |
158 | ---------------------------------------------------------------------------- | |
159 | ||
160 | In webargs 5.x, the deserialization schema was used to pull data out of the | |
161 | request object. That data was compiled into a dictionary which was then passed | |
162 | to the schema. | |
163 | ||
164 | One of the major changes in webargs 6.x allows the use of `unknown` parameter | |
165 | on schemas. This lets a schema decide what to do with fields not specified in | |
166 | the schema. In order to achieve this, webargs now passes the full data from | |
167 | the specified location to the schema. | |
168 | ||
169 | Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in | |
170 | order to filter out unknown fields. Like so: | |
171 | ||
172 | .. code-block:: python | |
173 | ||
174 | # webargs 5.x | |
175 | # this can assume that "q" is the only parameter passed, and all other | |
176 | # parameters will be ignored | |
177 | @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",)) | |
178 | def foo(q): | |
179 | ... | |
180 | ||
181 | ||
182 | # webargs 6.x, Solution 1: declare a schema with Meta.unknown set | |
183 | class QuerySchema(ma.Schema): | |
184 | q = ma.fields.String() | |
185 | ||
186 | class Meta: | |
187 | unknown = ma.EXCLUDE | |
188 | ||
189 | ||
190 | @parser.use_kwargs(QuerySchema, location="query") | |
191 | def foo(q): | |
192 | ... | |
193 | ||
194 | ||
195 | # webargs 6.x, Solution 2: instantiate a schema with unknown set | |
196 | class QuerySchema(ma.Schema): | |
197 | q = ma.fields.String() | |
198 | ||
199 | ||
200 | @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query") | |
201 | def foo(q): | |
202 | ... | |
203 | ||
204 | ||
205 | This also allows usage which passes the unknown parameters through, like so: | |
206 | ||
207 | .. code-block:: python | |
208 | ||
209 | # webargs 6.x only! cannot be done in 5.x | |
210 | class QuerySchema(ma.Schema): | |
211 | q = ma.fields.String() | |
212 | ||
213 | ||
214 | # will pass *all* query params through as "kwargs" | |
215 | @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query") | |
216 | def foo(q, **kwargs): | |
217 | ... | |
218 | ||
219 | ||
220 | However, many types of request data are so-called "multidicts" -- dictionary-like | |
221 | types which can return one or multiple values. To handle `marshmallow.fields.List` | |
222 | and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs | |
223 | must combine schema information with the raw request data. This is done in the | |
224 | :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which | |
225 | will often be passed to schemas. | |
226 | ||
227 | This means that if a schema has a `pre_load` hook which interacts with the data, | |
228 | it may need modifications. For example, a `flask` query string will be parsed | |
229 | into an `ImmutableMultiDict` type, which will break pre-load hooks which modify | |
230 | the data in-place. Such usages need rewrites like so: | |
231 | ||
232 | .. code-block:: python | |
233 | ||
234 | # webargs 5.x | |
235 | # flask query params is just an example -- applies to several types | |
236 | from webargs.flaskparser import use_kwargs | |
237 | ||
238 | ||
239 | class QuerySchema(ma.Schema): | |
240 | q = ma.fields.String() | |
241 | ||
242 | @ma.pre_load | |
243 | def convert_nil_to_none(self, obj, **kwargs): | |
244 | if obj.get("q") == "nil": | |
245 | obj["q"] = None | |
246 | return obj | |
247 | ||
248 | ||
249 | @use_kwargs(QuerySchema, locations=("query",)) | |
250 | def foo(q): | |
251 | ... | |
252 | ||
253 | ||
254 | # webargs 6.x | |
255 | class QuerySchema(ma.Schema): | |
256 | q = ma.fields.String() | |
257 | ||
258 | # unlike under 5.x, we cannot modify 'obj' in-place because writing | |
259 | # to the MultiDictProxy will try to write to the underlying | |
260 | # ImmutableMultiDict, which is not allowed | |
261 | @ma.pre_load | |
262 | def convert_nil_to_none(self, obj, **kwargs): | |
263 | # creating a dict from a MultiDictProxy works well because it | |
264 | # "unwraps" lists and delimited lists correctly | |
265 | data = dict(obj) | |
266 | if data.get("q") == "nil": | |
267 | data["q"] = None | |
268 | return data | |
269 | ||
270 | ||
271 | @parser.use_kwargs(QuerySchema, location="query") | |
272 | def foo(q): | |
273 | ... | |
274 | ||
275 | ||
276 | DelimitedList Now Only Takes A String Input | |
277 | ------------------------------------------- | |
278 | ||
279 | Combining `List` and string parsing functionality in a single type had some | |
280 | messy corner cases. For the most part, this should not require rewrites. But | |
281 | for APIs which need to allow both usages, rewrites are possible like so: | |
282 | ||
283 | .. code-block:: python | |
284 | ||
285 | # webargs 5.x | |
286 | # this allows ...?x=1&x=2&x=3 | |
287 | # as well as ...?x=1,2,3 | |
288 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",)) | |
289 | def foo(x): | |
290 | ... | |
291 | ||
292 | ||
293 | # webargs 6.x | |
294 | # this accepts x=1,2,3 but NOT x=1&x=2&x=3 | |
295 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query") | |
296 | def foo(x): | |
297 | ... | |
298 | ||
299 | ||
300 | # webargs 6.x | |
301 | # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3 | |
302 | # to do this, it needs a post_load hook which will flatten out the list data | |
303 | class UnpackingDelimitedListSchema(ma.Schema): | |
304 | x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int)) | |
305 | ||
306 | @ma.post_load | |
307 | def flatten_lists(self, data, **kwargs): | |
308 | new_x = [] | |
309 | for x in data["x"]: | |
310 | new_x.extend(x) | |
311 | data["x"] = new_x | |
312 | return data | |
313 | ||
314 | ||
315 | @parser.use_kwargs(UnpackingDelimitedListSchema, location="query") | |
316 | def foo(x): | |
317 | ... | |
318 | ||
319 | ||
320 | ValidationError Messages Are Namespaced Under The Location | |
321 | ---------------------------------------------------------- | |
322 | ||
323 | Code parsing ValidationError messages will notice a change in the messages | |
324 | produced by webargs. | |
325 | What would previously have come back with messages like `{"foo":["Not a valid integer."]}` | |
326 | will now have messages nested one layer deeper, like | |
327 | `{"json":{"foo":["Not a valid integer."]}}`. | |
328 | ||
329 | To rewrite code which was handling these errors, the handler will need to be | |
330 | prepared to traverse messages by one additional level. For example: | |
331 | ||
332 | .. code-block:: python | |
333 | ||
334 | import logging | |
335 | ||
336 | log = logging.getLogger(__name__) | |
337 | ||
338 | ||
339 | # webargs 5.x | |
340 | # logs debug messages like | |
341 | # bad value for 'foo': ["Not a valid integer."] | |
342 | # bad value for 'bar': ["Not a valid boolean."] | |
343 | def log_invalid_parameters(validation_error): | |
344 | for field, messages in validation_error.messages.items(): | |
345 | log.debug("bad value for '{}': {}".format(field, messages)) | |
346 | ||
347 | ||
348 | # webargs 6.x | |
349 | # logs debug messages like | |
350 | # bad value for 'foo' [query]: ["Not a valid integer."] | |
351 | # bad value for 'bar' [json]: ["Not a valid boolean."] | |
352 | def log_invalid_parameters(validation_error): | |
353 | for location, fielddata in validation_error.messages.items(): | |
354 | for field, messages in fielddata.items(): | |
355 | log.debug("bad value for '{}' [{}]: {}".format(field, location, messages)) | |
356 | ||
357 | ||
358 | Custom Error Handler Argument Names Changed | |
359 | ------------------------------------------- | |
360 | ||
361 | If you define a custom error handler via `@parser.error_handler` the function | |
362 | arguments are now keyword-only and `status_code` and `headers` have been renamed | |
363 | `error_status_code` and `error_headers`. | |
364 | ||
365 | .. code-block:: python | |
366 | ||
367 | # webargs 5.x | |
368 | @parser.error_handler | |
369 | def custom_handle_error(error, req, schema, status_code, headers): | |
370 | ... | |
371 | ||
372 | ||
373 | # webargs 6.x | |
374 | @parser.error_handler | |
375 | def custom_handle_error(error, req, schema, *, error_status_code, error_headers): | |
376 | ... | |
377 | ||
378 | ||
379 | Some Functions Take Keyword-Only Arguments Now | |
380 | ---------------------------------------------- | |
381 | ||
382 | The signature of several methods has changed to have keyword-only arguments. | |
383 | For the most part, this should not require any changes, but here's a list of | |
384 | the changes. | |
385 | ||
386 | `parser.error_handler` methods: | |
387 | ||
388 | .. code-block:: python | |
389 | ||
390 | # webargs 5.x | |
391 | def handle_error(error, req, schema, status_code, headers): | |
392 | ... | |
393 | ||
394 | ||
395 | # webargs 6.x | |
396 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
397 | ... | |
398 | ||
399 | `parser.__init__` methods: | |
400 | ||
401 | .. code-block:: python | |
402 | ||
403 | # webargs 5.x | |
404 | def __init__(self, location=None, error_handler=None, schema_class=None): | |
405 | ... | |
406 | ||
407 | ||
408 | # webargs 6.x | |
409 | def __init__(self, location=None, *, error_handler=None, schema_class=None): | |
410 | ... | |
411 | ||
412 | `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods: | |
413 | ||
414 | ||
415 | .. code-block:: python | |
416 | ||
417 | # webargs 5.x | |
418 | def parse( | |
419 | self, | |
420 | argmap, | |
421 | req=None, | |
422 | location=None, | |
423 | validate=None, | |
424 | error_status_code=None, | |
425 | error_headers=None, | |
426 | ): | |
427 | ... | |
428 | ||
429 | ||
430 | # webargs 6.x | |
431 | def parse( | |
432 | self, | |
433 | argmap, | |
434 | req=None, | |
435 | *, | |
436 | location=None, | |
437 | validate=None, | |
438 | error_status_code=None, | |
439 | error_headers=None | |
440 | ): | |
441 | ... | |
442 | ||
443 | ||
444 | # webargs 5.x | |
445 | def use_args( | |
446 | self, | |
447 | argmap, | |
448 | req=None, | |
449 | location=None, | |
450 | as_kwargs=False, | |
451 | validate=None, | |
452 | error_status_code=None, | |
453 | error_headers=None, | |
454 | ): | |
455 | ... | |
456 | ||
457 | ||
458 | # webargs 6.x | |
459 | def use_args( | |
460 | self, | |
461 | argmap, | |
462 | req=None, | |
463 | *, | |
464 | location=None, | |
465 | as_kwargs=False, | |
466 | validate=None, | |
467 | error_status_code=None, | |
468 | error_headers=None | |
469 | ): | |
470 | ... | |
471 | ||
472 | ||
473 | # use_kwargs is just an alias for use_args with as_kwargs=True | |
474 | ||
475 | and finally, the `dict2schema` function: | |
476 | ||
477 | .. code-block:: python | |
478 | ||
479 | # webargs 5.x | |
480 | def dict2schema(dct, schema_class=ma.Schema): | |
481 | ... | |
482 | ||
483 | ||
484 | # webargs 6.x | |
485 | def dict2schema(dct, *, schema_class=ma.Schema): | |
486 | ... | |
487 | ||
488 | ||
489 | PyramidParser Now Appends Arguments (Used To Prepend) | |
490 | ----------------------------------------------------- | |
491 | ||
492 | `PyramidParser.use_args` was not conformant with the other parsers in webargs. | |
493 | While all other parsers added new arguments to the end of the argument list of | |
494 | a decorated view function, the Pyramid implementation added them to the front | |
495 | of the argument list. | |
496 | ||
497 | This has been corrected, but as a result pyramid views with `use_args` may need | |
498 | to be rewritten. The `request` object is always passed first in both versions, | |
499 | so the issue is only apparent with view functions taking other positional | |
500 | arguments. | |
501 | ||
502 | For example, imagine code with a decorator for passing user information, | |
503 | `pass_userinfo`, like so: | |
504 | ||
505 | .. code-block:: python | |
506 | ||
507 | # a decorator which gets information about the authenticated user | |
508 | def pass_userinfo(f): | |
509 | def decorator(request, *args, **kwargs): | |
510 | return f(request, get_userinfo(), *args, **kwargs) | |
511 | ||
512 | return decorator | |
513 | ||
514 | You will see a behavioral change if `pass_userinfo` is called on a function | |
515 | decorated with `use_args`. The difference between the two versions will be like | |
516 | so: | |
517 | ||
518 | .. code-block:: python | |
519 | ||
520 | from webargs.pyramidparser import use_args | |
521 | ||
522 | # webargs 5.x | |
523 | # pass_userinfo is called first, webargs sees positional arguments of | |
524 | # (userinfo,) | |
525 | # and changes it to | |
526 | # (request, args, userinfo) | |
527 | @pass_userinfo | |
528 | @use_args({"q": ma.fields.String()}, locations=("query",)) | |
529 | def viewfunc(request, args, userinfo): | |
530 | q = args.get("q") | |
531 | ... | |
532 | ||
533 | ||
534 | # webargs 6.x | |
535 | # pass_userinfo is called first, webargs sees positional arguments of | |
536 | # (userinfo,) | |
537 | # and changes it to | |
538 | # (request, userinfo, args) | |
539 | @pass_userinfo | |
540 | @use_args({"q": ma.fields.String()}, location="query") | |
541 | def viewfunc(request, userinfo, args): | |
542 | q = args.get("q") | |
543 | ... |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/aiohttp_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | import asyncio | |
15 | import datetime as dt | |
16 | ||
17 | from aiohttp import web | |
18 | from aiohttp.web import json_response | |
19 | from webargs import fields, validate | |
20 | from webargs.aiohttpparser import use_args, use_kwargs | |
21 | ||
22 | hello_args = {"name": fields.Str(missing="Friend")} | |
23 | ||
24 | ||
25 | @use_args(hello_args) | |
26 | async def index(request, args): | |
27 | """A welcome page.""" | |
28 | return json_response({"message": "Welcome, {}!".format(args["name"])}) | |
29 | ||
30 | ||
31 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
32 | ||
33 | ||
34 | @use_kwargs(add_args) | |
35 | async def add(request, x, y): | |
36 | """An addition endpoint.""" | |
37 | return json_response({"result": x + y}) | |
38 | ||
39 | ||
40 | dateadd_args = { | |
41 | "value": fields.Date(required=False), | |
42 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
43 | "unit": fields.Str(missing="days", validate=validate.OneOf(["minutes", "days"])), | |
44 | } | |
45 | ||
46 | ||
47 | @use_kwargs(dateadd_args) | |
48 | async def dateadd(request, value, addend, unit): | |
49 | """A datetime adder endpoint.""" | |
50 | value = value or dt.datetime.utcnow() | |
51 | if unit == "minutes": | |
52 | delta = dt.timedelta(minutes=addend) | |
53 | else: | |
54 | delta = dt.timedelta(days=addend) | |
55 | result = value + delta | |
56 | return json_response({"result": result.isoformat()}) | |
57 | ||
58 | ||
59 | def create_app(): | |
60 | app = web.Application() | |
61 | app.router.add_route("GET", "/", index) | |
62 | app.router.add_route("POST", "/add", add) | |
63 | app.router.add_route("POST", "/dateadd", dateadd) | |
64 | return app | |
65 | ||
66 | ||
67 | def run(app, port=5001): | |
68 | loop = asyncio.get_event_loop() | |
69 | handler = app.make_handler() | |
70 | f = loop.create_server(handler, "0.0.0.0", port) | |
71 | srv = loop.run_until_complete(f) | |
72 | print("serving on", srv.sockets[0].getsockname()) | |
73 | try: | |
74 | loop.run_forever() | |
75 | except KeyboardInterrupt: | |
76 | pass | |
77 | finally: | |
78 | loop.run_until_complete(handler.finish_connections(1.0)) | |
79 | srv.close() | |
80 | loop.run_until_complete(srv.wait_closed()) | |
81 | loop.run_until_complete(app.finish()) | |
82 | loop.close() | |
83 | ||
84 | ||
85 | if __name__ == "__main__": | |
86 | app = create_app() | |
87 | run(app) |
0 | """Example of using Python 3 function annotations to define | |
1 | request arguments and output schemas. | |
2 | ||
3 | Run the app: | |
4 | ||
5 | $ python examples/annotations_example.py | |
6 | ||
7 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
8 | ||
9 | $ pip install httpie | |
10 | $ http GET :5001/ | |
11 | $ http GET :5001/ name==Ada | |
12 | $ http POST :5001/add x=40 y=2 | |
13 | $ http GET :5001/users/42 | |
14 | """ | |
15 | import random | |
16 | import functools | |
17 | ||
18 | from flask import Flask, request | |
19 | from marshmallow import Schema | |
20 | from webargs import fields | |
21 | from webargs.flaskparser import parser | |
22 | ||
23 | ||
24 | app = Flask(__name__) | |
25 | ||
26 | ##### Routing wrapper #### | |
27 | ||
28 | ||
29 | def route(*args, **kwargs): | |
30 | """Combines `Flask.route` and webargs parsing. Allows arguments to be specified | |
31 | as function annotations. An output schema can optionally be specified by a | |
32 | return annotation. | |
33 | """ | |
34 | ||
35 | def decorator(func): | |
36 | @app.route(*args, **kwargs) | |
37 | @functools.wraps(func) | |
38 | def wrapped_view(*a, **kw): | |
39 | annotations = getattr(func, "__annotations__", {}) | |
40 | reqargs = { | |
41 | name: value | |
42 | for name, value in annotations.items() | |
43 | if isinstance(value, fields.Field) and name != "return" | |
44 | } | |
45 | response_schema = annotations.get("return") | |
46 | schema_cls = Schema.from_dict(reqargs) | |
47 | partial = request.method != "POST" | |
48 | parsed = parser.parse(schema_cls(partial=partial), request) | |
49 | kw.update(parsed) | |
50 | response_data = func(*a, **kw) | |
51 | if response_schema: | |
52 | return response_schema.dump(response_data) | |
53 | else: | |
54 | return func(*a, **kw) | |
55 | ||
56 | return wrapped_view | |
57 | ||
58 | return decorator | |
59 | ||
60 | ||
61 | ##### Fake database and model ##### | |
62 | ||
63 | ||
64 | class Model: | |
65 | def __init__(self, **kwargs): | |
66 | self.__dict__.update(kwargs) | |
67 | ||
68 | def update(self, **kwargs): | |
69 | self.__dict__.update(kwargs) | |
70 | ||
71 | @classmethod | |
72 | def insert(cls, db, **kwargs): | |
73 | collection = db[cls.collection] | |
74 | new_id = None | |
75 | if "id" in kwargs: # for setting up fixtures | |
76 | new_id = kwargs.pop("id") | |
77 | else: # find a new id | |
78 | found_id = False | |
79 | while not found_id: | |
80 | new_id = random.randint(1, 9999) | |
81 | if new_id not in collection: | |
82 | found_id = True | |
83 | new_record = cls(id=new_id, **kwargs) | |
84 | collection[new_id] = new_record | |
85 | return new_record | |
86 | ||
87 | ||
88 | class User(Model): | |
89 | collection = "users" | |
90 | ||
91 | ||
92 | db = {"users": {}} | |
93 | ||
94 | ##### Views ##### | |
95 | ||
96 | ||
97 | @route("/", methods=["GET"]) | |
98 | def index(name: fields.Str(missing="Friend")): # noqa: F821 | |
99 | return {"message": f"Hello, {name}!"} | |
100 | ||
101 | ||
102 | @route("/add", methods=["POST"]) | |
103 | def add(x: fields.Float(required=True), y: fields.Float(required=True)): | |
104 | return {"result": x + y} | |
105 | ||
106 | ||
107 | class UserSchema(Schema): | |
108 | id = fields.Int(dump_only=True) | |
109 | username = fields.Str(required=True) | |
110 | first_name = fields.Str() | |
111 | last_name = fields.Str() | |
112 | ||
113 | ||
114 | @route("/users/<int:user_id>", methods=["GET", "PATCH"]) | |
115 | def user_detail(user_id, username: fields.Str(required=True) = None) -> UserSchema(): | |
116 | user = db["users"].get(user_id) | |
117 | if not user: | |
118 | return {"message": "User not found"}, 404 | |
119 | if request.method == "PATCH": | |
120 | user.update(username=username) | |
121 | return user | |
122 | ||
123 | ||
124 | # Return validation errors as JSON | |
125 | @app.errorhandler(422) | |
126 | @app.errorhandler(400) | |
127 | def handle_error(err): | |
128 | headers = err.data.get("headers", None) | |
129 | messages = err.data.get("messages", ["Invalid request."]) | |
130 | if headers: | |
131 | return {"errors": messages}, err.code, headers | |
132 | else: | |
133 | return {"errors": messages}, err.code | |
134 | ||
135 | ||
136 | if __name__ == "__main__": | |
137 | User.insert( | |
138 | db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" | |
139 | ) | |
140 | app.run(port=5001, debug=True) |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/bottle_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | import datetime as dt | |
15 | ||
16 | from bottle import route, run, error, response | |
17 | from webargs import fields, validate | |
18 | from webargs.bottleparser import use_args, use_kwargs | |
19 | ||
20 | ||
21 | hello_args = {"name": fields.Str(missing="Friend")} | |
22 | ||
23 | ||
24 | @route("/", method="GET", apply=use_args(hello_args)) | |
25 | def index(args): | |
26 | """A welcome page.""" | |
27 | return {"message": "Welcome, {}!".format(args["name"])} | |
28 | ||
29 | ||
30 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
31 | ||
32 | ||
33 | @route("/add", method="POST", apply=use_kwargs(add_args)) | |
34 | def add(x, y): | |
35 | """An addition endpoint.""" | |
36 | return {"result": x + y} | |
37 | ||
38 | ||
39 | dateadd_args = { | |
40 | "value": fields.Date(required=False), | |
41 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
42 | "unit": fields.Str(missing="days", validate=validate.OneOf(["minutes", "days"])), | |
43 | } | |
44 | ||
45 | ||
46 | @route("/dateadd", method="POST", apply=use_kwargs(dateadd_args)) | |
47 | def dateadd(value, addend, unit): | |
48 | """A date adder endpoint.""" | |
49 | value = value or dt.datetime.utcnow() | |
50 | if unit == "minutes": | |
51 | delta = dt.timedelta(minutes=addend) | |
52 | else: | |
53 | delta = dt.timedelta(days=addend) | |
54 | result = value + delta | |
55 | return {"result": result.isoformat()} | |
56 | ||
57 | ||
58 | # Return validation errors as JSON | |
59 | @error(400) | |
60 | @error(422) | |
61 | def handle_error(err): | |
62 | response.content_type = "application/json" | |
63 | return err.body | |
64 | ||
65 | ||
66 | if __name__ == "__main__": | |
67 | run(port=5001, reloader=True, debug=True) |
0 | """A simple number and datetime addition JSON API. | |
1 | Demonstrates different strategies for parsing arguments | |
2 | with the FalconParser. | |
3 | ||
4 | Run the app: | |
5 | ||
6 | $ pip install gunicorn | |
7 | $ gunicorn examples.falcon_example:app | |
8 | ||
9 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
10 | ||
11 | $ pip install httpie | |
12 | $ http GET :8000/ | |
13 | $ http GET :8000/ name==Ada | |
14 | $ http POST :8000/add x=40 y=2 | |
15 | $ http POST :8000/dateadd value=1973-04-10 addend=63 | |
16 | $ http POST :8000/dateadd value=2014-10-23 addend=525600 unit=minutes | |
17 | """ | |
18 | import datetime as dt | |
19 | ||
20 | from webargs.core import json | |
21 | ||
22 | import falcon | |
23 | from webargs import fields, validate | |
24 | from webargs.falconparser import use_args, use_kwargs, parser | |
25 | ||
26 | ### Middleware and hooks ### | |
27 | ||
28 | ||
29 | class JSONTranslator: | |
30 | def process_response(self, req, resp, resource): | |
31 | if "result" not in req.context: | |
32 | return | |
33 | resp.body = json.dumps(req.context["result"]) | |
34 | ||
35 | ||
36 | def add_args(argmap, **kwargs): | |
37 | def hook(req, resp, params): | |
38 | req.context["args"] = parser.parse(argmap, req=req, **kwargs) | |
39 | ||
40 | return hook | |
41 | ||
42 | ||
43 | ### Resources ### | |
44 | ||
45 | ||
46 | class HelloResource: | |
47 | """A welcome page.""" | |
48 | ||
49 | hello_args = {"name": fields.Str(missing="Friend", location="query")} | |
50 | ||
51 | @use_args(hello_args) | |
52 | def on_get(self, req, resp, args): | |
53 | req.context["result"] = {"message": "Welcome, {}!".format(args["name"])} | |
54 | ||
55 | ||
56 | class AdderResource: | |
57 | """An addition endpoint.""" | |
58 | ||
59 | adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
60 | ||
61 | @use_kwargs(adder_args) | |
62 | def on_post(self, req, resp, x, y): | |
63 | req.context["result"] = {"result": x + y} | |
64 | ||
65 | ||
66 | class DateAddResource: | |
67 | """A datetime adder endpoint.""" | |
68 | ||
69 | dateadd_args = { | |
70 | "value": fields.Date(required=False), | |
71 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
72 | "unit": fields.Str( | |
73 | missing="days", validate=validate.OneOf(["minutes", "days"]) | |
74 | ), | |
75 | } | |
76 | ||
77 | @falcon.before(add_args(dateadd_args)) | |
78 | def on_post(self, req, resp): | |
79 | """A datetime adder endpoint.""" | |
80 | args = req.context["args"] | |
81 | value = args["value"] or dt.datetime.utcnow() | |
82 | if args["unit"] == "minutes": | |
83 | delta = dt.timedelta(minutes=args["addend"]) | |
84 | else: | |
85 | delta = dt.timedelta(days=args["addend"]) | |
86 | result = value + delta | |
87 | req.context["result"] = {"result": result.isoformat()} | |
88 | ||
89 | ||
90 | app = falcon.API(middleware=[JSONTranslator()]) | |
91 | app.add_route("/", HelloResource()) | |
92 | app.add_route("/add", AdderResource()) | |
93 | app.add_route("/dateadd", DateAddResource()) |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/flask_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | import datetime as dt | |
15 | ||
16 | from flask import Flask, jsonify | |
17 | from webargs import fields, validate | |
18 | from webargs.flaskparser import use_args, use_kwargs | |
19 | ||
20 | app = Flask(__name__) | |
21 | ||
22 | hello_args = {"name": fields.Str(missing="Friend")} | |
23 | ||
24 | ||
25 | @app.route("/", methods=["GET"]) | |
26 | @use_args(hello_args) | |
27 | def index(args): | |
28 | """A welcome page.""" | |
29 | return jsonify({"message": "Welcome, {}!".format(args["name"])}) | |
30 | ||
31 | ||
32 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
33 | ||
34 | ||
35 | @app.route("/add", methods=["POST"]) | |
36 | @use_kwargs(add_args) | |
37 | def add(x, y): | |
38 | """An addition endpoint.""" | |
39 | return jsonify({"result": x + y}) | |
40 | ||
41 | ||
42 | dateadd_args = { | |
43 | "value": fields.Date(required=False), | |
44 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
45 | "unit": fields.Str(missing="days", validate=validate.OneOf(["minutes", "days"])), | |
46 | } | |
47 | ||
48 | ||
49 | @app.route("/dateadd", methods=["POST"]) | |
50 | @use_kwargs(dateadd_args) | |
51 | def dateadd(value, addend, unit): | |
52 | """A date adder endpoint.""" | |
53 | value = value or dt.datetime.utcnow() | |
54 | if unit == "minutes": | |
55 | delta = dt.timedelta(minutes=addend) | |
56 | else: | |
57 | delta = dt.timedelta(days=addend) | |
58 | result = value + delta | |
59 | return jsonify({"result": result.isoformat()}) | |
60 | ||
61 | ||
62 | # Return validation errors as JSON | |
63 | @app.errorhandler(422) | |
64 | @app.errorhandler(400) | |
65 | def handle_error(err): | |
66 | headers = err.data.get("headers", None) | |
67 | messages = err.data.get("messages", ["Invalid request."]) | |
68 | if headers: | |
69 | return jsonify({"errors": messages}), err.code, headers | |
70 | else: | |
71 | return jsonify({"errors": messages}), err.code | |
72 | ||
73 | ||
74 | if __name__ == "__main__": | |
75 | app.run(port=5001, debug=True) |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/flaskrestful_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | import datetime as dt | |
15 | ||
16 | from flask import Flask | |
17 | from flask_restful import Api, Resource | |
18 | ||
19 | from webargs import fields, validate | |
20 | from webargs.flaskparser import use_args, use_kwargs, parser, abort | |
21 | ||
22 | app = Flask(__name__) | |
23 | api = Api(app) | |
24 | ||
25 | ||
26 | class IndexResource(Resource): | |
27 | """A welcome page.""" | |
28 | ||
29 | hello_args = {"name": fields.Str(missing="Friend")} | |
30 | ||
31 | @use_args(hello_args) | |
32 | def get(self, args): | |
33 | return {"message": "Welcome, {}!".format(args["name"])} | |
34 | ||
35 | ||
36 | class AddResource(Resource): | |
37 | """An addition endpoint.""" | |
38 | ||
39 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
40 | ||
41 | @use_kwargs(add_args) | |
42 | def post(self, x, y): | |
43 | """An addition endpoint.""" | |
44 | return {"result": x + y} | |
45 | ||
46 | ||
47 | class DateAddResource(Resource): | |
48 | ||
49 | dateadd_args = { | |
50 | "value": fields.Date(required=False), | |
51 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
52 | "unit": fields.Str( | |
53 | missing="days", validate=validate.OneOf(["minutes", "days"]) | |
54 | ), | |
55 | } | |
56 | ||
57 | @use_kwargs(dateadd_args) | |
58 | def post(self, value, addend, unit): | |
59 | """A date adder endpoint.""" | |
60 | value = value or dt.datetime.utcnow() | |
61 | if unit == "minutes": | |
62 | delta = dt.timedelta(minutes=addend) | |
63 | else: | |
64 | delta = dt.timedelta(days=addend) | |
65 | result = value + delta | |
66 | return {"result": result.isoformat()} | |
67 | ||
68 | ||
69 | # This error handler is necessary for usage with Flask-RESTful | |
70 | @parser.error_handler | |
71 | def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers): | |
72 | """webargs error handler that uses Flask-RESTful's abort function to return | |
73 | a JSON error response to the client. | |
74 | """ | |
75 | abort(error_status_code, errors=err.messages) | |
76 | ||
77 | ||
78 | if __name__ == "__main__": | |
79 | api.add_resource(IndexResource, "/") | |
80 | api.add_resource(AddResource, "/add") | |
81 | api.add_resource(DateAddResource, "/dateadd") | |
82 | app.run(port=5001, debug=True) |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/pyramid_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | ||
15 | import datetime as dt | |
16 | ||
17 | from wsgiref.simple_server import make_server | |
18 | from pyramid.config import Configurator | |
19 | from pyramid.view import view_config | |
20 | from pyramid.renderers import JSON | |
21 | from webargs import fields, validate | |
22 | from webargs.pyramidparser import use_args, use_kwargs | |
23 | ||
24 | ||
25 | hello_args = {"name": fields.Str(missing="Friend")} | |
26 | ||
27 | ||
28 | @view_config(route_name="hello", request_method="GET", renderer="json") | |
29 | @use_args(hello_args) | |
30 | def index(request, args): | |
31 | """A welcome page.""" | |
32 | return {"message": "Welcome, {}!".format(args["name"])} | |
33 | ||
34 | ||
35 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
36 | ||
37 | ||
38 | @view_config(route_name="add", request_method="POST", renderer="json") | |
39 | @use_kwargs(add_args) | |
40 | def add(request, x, y): | |
41 | """An addition endpoint.""" | |
42 | return {"result": x + y} | |
43 | ||
44 | ||
45 | dateadd_args = { | |
46 | "value": fields.Date(required=False), | |
47 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
48 | "unit": fields.Str(missing="days", validate=validate.OneOf(["minutes", "days"])), | |
49 | } | |
50 | ||
51 | ||
52 | @view_config(route_name="dateadd", request_method="POST", renderer="json") | |
53 | @use_kwargs(dateadd_args) | |
54 | def dateadd(request, value, addend, unit): | |
55 | """A date adder endpoint.""" | |
56 | value = value or dt.datetime.utcnow() | |
57 | if unit == "minutes": | |
58 | delta = dt.timedelta(minutes=addend) | |
59 | else: | |
60 | delta = dt.timedelta(days=addend) | |
61 | result = value + delta | |
62 | return {"result": result} | |
63 | ||
64 | ||
65 | if __name__ == "__main__": | |
66 | config = Configurator() | |
67 | ||
68 | json_renderer = JSON() | |
69 | json_renderer.add_adapter(dt.datetime, lambda v, request: v.isoformat()) | |
70 | config.add_renderer("json", json_renderer) | |
71 | ||
72 | config.add_route("hello", "/") | |
73 | config.add_route("add", "/add") | |
74 | config.add_route("dateadd", "/dateadd") | |
75 | config.scan(__name__) | |
76 | app = config.make_wsgi_app() | |
77 | port = 5001 | |
78 | server = make_server("0.0.0.0", port, app) | |
79 | print(f"Serving on port {port}") | |
80 | server.serve_forever() |
0 | """Example implementation of using a marshmallow Schema for both request input | |
1 | and output with a `use_schema` decorator. | |
2 | Run the app: | |
3 | ||
4 | $ python examples/schema_example.py | |
5 | ||
6 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
7 | ||
8 | $ pip install httpie | |
9 | $ http GET :5001/users/ | |
10 | $ http GET :5001/users/42 | |
11 | $ http POST :5001/users/ username=brian first_name=Brian last_name=May | |
12 | $ http PATCH :5001/users/42 username=freddie | |
13 | $ http GET :5001/users/ limit==1 | |
14 | """ | |
15 | import functools | |
16 | from flask import Flask, request | |
17 | import random | |
18 | ||
19 | from marshmallow import Schema, fields, post_dump | |
20 | from webargs.flaskparser import parser, use_kwargs | |
21 | ||
22 | app = Flask(__name__) | |
23 | ||
24 | ##### Fake database and model ##### | |
25 | ||
26 | ||
27 | class Model: | |
28 | def __init__(self, **kwargs): | |
29 | self.__dict__.update(kwargs) | |
30 | ||
31 | def update(self, **kwargs): | |
32 | self.__dict__.update(kwargs) | |
33 | ||
34 | @classmethod | |
35 | def insert(cls, db, **kwargs): | |
36 | collection = db[cls.collection] | |
37 | new_id = None | |
38 | if "id" in kwargs: # for setting up fixtures | |
39 | new_id = kwargs.pop("id") | |
40 | else: # find a new id | |
41 | found_id = False | |
42 | while not found_id: | |
43 | new_id = random.randint(1, 9999) | |
44 | if new_id not in collection: | |
45 | found_id = True | |
46 | new_record = cls(id=new_id, **kwargs) | |
47 | collection[new_id] = new_record | |
48 | return new_record | |
49 | ||
50 | ||
51 | class User(Model): | |
52 | collection = "users" | |
53 | ||
54 | ||
55 | db = {"users": {}} | |
56 | ||
57 | ||
58 | ##### use_schema ##### | |
59 | ||
60 | ||
61 | def use_schema(schema_cls, list_view=False, locations=None): | |
62 | """View decorator for using a marshmallow schema to | |
63 | (1) parse a request's input and | |
64 | (2) serializing the view's output to a JSON response. | |
65 | """ | |
66 | ||
67 | def decorator(func): | |
68 | @functools.wraps(func) | |
69 | def wrapped(*args, **kwargs): | |
70 | partial = request.method != "POST" | |
71 | schema = schema_cls(partial=partial) | |
72 | use_args_wrapper = parser.use_args(schema, locations=locations) | |
73 | # Function wrapped with use_args | |
74 | func_with_args = use_args_wrapper(func) | |
75 | ret = func_with_args(*args, **kwargs) | |
76 | return schema.dump(ret, many=list_view) | |
77 | ||
78 | return wrapped | |
79 | ||
80 | return decorator | |
81 | ||
82 | ||
83 | ##### Schemas ##### | |
84 | ||
85 | ||
86 | class UserSchema(Schema): | |
87 | id = fields.Int(dump_only=True) | |
88 | username = fields.Str(required=True) | |
89 | first_name = fields.Str() | |
90 | last_name = fields.Str() | |
91 | ||
92 | @post_dump(pass_many=True) | |
93 | def wrap_with_envelope(self, data, many, **kwargs): | |
94 | return {"data": data} | |
95 | ||
96 | ||
97 | ##### Routes ##### | |
98 | ||
99 | ||
100 | @app.route("/users/<int:user_id>", methods=["GET", "PATCH"]) | |
101 | @use_schema(UserSchema) | |
102 | def user_detail(reqargs, user_id): | |
103 | user = db["users"].get(user_id) | |
104 | if not user: | |
105 | return {"message": "User not found"}, 404 | |
106 | if request.method == "PATCH" and reqargs: | |
107 | user.update(**reqargs) | |
108 | return user | |
109 | ||
110 | ||
111 | # You can add additional arguments with use_kwargs | |
112 | @app.route("/users/", methods=["GET", "POST"]) | |
113 | @use_kwargs({"limit": fields.Int(missing=10, location="query")}) | |
114 | @use_schema(UserSchema, list_view=True) | |
115 | def user_list(reqargs, limit): | |
116 | users = db["users"].values() | |
117 | if request.method == "POST": | |
118 | User.insert(db=db, **reqargs) | |
119 | return list(users)[:limit] | |
120 | ||
121 | ||
122 | # Return validation errors as JSON | |
123 | @app.errorhandler(422) | |
124 | @app.errorhandler(400) | |
125 | def handle_validation_error(err): | |
126 | exc = getattr(err, "exc", None) | |
127 | if exc: | |
128 | headers = err.data["headers"] | |
129 | messages = exc.messages | |
130 | else: | |
131 | headers = None | |
132 | messages = ["Invalid request."] | |
133 | if headers: | |
134 | return {"errors": messages}, err.code, headers | |
135 | else: | |
136 | return {"errors": messages}, err.code | |
137 | ||
138 | ||
139 | if __name__ == "__main__": | |
140 | User.insert( | |
141 | db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" | |
142 | ) | |
143 | app.run(port=5001, debug=True) |
0 | """A simple number and datetime addition JSON API. | |
1 | Run the app: | |
2 | ||
3 | $ python examples/tornado_example.py | |
4 | ||
5 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
6 | ||
7 | $ pip install httpie | |
8 | $ http GET :5001/ | |
9 | $ http GET :5001/ name==Ada | |
10 | $ http POST :5001/add x=40 y=2 | |
11 | $ http POST :5001/dateadd value=1973-04-10 addend=63 | |
12 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes | |
13 | """ | |
14 | import datetime as dt | |
15 | ||
16 | import tornado.ioloop | |
17 | from tornado.web import RequestHandler | |
18 | from webargs import fields, validate | |
19 | from webargs.tornadoparser import use_args, use_kwargs | |
20 | ||
21 | ||
22 | class BaseRequestHandler(RequestHandler): | |
23 | def write_error(self, status_code, **kwargs): | |
24 | """Write errors as JSON.""" | |
25 | self.set_header("Content-Type", "application/json") | |
26 | if "exc_info" in kwargs: | |
27 | etype, exc, traceback = kwargs["exc_info"] | |
28 | if hasattr(exc, "messages"): | |
29 | self.write({"errors": exc.messages}) | |
30 | if getattr(exc, "headers", None): | |
31 | for name, val in exc.headers.items(): | |
32 | self.set_header(name, val) | |
33 | self.finish() | |
34 | ||
35 | ||
36 | class HelloHandler(BaseRequestHandler): | |
37 | """A welcome page.""" | |
38 | ||
39 | hello_args = {"name": fields.Str(missing="Friend")} | |
40 | ||
41 | @use_args(hello_args) | |
42 | def get(self, args): | |
43 | response = {"message": "Welcome, {}!".format(args["name"])} | |
44 | self.write(response) | |
45 | ||
46 | ||
47 | class AdderHandler(BaseRequestHandler): | |
48 | """An addition endpoint.""" | |
49 | ||
50 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} | |
51 | ||
52 | @use_kwargs(add_args) | |
53 | def post(self, x, y): | |
54 | self.write({"result": x + y}) | |
55 | ||
56 | ||
57 | class DateAddHandler(BaseRequestHandler): | |
58 | """A date adder endpoint.""" | |
59 | ||
60 | dateadd_args = { | |
61 | "value": fields.Date(required=False), | |
62 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), | |
63 | "unit": fields.Str( | |
64 | missing="days", validate=validate.OneOf(["minutes", "days"]) | |
65 | ), | |
66 | } | |
67 | ||
68 | @use_kwargs(dateadd_args) | |
69 | def post(self, value, addend, unit): | |
70 | """A date adder endpoint.""" | |
71 | value = value or dt.datetime.utcnow() | |
72 | if unit == "minutes": | |
73 | delta = dt.timedelta(minutes=addend) | |
74 | else: | |
75 | delta = dt.timedelta(days=addend) | |
76 | result = value + delta | |
77 | self.write({"result": result.isoformat()}) | |
78 | ||
79 | ||
80 | if __name__ == "__main__": | |
81 | app = tornado.web.Application( | |
82 | [(r"/", HelloHandler), (r"/add", AdderHandler), (r"/dateadd", DateAddHandler)], | |
83 | debug=True, | |
84 | ) | |
85 | port = 5001 | |
86 | app.listen(port) | |
87 | print(f"Serving on port {port}") | |
88 | tornado.ioloop.IOLoop.instance().start() |
11 | 11 | |
12 | 12 | [mypy] |
13 | 13 | ignore_missing_imports = true |
14 | ||
15 | [egg_info] | |
16 | tag_build = | |
17 | tag_date = 0 | |
18 |
19 | 19 | ] |
20 | 20 | + FRAMEWORKS, |
21 | 21 | "lint": [ |
22 | "mypy==0.790", | |
23 | "flake8==3.8.4", | |
24 | "flake8-bugbear==20.11.1", | |
22 | "mypy==0.812", | |
23 | "flake8==3.9.0", | |
24 | "flake8-bugbear==21.3.2", | |
25 | 25 | "pre-commit~=2.4", |
26 | 26 | ], |
27 | "docs": ["Sphinx==3.3.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"] | |
27 | "docs": ["Sphinx==3.5.3", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"] | |
28 | 28 | + FRAMEWORKS, |
29 | 29 | } |
30 | 30 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
70 | 70 | class AIOHTTPParser(AsyncParser): |
71 | 71 | """aiohttp request argument parser.""" |
72 | 72 | |
73 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
73 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
74 | 74 | "match_info": RAISE, |
75 | 75 | "path": RAISE, |
76 | 76 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
83 | 83 | |
84 | 84 | def load_querystring(self, req, schema: Schema) -> MultiDictProxy: |
85 | 85 | """Return query params from the request as a MultiDictProxy.""" |
86 | return MultiDictProxy(req.query, schema) | |
86 | return self._makeproxy(req.query, schema) | |
87 | 87 | |
88 | 88 | async def load_form(self, req, schema: Schema) -> MultiDictProxy: |
89 | 89 | """Return form values from the request as a MultiDictProxy.""" |
90 | 90 | post_data = await req.post() |
91 | return MultiDictProxy(post_data, schema) | |
91 | return self._makeproxy(post_data, schema) | |
92 | 92 | |
93 | 93 | async def load_json_or_form( |
94 | 94 | self, req, schema: Schema |
113 | 113 | |
114 | 114 | def load_headers(self, req, schema: Schema) -> MultiDictProxy: |
115 | 115 | """Return headers from the request as a MultiDictProxy.""" |
116 | return MultiDictProxy(req.headers, schema) | |
116 | return self._makeproxy(req.headers, schema) | |
117 | 117 | |
118 | 118 | def load_cookies(self, req, schema: Schema) -> MultiDictProxy: |
119 | 119 | """Return cookies from the request as a MultiDictProxy.""" |
120 | return MultiDictProxy(req.cookies, schema) | |
120 | return self._makeproxy(req.cookies, schema) | |
121 | 121 | |
122 | 122 | def load_files(self, req, schema: Schema) -> typing.NoReturn: |
123 | 123 | raise NotImplementedError( |
18 | 18 | import bottle |
19 | 19 | |
20 | 20 | from webargs import core |
21 | from webargs.multidictproxy import MultiDictProxy | |
22 | 21 | |
23 | 22 | |
24 | 23 | class BottleParser(core.Parser): |
48 | 47 | |
49 | 48 | def load_querystring(self, req, schema): |
50 | 49 | """Return query params from the request as a MultiDictProxy.""" |
51 | return MultiDictProxy(req.query, schema) | |
50 | return self._makeproxy(req.query, schema) | |
52 | 51 | |
53 | 52 | def load_form(self, req, schema): |
54 | 53 | """Return form values from the request as a MultiDictProxy.""" |
57 | 56 | # TODO: Make this check more specific |
58 | 57 | if core.is_json(req.content_type): |
59 | 58 | return core.missing |
60 | return MultiDictProxy(req.forms, schema) | |
59 | return self._makeproxy(req.forms, schema) | |
61 | 60 | |
62 | 61 | def load_headers(self, req, schema): |
63 | 62 | """Return headers from the request as a MultiDictProxy.""" |
64 | return MultiDictProxy(req.headers, schema) | |
63 | return self._makeproxy(req.headers, schema) | |
65 | 64 | |
66 | 65 | def load_cookies(self, req, schema): |
67 | 66 | """Return cookies from the request.""" |
69 | 68 | |
70 | 69 | def load_files(self, req, schema): |
71 | 70 | """Return files from the request as a MultiDictProxy.""" |
72 | return MultiDictProxy(req.files, schema) | |
71 | return self._makeproxy(req.files, schema) | |
73 | 72 | |
74 | 73 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
75 | 74 | """Handles errors during parsing. Aborts the current request with a |
7 | 7 | from marshmallow import ValidationError |
8 | 8 | from marshmallow.utils import missing |
9 | 9 | |
10 | from webargs.fields import DelimitedList | |
10 | from webargs.multidictproxy import MultiDictProxy | |
11 | 11 | |
12 | 12 | logger = logging.getLogger(__name__) |
13 | 13 | |
14 | 14 | |
15 | 15 | __all__ = [ |
16 | 16 | "ValidationError", |
17 | "is_multiple", | |
18 | 17 | "Parser", |
19 | 18 | "missing", |
20 | 19 | "parse_json", |
54 | 53 | if obj and not _iscallable(obj): |
55 | 54 | raise ValueError(f"{obj!r} is not callable.") |
56 | 55 | return obj |
57 | ||
58 | ||
59 | def is_multiple(field: ma.fields.Field) -> bool: | |
60 | """Return whether or not `field` handles repeated/multi-value arguments.""" | |
61 | return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList) | |
62 | 56 | |
63 | 57 | |
64 | 58 | def get_mimetype(content_type: str) -> str: |
131 | 125 | DEFAULT_LOCATION: str = "json" |
132 | 126 | #: Default value to use for 'unknown' on schema load |
133 | 127 | # on a per-location basis |
134 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, str] = { | |
135 | "json": ma.RAISE, | |
136 | "form": ma.RAISE, | |
137 | "json_or_form": ma.RAISE, | |
128 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
129 | "json": None, | |
130 | "form": None, | |
131 | "json_or_form": None, | |
138 | 132 | "querystring": ma.EXCLUDE, |
139 | 133 | "query": ma.EXCLUDE, |
140 | 134 | "headers": ma.EXCLUDE, |
147 | 141 | DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS |
148 | 142 | #: Default error message for validation errors |
149 | 143 | DEFAULT_VALIDATION_MESSAGE: str = "Invalid value." |
144 | #: field types which should always be treated as if they set `is_multiple=True` | |
145 | KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List, ma.fields.Tuple] | |
150 | 146 | |
151 | 147 | #: Maps location => method name |
152 | 148 | __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = { |
174 | 170 | ) |
175 | 171 | self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS |
176 | 172 | self.unknown = unknown |
173 | ||
174 | def _makeproxy( | |
175 | self, multidict, schema: ma.Schema, cls: typing.Type = MultiDictProxy | |
176 | ): | |
177 | """Create a multidict proxy object with options from the current parser""" | |
178 | return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS)) | |
177 | 179 | |
178 | 180 | def _get_loader(self, location: str) -> typing.Callable: |
179 | 181 | """Get the loader function for the given location. |
319 | 321 | location_data = self._load_location_data( |
320 | 322 | schema=schema, req=req, location=location |
321 | 323 | ) |
322 | data = schema.load(location_data, **load_kwargs) | |
324 | preprocessed_data = self.pre_load( | |
325 | location_data, schema=schema, req=req, location=location | |
326 | ) | |
327 | data = schema.load(preprocessed_data, **load_kwargs) | |
323 | 328 | self._validate_arguments(data, validators) |
324 | 329 | except ma.exceptions.ValidationError as error: |
325 | 330 | self._on_validation_error( |
520 | 525 | self.error_callback = func |
521 | 526 | return func |
522 | 527 | |
528 | def pre_load( | |
529 | self, location_data: Mapping, *, schema: ma.Schema, req: Request, location: str | |
530 | ) -> Mapping: | |
531 | """A method of the parser which can transform data after location | |
532 | loading is done. By default it does nothing, but users can subclass | |
533 | parsers and override this method. | |
534 | """ | |
535 | return location_data | |
536 | ||
523 | 537 | def _handle_invalid_json_error( |
524 | 538 | self, |
525 | 539 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], |
17 | 17 | return HttpResponse('Hello ' + args['name']) |
18 | 18 | """ |
19 | 19 | from webargs import core |
20 | from webargs.multidictproxy import MultiDictProxy | |
21 | 20 | |
22 | 21 | |
23 | 22 | def is_json_request(req): |
47 | 46 | |
48 | 47 | def load_querystring(self, req, schema): |
49 | 48 | """Return query params from the request as a MultiDictProxy.""" |
50 | return MultiDictProxy(req.GET, schema) | |
49 | return self._makeproxy(req.GET, schema) | |
51 | 50 | |
52 | 51 | def load_form(self, req, schema): |
53 | 52 | """Return form values from the request as a MultiDictProxy.""" |
54 | return MultiDictProxy(req.POST, schema) | |
53 | return self._makeproxy(req.POST, schema) | |
55 | 54 | |
56 | 55 | def load_cookies(self, req, schema): |
57 | 56 | """Return cookies from the request.""" |
65 | 64 | |
66 | 65 | def load_files(self, req, schema): |
67 | 66 | """Return files from the request as a MultiDictProxy.""" |
68 | return MultiDictProxy(req.FILES, schema) | |
67 | return self._makeproxy(req.FILES, schema) | |
69 | 68 | |
70 | 69 | def get_request_from_view_args(self, view, args, kwargs): |
71 | 70 | # The first argument is either `self` or `request` |
5 | 5 | import marshmallow as ma |
6 | 6 | |
7 | 7 | from webargs import core |
8 | from webargs.multidictproxy import MultiDictProxy | |
9 | 8 | |
10 | 9 | HTTP_422 = "422 Unprocessable Entity" |
11 | 10 | |
96 | 95 | |
97 | 96 | def load_querystring(self, req, schema): |
98 | 97 | """Return query params from the request as a MultiDictProxy.""" |
99 | return MultiDictProxy(req.params, schema) | |
98 | return self._makeproxy(req.params, schema) | |
100 | 99 | |
101 | 100 | def load_form(self, req, schema): |
102 | 101 | """Return form values from the request as a MultiDictProxy |
108 | 107 | form = parse_form_body(req) |
109 | 108 | if form is core.missing: |
110 | 109 | return form |
111 | return MultiDictProxy(form, schema) | |
110 | return self._makeproxy(form, schema) | |
112 | 111 | |
113 | 112 | def load_media(self, req, schema): |
114 | 113 | """Return data unpacked and parsed by one of Falcon's media handlers. |
54 | 54 | """ |
55 | 55 | |
56 | 56 | delimiter: str = "," |
57 | # delimited fields set is_multiple=False for webargs.core.is_multiple | |
58 | is_multiple: bool = False | |
57 | 59 | |
58 | 60 | def _serialize(self, value, attr, obj, **kwargs): |
59 | 61 | # serializing will start with parent-class serialization, so that we correctly |
19 | 19 | uid=uid, per_page=args["per_page"] |
20 | 20 | ) |
21 | 21 | """ |
22 | import typing | |
23 | ||
22 | 24 | import flask |
23 | 25 | from werkzeug.exceptions import HTTPException |
24 | 26 | |
25 | 27 | import marshmallow as ma |
26 | 28 | |
27 | 29 | from webargs import core |
28 | from webargs.multidictproxy import MultiDictProxy | |
29 | 30 | |
30 | 31 | |
31 | 32 | def abort(http_status_code, exc=None, **kwargs): |
49 | 50 | class FlaskParser(core.Parser): |
50 | 51 | """Flask request argument parser.""" |
51 | 52 | |
52 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
53 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
53 | 54 | "view_args": ma.RAISE, |
54 | 55 | "path": ma.RAISE, |
55 | 56 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
79 | 80 | |
80 | 81 | def load_querystring(self, req, schema): |
81 | 82 | """Return query params from the request as a MultiDictProxy.""" |
82 | return MultiDictProxy(req.args, schema) | |
83 | return self._makeproxy(req.args, schema) | |
83 | 84 | |
84 | 85 | def load_form(self, req, schema): |
85 | 86 | """Return form values from the request as a MultiDictProxy.""" |
86 | return MultiDictProxy(req.form, schema) | |
87 | return self._makeproxy(req.form, schema) | |
87 | 88 | |
88 | 89 | def load_headers(self, req, schema): |
89 | 90 | """Return headers from the request as a MultiDictProxy.""" |
90 | return MultiDictProxy(req.headers, schema) | |
91 | return self._makeproxy(req.headers, schema) | |
91 | 92 | |
92 | 93 | def load_cookies(self, req, schema): |
93 | 94 | """Return cookies from the request.""" |
95 | 96 | |
96 | 97 | def load_files(self, req, schema): |
97 | 98 | """Return files from the request as a MultiDictProxy.""" |
98 | return MultiDictProxy(req.files, schema) | |
99 | return self._makeproxy(req.files, schema) | |
99 | 100 | |
100 | 101 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
101 | 102 | """Handles errors during parsing. Aborts the current HTTP request and |
0 | 0 | from collections.abc import Mapping |
1 | import typing | |
1 | 2 | |
2 | 3 | import marshmallow as ma |
3 | ||
4 | from webargs.core import missing, is_multiple | |
5 | 4 | |
6 | 5 | |
7 | 6 | class MultiDictProxy(Mapping): |
14 | 13 | In all other cases, __getitem__ proxies directly to the input multidict. |
15 | 14 | """ |
16 | 15 | |
17 | def __init__(self, multidict, schema: ma.Schema): | |
16 | def __init__( | |
17 | self, | |
18 | multidict, | |
19 | schema: ma.Schema, | |
20 | known_multi_fields: typing.Tuple[typing.Type, ...] = ( | |
21 | ma.fields.List, | |
22 | ma.fields.Tuple, | |
23 | ), | |
24 | ): | |
18 | 25 | self.data = multidict |
26 | self.known_multi_fields = known_multi_fields | |
19 | 27 | self.multiple_keys = self._collect_multiple_keys(schema) |
20 | 28 | |
21 | @staticmethod | |
22 | def _collect_multiple_keys(schema: ma.Schema): | |
29 | def _is_multiple(self, field: ma.fields.Field) -> bool: | |
30 | """Return whether or not `field` handles repeated/multi-value arguments.""" | |
31 | # fields which set `is_multiple = True/False` will have the value selected, | |
32 | # otherwise, we check for explicit criteria | |
33 | is_multiple_attr = getattr(field, "is_multiple", None) | |
34 | if is_multiple_attr is not None: | |
35 | return is_multiple_attr | |
36 | return isinstance(field, self.known_multi_fields) | |
37 | ||
38 | def _collect_multiple_keys(self, schema: ma.Schema): | |
23 | 39 | result = set() |
24 | 40 | for name, field in schema.fields.items(): |
25 | if not is_multiple(field): | |
41 | if not self._is_multiple(field): | |
26 | 42 | continue |
27 | 43 | result.add(field.data_key if field.data_key is not None else name) |
28 | 44 | return result |
29 | 45 | |
30 | 46 | def __getitem__(self, key): |
31 | val = self.data.get(key, missing) | |
32 | if val is missing or key not in self.multiple_keys: | |
47 | val = self.data.get(key, ma.missing) | |
48 | if val is ma.missing or key not in self.multiple_keys: | |
33 | 49 | return val |
34 | 50 | if hasattr(self.data, "getlist"): |
35 | 51 | return self.data.getlist(key) |
24 | 24 | server.serve_forever() |
25 | 25 | """ |
26 | 26 | import functools |
27 | import typing | |
27 | 28 | from collections.abc import Mapping |
28 | 29 | |
29 | 30 | from webob.multidict import MultiDict |
33 | 34 | |
34 | 35 | from webargs import core |
35 | 36 | from webargs.core import json |
36 | from webargs.multidictproxy import MultiDictProxy | |
37 | 37 | |
38 | 38 | |
39 | 39 | def is_json_request(req): |
43 | 43 | class PyramidParser(core.Parser): |
44 | 44 | """Pyramid request argument parser.""" |
45 | 45 | |
46 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
46 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
47 | 47 | "matchdict": ma.RAISE, |
48 | 48 | "path": ma.RAISE, |
49 | 49 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
66 | 66 | |
67 | 67 | def load_querystring(self, req, schema): |
68 | 68 | """Return query params from the request as a MultiDictProxy.""" |
69 | return MultiDictProxy(req.GET, schema) | |
69 | return self._makeproxy(req.GET, schema) | |
70 | 70 | |
71 | 71 | def load_form(self, req, schema): |
72 | 72 | """Return form values from the request as a MultiDictProxy.""" |
73 | return MultiDictProxy(req.POST, schema) | |
73 | return self._makeproxy(req.POST, schema) | |
74 | 74 | |
75 | 75 | def load_cookies(self, req, schema): |
76 | 76 | """Return cookies from the request as a MultiDictProxy.""" |
77 | return MultiDictProxy(req.cookies, schema) | |
77 | return self._makeproxy(req.cookies, schema) | |
78 | 78 | |
79 | 79 | def load_headers(self, req, schema): |
80 | 80 | """Return headers from the request as a MultiDictProxy.""" |
81 | return MultiDictProxy(req.headers, schema) | |
81 | return self._makeproxy(req.headers, schema) | |
82 | 82 | |
83 | 83 | def load_files(self, req, schema): |
84 | 84 | """Return files from the request as a MultiDictProxy.""" |
85 | 85 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) |
86 | return MultiDictProxy(MultiDict(files), schema) | |
86 | return self._makeproxy(MultiDict(files), schema) | |
87 | 87 | |
88 | 88 | def load_matchdict(self, req, schema): |
89 | 89 | """Return the request's ``matchdict`` as a MultiDictProxy.""" |
90 | return MultiDictProxy(req.matchdict, schema) | |
90 | return self._makeproxy(req.matchdict, schema) | |
91 | 91 | |
92 | 92 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
93 | 93 | """Handles errors during parsing. Aborts the current HTTP request and |
96 | 96 | |
97 | 97 | def load_querystring(self, req, schema): |
98 | 98 | """Return query params from the request as a MultiDictProxy.""" |
99 | return WebArgsTornadoMultiDictProxy(req.query_arguments, schema) | |
99 | return self._makeproxy( | |
100 | req.query_arguments, schema, cls=WebArgsTornadoMultiDictProxy | |
101 | ) | |
100 | 102 | |
101 | 103 | def load_form(self, req, schema): |
102 | 104 | """Return form values from the request as a MultiDictProxy.""" |
103 | return WebArgsTornadoMultiDictProxy(req.body_arguments, schema) | |
105 | return self._makeproxy( | |
106 | req.body_arguments, schema, cls=WebArgsTornadoMultiDictProxy | |
107 | ) | |
104 | 108 | |
105 | 109 | def load_headers(self, req, schema): |
106 | 110 | """Return headers from the request as a MultiDictProxy.""" |
107 | return WebArgsTornadoMultiDictProxy(req.headers, schema) | |
111 | return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy) | |
108 | 112 | |
109 | 113 | def load_cookies(self, req, schema): |
110 | 114 | """Return cookies from the request as a MultiDictProxy.""" |
111 | 115 | # use the specialized subclass specifically for handling Tornado |
112 | 116 | # cookies |
113 | return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema) | |
117 | return self._makeproxy( | |
118 | req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy | |
119 | ) | |
114 | 120 | |
115 | 121 | def load_files(self, req, schema): |
116 | 122 | """Return files from the request as a MultiDictProxy.""" |
117 | return WebArgsTornadoMultiDictProxy(req.files, schema) | |
123 | return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy) | |
118 | 124 | |
119 | 125 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
120 | 126 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` |
0 | Metadata-Version: 2.1 | |
1 | Name: webargs | |
2 | Version: 7.0.1 | |
3 | Summary: Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. | |
4 | Home-page: https://github.com/marshmallow-code/webargs | |
5 | Author: Steven Loria | |
6 | Author-email: [email protected] | |
7 | License: MIT | |
8 | Project-URL: Changelog, https://webargs.readthedocs.io/en/latest/changelog.html | |
9 | Project-URL: Issues, https://github.com/marshmallow-code/webargs/issues | |
10 | Project-URL: Funding, https://opencollective.com/marshmallow | |
11 | Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-marshmallow&utm_medium=pypi | |
12 | Description: ******* | |
13 | webargs | |
14 | ******* | |
15 | ||
16 | .. image:: https://badgen.net/pypi/v/webargs | |
17 | :target: https://pypi.org/project/webargs/ | |
18 | :alt: PyPI version | |
19 | ||
20 | .. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.webargs?branchName=dev | |
21 | :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=6&branchName=dev | |
22 | :alt: Build status | |
23 | ||
24 | .. image:: https://readthedocs.org/projects/webargs/badge/ | |
25 | :target: https://webargs.readthedocs.io/ | |
26 | :alt: Documentation | |
27 | ||
28 | .. image:: https://badgen.net/badge/marshmallow/3 | |
29 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html | |
30 | :alt: marshmallow 3 compatible | |
31 | ||
32 | .. image:: https://badgen.net/badge/code%20style/black/000 | |
33 | :target: https://github.com/ambv/black | |
34 | :alt: code style: black | |
35 | ||
36 | Homepage: https://webargs.readthedocs.io/ | |
37 | ||
38 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. | |
39 | ||
40 | .. code-block:: python | |
41 | ||
42 | from flask import Flask | |
43 | from webargs import fields | |
44 | from webargs.flaskparser import use_args | |
45 | ||
46 | app = Flask(__name__) | |
47 | ||
48 | ||
49 | @app.route("/") | |
50 | @use_args({"name": fields.Str(required=True)}, location="query") | |
51 | def index(args): | |
52 | return "Hello " + args["name"] | |
53 | ||
54 | ||
55 | if __name__ == "__main__": | |
56 | app.run() | |
57 | ||
58 | # curl http://localhost:5000/\?name\='World' | |
59 | # Hello World | |
60 | ||
61 | Install | |
62 | ======= | |
63 | ||
64 | :: | |
65 | ||
66 | pip install -U webargs | |
67 | ||
68 | webargs supports Python >= 3.6. | |
69 | ||
70 | ||
71 | Documentation | |
72 | ============= | |
73 | ||
74 | Full documentation is available at https://webargs.readthedocs.io/. | |
75 | ||
76 | Support webargs | |
77 | =============== | |
78 | ||
79 | webargs is maintained by a group of | |
80 | `volunteers <https://webargs.readthedocs.io/en/latest/authors.html>`_. | |
81 | If you'd like to support the future of the project, please consider | |
82 | contributing to our Open Collective: | |
83 | ||
84 | .. image:: https://opencollective.com/marshmallow/donate/button.png | |
85 | :target: https://opencollective.com/marshmallow | |
86 | :width: 200 | |
87 | :alt: Donate to our collective | |
88 | ||
89 | Professional Support | |
90 | ==================== | |
91 | ||
92 | Professionally-supported webargs is available through the | |
93 | `Tidelift Subscription <https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme>`_. | |
94 | ||
95 | Tidelift gives software development teams a single source for purchasing and maintaining their software, | |
96 | with professional-grade assurances from the experts who know it best, | |
97 | while seamlessly integrating with existing tools. [`Get professional support`_] | |
98 | ||
99 | .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme | |
100 | ||
101 | .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png | |
102 | :target: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme | |
103 | :alt: Get supported marshmallow with Tidelift | |
104 | ||
105 | Security Contact Information | |
106 | ============================ | |
107 | ||
108 | To report a security vulnerability, please use the | |
109 | `Tidelift security contact <https://tidelift.com/security>`_. | |
110 | Tidelift will coordinate the fix and disclosure. | |
111 | ||
112 | Project Links | |
113 | ============= | |
114 | ||
115 | - Docs: https://webargs.readthedocs.io/ | |
116 | - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html | |
117 | - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html | |
118 | - PyPI: https://pypi.python.org/pypi/webargs | |
119 | - Issues: https://github.com/marshmallow-code/webargs/issues | |
120 | ||
121 | ||
122 | License | |
123 | ======= | |
124 | ||
125 | MIT licensed. See the `LICENSE <https://github.com/marshmallow-code/webargs/blob/dev/LICENSE>`_ file for more details. | |
126 | ||
127 | Keywords: webargs,http,flask,django,bottle,tornado,aiohttp,request,arguments,validation,parameters,rest,api,marshmallow | |
128 | Platform: UNKNOWN | |
129 | Classifier: Development Status :: 5 - Production/Stable | |
130 | Classifier: Intended Audience :: Developers | |
131 | Classifier: License :: OSI Approved :: MIT License | |
132 | Classifier: Natural Language :: English | |
133 | Classifier: Programming Language :: Python :: 3 | |
134 | Classifier: Programming Language :: Python :: 3.6 | |
135 | Classifier: Programming Language :: Python :: 3.7 | |
136 | Classifier: Programming Language :: Python :: 3.8 | |
137 | Classifier: Programming Language :: Python :: 3.9 | |
138 | Classifier: Programming Language :: Python :: 3 :: Only | |
139 | Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content | |
140 | Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application | |
141 | Requires-Python: >=3.6 | |
142 | Provides-Extra: dev | |
143 | Provides-Extra: docs | |
144 | Provides-Extra: frameworks | |
145 | Provides-Extra: lint | |
146 | Provides-Extra: tests |
0 | AUTHORS.rst | |
1 | CHANGELOG.rst | |
2 | CONTRIBUTING.rst | |
3 | LICENSE | |
4 | MANIFEST.in | |
5 | README.rst | |
6 | pyproject.toml | |
7 | setup.cfg | |
8 | setup.py | |
9 | tox.ini | |
10 | src/webargs/__init__.py | |
11 | src/webargs/aiohttpparser.py | |
12 | src/webargs/asyncparser.py | |
13 | src/webargs/bottleparser.py | |
14 | src/webargs/core.py | |
15 | src/webargs/djangoparser.py | |
16 | src/webargs/falconparser.py | |
17 | src/webargs/fields.py | |
18 | src/webargs/flaskparser.py | |
19 | src/webargs/multidictproxy.py | |
20 | src/webargs/py.typed | |
21 | src/webargs/pyramidparser.py | |
22 | src/webargs/testing.py | |
23 | src/webargs/tornadoparser.py | |
24 | src/webargs.egg-info/PKG-INFO | |
25 | src/webargs.egg-info/SOURCES.txt | |
26 | src/webargs.egg-info/dependency_links.txt | |
27 | src/webargs.egg-info/not-zip-safe | |
28 | src/webargs.egg-info/requires.txt | |
29 | src/webargs.egg-info/top_level.txt | |
30 | tests/__init__.py | |
31 | tests/conftest.py | |
32 | tests/test_aiohttpparser.py | |
33 | tests/test_bottleparser.py | |
34 | tests/test_core.py | |
35 | tests/test_djangoparser.py | |
36 | tests/test_falconparser.py | |
37 | tests/test_flaskparser.py | |
38 | tests/test_pyramidparser.py | |
39 | tests/test_tornadoparser.py | |
40 | tests/apps/__init__.py | |
41 | tests/apps/aiohttp_app.py | |
42 | tests/apps/bottle_app.py | |
43 | tests/apps/falcon_app.py | |
44 | tests/apps/flask_app.py | |
45 | tests/apps/pyramid_app.py | |
46 | tests/apps/django_app/__init__.py | |
47 | tests/apps/django_app/manage.py | |
48 | tests/apps/django_app/base/__init__.py | |
49 | tests/apps/django_app/base/settings.py | |
50 | tests/apps/django_app/base/urls.py | |
51 | tests/apps/django_app/base/wsgi.py | |
52 | tests/apps/django_app/echo/__init__.py | |
53 | tests/apps/django_app/echo/views.py⏎ |
0 | marshmallow>=3.0.0 | |
1 | ||
2 | [dev] | |
3 | Django>=2.2.0 | |
4 | Flask>=0.12.5 | |
5 | aiohttp>=3.0.8 | |
6 | bottle>=0.12.13 | |
7 | falcon>=2.0.0 | |
8 | flake8-bugbear==21.3.2 | |
9 | flake8==3.9.0 | |
10 | mypy==0.812 | |
11 | pre-commit~=2.4 | |
12 | pyramid>=1.9.1 | |
13 | pytest | |
14 | pytest-aiohttp>=0.3.0 | |
15 | tornado>=4.5.2 | |
16 | tox | |
17 | webtest-aiohttp==2.0.0 | |
18 | webtest==2.0.35 | |
19 | ||
20 | [docs] | |
21 | Django>=2.2.0 | |
22 | Flask>=0.12.5 | |
23 | Sphinx==3.5.3 | |
24 | aiohttp>=3.0.8 | |
25 | bottle>=0.12.13 | |
26 | falcon>=2.0.0 | |
27 | pyramid>=1.9.1 | |
28 | sphinx-issues==1.2.0 | |
29 | sphinx-typlog-theme==0.8.0 | |
30 | tornado>=4.5.2 | |
31 | ||
32 | [frameworks] | |
33 | Django>=2.2.0 | |
34 | Flask>=0.12.5 | |
35 | aiohttp>=3.0.8 | |
36 | bottle>=0.12.13 | |
37 | falcon>=2.0.0 | |
38 | pyramid>=1.9.1 | |
39 | tornado>=4.5.2 | |
40 | ||
41 | [lint] | |
42 | flake8-bugbear==21.3.2 | |
43 | flake8==3.9.0 | |
44 | mypy==0.812 | |
45 | pre-commit~=2.4 | |
46 | ||
47 | [tests] | |
48 | Django>=2.2.0 | |
49 | Flask>=0.12.5 | |
50 | aiohttp>=3.0.8 | |
51 | bottle>=0.12.13 | |
52 | falcon>=2.0.0 | |
53 | pyramid>=1.9.1 | |
54 | pytest | |
55 | pytest-aiohttp>=0.3.0 | |
56 | tornado>=4.5.2 | |
57 | webtest-aiohttp==2.0.0 | |
58 | webtest==2.0.35 |
0 | webargs |
0 | 0 | import datetime |
1 | import typing | |
1 | 2 | from unittest import mock |
2 | 3 | |
3 | 4 | import pytest |
34 | 35 | """A minimal parser implementation that parses mock requests.""" |
35 | 36 | |
36 | 37 | def load_querystring(self, req, schema): |
37 | return MultiDictProxy(req.query, schema) | |
38 | return self._makeproxy(req.query, schema) | |
39 | ||
40 | def load_form(self, req, schema): | |
41 | return MultiDictProxy(req.form, schema) | |
38 | 42 | |
39 | 43 | def load_json(self, req, schema): |
40 | 44 | return req.json |
1031 | 1035 | parser.parse(args, web_request) |
1032 | 1036 | |
1033 | 1037 | |
1038 | @pytest.mark.parametrize("input_dict", multidicts) | |
1039 | @pytest.mark.parametrize( | |
1040 | "setting", | |
1041 | [ | |
1042 | "is_multiple_true", | |
1043 | "is_multiple_false", | |
1044 | "is_multiple_notset", | |
1045 | "list_field", | |
1046 | "tuple_field", | |
1047 | "added_to_known", | |
1048 | ], | |
1049 | ) | |
1050 | def test_is_multiple_detection(web_request, parser, input_dict, setting): | |
1051 | # this custom class "multiplexes" in that it can be given a single value or | |
1052 | # list of values -- a single value is treated as a string, and a list of | |
1053 | # values is treated as a list of strings | |
1054 | class CustomMultiplexingField(fields.String): | |
1055 | def _deserialize(self, value, attr, data, **kwargs): | |
1056 | if isinstance(value, str): | |
1057 | return super()._deserialize(value, attr, data, **kwargs) | |
1058 | return [ | |
1059 | self._deserialize(v, attr, data, **kwargs) | |
1060 | for v in value | |
1061 | if isinstance(v, str) | |
1062 | ] | |
1063 | ||
1064 | def _serialize(self, value, attr, **kwargs): | |
1065 | if isinstance(value, str): | |
1066 | return super()._serialize(value, attr, **kwargs) | |
1067 | return [ | |
1068 | self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str) | |
1069 | ] | |
1070 | ||
1071 | class CustomMultipleField(CustomMultiplexingField): | |
1072 | is_multiple = True | |
1073 | ||
1074 | class CustomNonMultipleField(CustomMultiplexingField): | |
1075 | is_multiple = False | |
1076 | ||
1077 | # the request's query params are the input multidict | |
1078 | web_request.query = input_dict | |
1079 | ||
1080 | # case 1: is_multiple=True | |
1081 | if setting == "is_multiple_true": | |
1082 | # the multidict should unpack to a list of strings | |
1083 | # | |
1084 | # order is not necessarily guaranteed by the multidict implementations, but | |
1085 | # both values must be present | |
1086 | args = {"foos": CustomMultipleField()} | |
1087 | result = parser.parse(args, web_request, location="query") | |
1088 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1089 | # case 2: is_multiple=False | |
1090 | elif setting == "is_multiple_false": | |
1091 | # the multidict should unpack to a string | |
1092 | # | |
1093 | # either value may be returned, depending on the multidict implementation, | |
1094 | # but not both | |
1095 | args = {"foos": CustomNonMultipleField()} | |
1096 | result = parser.parse(args, web_request, location="query") | |
1097 | assert result["foos"] in ("a", "b") | |
1098 | # case 3: is_multiple is not set | |
1099 | elif setting == "is_multiple_notset": | |
1100 | # this should be the same as is_multiple=False | |
1101 | args = {"foos": CustomMultiplexingField()} | |
1102 | result = parser.parse(args, web_request, location="query") | |
1103 | assert result["foos"] in ("a", "b") | |
1104 | # case 4: the field is a List (special case) | |
1105 | elif setting == "list_field": | |
1106 | # this should behave like the is_multiple=True case | |
1107 | args = {"foos": fields.List(fields.Str())} | |
1108 | result = parser.parse(args, web_request, location="query") | |
1109 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1110 | # case 5: the field is a Tuple (special case) | |
1111 | elif setting == "tuple_field": | |
1112 | # this should behave like the is_multiple=True case and produce a tuple | |
1113 | args = {"foos": fields.Tuple((fields.Str, fields.Str))} | |
1114 | result = parser.parse(args, web_request, location="query") | |
1115 | assert result["foos"] in (("a", "b"), ("b", "a")) | |
1116 | # case 6: the field is custom, but added to the known fields of the parser | |
1117 | elif setting == "added_to_known": | |
1118 | # if it's included in the known multifields and is_multiple is not set, behave | |
1119 | # like is_multiple=True | |
1120 | parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField) | |
1121 | args = {"foos": CustomMultiplexingField()} | |
1122 | result = parser.parse(args, web_request, location="query") | |
1123 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1124 | else: | |
1125 | raise NotImplementedError | |
1126 | ||
1127 | ||
1034 | 1128 | def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): |
1035 | 1129 | def validate(value): |
1036 | 1130 | raise ValidationError("Something went wrong.") |
1133 | 1227 | p = CustomParser() |
1134 | 1228 | ret = p.parse(argmap, web_request) |
1135 | 1229 | assert ret == {"value": "hello world"} |
1230 | ||
1231 | ||
1232 | def test_parser_pre_load(web_request): | |
1233 | class CustomParser(MockRequestParser): | |
1234 | # pre-load hook to strip whitespace from query params | |
1235 | def pre_load(self, data, *, schema, req, location): | |
1236 | if location == "query": | |
1237 | return {k: v.strip() for k, v in data.items()} | |
1238 | return data | |
1239 | ||
1240 | parser = CustomParser() | |
1241 | ||
1242 | # mock data for both query and json | |
1243 | web_request.query = web_request.json = {"value": " hello "} | |
1244 | argmap = {"value": fields.Str()} | |
1245 | ||
1246 | # data gets through for 'json' just fine | |
1247 | ret = parser.parse(argmap, web_request) | |
1248 | assert ret == {"value": " hello "} | |
1249 | ||
1250 | # but for 'query', the pre_load hook changes things | |
1251 | ret = parser.parse(argmap, web_request, location="query") | |
1252 | assert ret == {"value": "hello"} | |
1253 | ||
1254 | ||
1255 | # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give | |
1256 | # in the docs/advanced.rst examples for how to use pre_load | |
1257 | # this helps ensure that the example code is correct | |
1258 | # rather than a FlaskParser, we're working with the mock parser, but it's | |
1259 | # otherwise the same | |
1260 | def test_whitespace_stripping_parser_example(web_request): | |
1261 | def _strip_whitespace(value): | |
1262 | if isinstance(value, str): | |
1263 | value = value.strip() | |
1264 | elif isinstance(value, typing.Mapping): | |
1265 | return {k: _strip_whitespace(value[k]) for k in value} | |
1266 | elif isinstance(value, (list, tuple)): | |
1267 | return type(value)(map(_strip_whitespace, value)) | |
1268 | return value | |
1269 | ||
1270 | class WhitspaceStrippingParser(MockRequestParser): | |
1271 | def pre_load(self, location_data, *, schema, req, location): | |
1272 | if location in ("query", "form"): | |
1273 | ret = _strip_whitespace(location_data) | |
1274 | return ret | |
1275 | return location_data | |
1276 | ||
1277 | parser = WhitspaceStrippingParser() | |
1278 | ||
1279 | # mock data for query, form, and json | |
1280 | web_request.form = web_request.query = web_request.json = {"value": " hello "} | |
1281 | argmap = {"value": fields.Str()} | |
1282 | ||
1283 | # data gets through for 'json' just fine | |
1284 | ret = parser.parse(argmap, web_request) | |
1285 | assert ret == {"value": " hello "} | |
1286 | ||
1287 | # but for 'query' and 'form', the pre_load hook changes things | |
1288 | for loc in ("query", "form"): | |
1289 | ret = parser.parse(argmap, web_request, location=loc) | |
1290 | assert ret == {"value": "hello"} | |
1291 | ||
1292 | # check that it applies in the case where the field is a list type | |
1293 | # applied to an argument (logic for `tuple` is effectively the same) | |
1294 | web_request.form = web_request.query = web_request.json = { | |
1295 | "ids": [" 1", "3", " 4"], | |
1296 | "values": [" foo ", " bar"], | |
1297 | } | |
1298 | schema = Schema.from_dict( | |
1299 | {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)} | |
1300 | ) | |
1301 | for loc in ("query", "form"): | |
1302 | ret = parser.parse(schema, web_request, location=loc) | |
1303 | assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]} | |
1304 | ||
1305 | # json loading should also work even though the pre_load hook above | |
1306 | # doesn't strip whitespace from JSON data | |
1307 | # - values=[" foo ", ...] will have whitespace preserved | |
1308 | # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int | |
1309 | ret = parser.parse(schema, web_request, location="json") | |
1310 | assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]} |