Codebase list python-webargs / be387d5
Import upstream version 7.0.1+git20210322.e92f875, md5 9d8156895adfb14b6bfb66f8de2ef5ac Kali Janitor 3 years ago
58 changed file(s) with 715 addition(s) and 3703 deletion(s). Raw diff Collapse all Expand all
+0
-2
.github/FUNDING.yml less more
0 open_collective: "marshmallow"
1 tidelift: "pypi/webargs"
+0
-105
.gitignore less more
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
-27
.pre-commit-config.yaml less more
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
-11
.readthedocs.yml less more
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
5050 * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_
5151 * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_
5252 * Jason Williams `@jaswilli <https://github.com/jaswilli>`_
53 * Grey Li `@greyli <https://github.com/greyli>`_
00 Changelog
11 ---------
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.
228
329 7.0.1 (2020-12-14)
430 ******************
+0
-1
CODE_OF_CONDUCT.md less more
0 For the code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html
+0
-64
NOTICE less more
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
-44
azure-pipelines.yml less more
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
-177
docs/Makefile less more
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
-9
docs/_templates/donate.html less more
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
-11
docs/_templates/sponsors.html less more
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
-533
docs/advanced.rst less more
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
-73
docs/api.rst less more
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
-2
docs/authors.rst less more
0
1 .. include:: ../AUTHORS.rst
+0
-1
docs/changelog.rst less more
0 .. include:: ../CHANGELOG.rst
+0
-76
docs/conf.py less more
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
-1
docs/contributing.rst less more
0 .. include:: ../CONTRIBUTING.rst
+0
-6
docs/ecosystem.rst less more
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
-447
docs/framework_support.rst less more
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
-122
docs/index.rst less more
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
-23
docs/install.rst less more
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
-6
docs/license.rst less more
0
1 *******
2 License
3 *******
4
5 .. literalinclude:: ../LICENSE
+0
-242
docs/make.bat less more
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
-245
docs/quickstart.rst less more
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
-544
docs/upgrading.rst less more
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
-0
examples/__init__.py less more
(Empty file)
+0
-88
examples/aiohttp_example.py less more
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
-141
examples/annotations_example.py less more
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
-68
examples/bottle_example.py less more
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
-94
examples/falcon_example.py less more
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
-76
examples/flask_example.py less more
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
-83
examples/flaskrestful_example.py less more
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
-81
examples/pyramid_example.py less more
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
-6
examples/requirements.txt less more
0 python-dateutil==2.8.1
1 Flask
2 bottle
3 tornado
4 flask-restful
5 pyramid
+0
-144
examples/schema_example.py less more
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
-89
examples/tornado_example.py less more
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()
1111
1212 [mypy]
1313 ignore_missing_imports = true
14
15 [egg_info]
16 tag_build =
17 tag_date = 0
18
1919 ]
2020 + FRAMEWORKS,
2121 "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",
2525 "pre-commit~=2.4",
2626 ],
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"]
2828 + FRAMEWORKS,
2929 }
3030 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
7070 class AIOHTTPParser(AsyncParser):
7171 """aiohttp request argument parser."""
7272
73 DEFAULT_UNKNOWN_BY_LOCATION = {
73 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
7474 "match_info": RAISE,
7575 "path": RAISE,
7676 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
8383
8484 def load_querystring(self, req, schema: Schema) -> MultiDictProxy:
8585 """Return query params from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.query, schema)
86 return self._makeproxy(req.query, schema)
8787
8888 async def load_form(self, req, schema: Schema) -> MultiDictProxy:
8989 """Return form values from the request as a MultiDictProxy."""
9090 post_data = await req.post()
91 return MultiDictProxy(post_data, schema)
91 return self._makeproxy(post_data, schema)
9292
9393 async def load_json_or_form(
9494 self, req, schema: Schema
113113
114114 def load_headers(self, req, schema: Schema) -> MultiDictProxy:
115115 """Return headers from the request as a MultiDictProxy."""
116 return MultiDictProxy(req.headers, schema)
116 return self._makeproxy(req.headers, schema)
117117
118118 def load_cookies(self, req, schema: Schema) -> MultiDictProxy:
119119 """Return cookies from the request as a MultiDictProxy."""
120 return MultiDictProxy(req.cookies, schema)
120 return self._makeproxy(req.cookies, schema)
121121
122122 def load_files(self, req, schema: Schema) -> typing.NoReturn:
123123 raise NotImplementedError(
1818 import bottle
1919
2020 from webargs import core
21 from webargs.multidictproxy import MultiDictProxy
2221
2322
2423 class BottleParser(core.Parser):
4847
4948 def load_querystring(self, req, schema):
5049 """Return query params from the request as a MultiDictProxy."""
51 return MultiDictProxy(req.query, schema)
50 return self._makeproxy(req.query, schema)
5251
5352 def load_form(self, req, schema):
5453 """Return form values from the request as a MultiDictProxy."""
5756 # TODO: Make this check more specific
5857 if core.is_json(req.content_type):
5958 return core.missing
60 return MultiDictProxy(req.forms, schema)
59 return self._makeproxy(req.forms, schema)
6160
6261 def load_headers(self, req, schema):
6362 """Return headers from the request as a MultiDictProxy."""
64 return MultiDictProxy(req.headers, schema)
63 return self._makeproxy(req.headers, schema)
6564
6665 def load_cookies(self, req, schema):
6766 """Return cookies from the request."""
6968
7069 def load_files(self, req, schema):
7170 """Return files from the request as a MultiDictProxy."""
72 return MultiDictProxy(req.files, schema)
71 return self._makeproxy(req.files, schema)
7372
7473 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
7574 """Handles errors during parsing. Aborts the current request with a
77 from marshmallow import ValidationError
88 from marshmallow.utils import missing
99
10 from webargs.fields import DelimitedList
10 from webargs.multidictproxy import MultiDictProxy
1111
1212 logger = logging.getLogger(__name__)
1313
1414
1515 __all__ = [
1616 "ValidationError",
17 "is_multiple",
1817 "Parser",
1918 "missing",
2019 "parse_json",
5453 if obj and not _iscallable(obj):
5554 raise ValueError(f"{obj!r} is not callable.")
5655 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)
6256
6357
6458 def get_mimetype(content_type: str) -> str:
131125 DEFAULT_LOCATION: str = "json"
132126 #: Default value to use for 'unknown' on schema load
133127 # 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,
138132 "querystring": ma.EXCLUDE,
139133 "query": ma.EXCLUDE,
140134 "headers": ma.EXCLUDE,
147141 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
148142 #: Default error message for validation errors
149143 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]
150146
151147 #: Maps location => method name
152148 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
174170 )
175171 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
176172 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))
177179
178180 def _get_loader(self, location: str) -> typing.Callable:
179181 """Get the loader function for the given location.
319321 location_data = self._load_location_data(
320322 schema=schema, req=req, location=location
321323 )
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)
323328 self._validate_arguments(data, validators)
324329 except ma.exceptions.ValidationError as error:
325330 self._on_validation_error(
520525 self.error_callback = func
521526 return func
522527
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
523537 def _handle_invalid_json_error(
524538 self,
525539 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
1717 return HttpResponse('Hello ' + args['name'])
1818 """
1919 from webargs import core
20 from webargs.multidictproxy import MultiDictProxy
2120
2221
2322 def is_json_request(req):
4746
4847 def load_querystring(self, req, schema):
4948 """Return query params from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.GET, schema)
49 return self._makeproxy(req.GET, schema)
5150
5251 def load_form(self, req, schema):
5352 """Return form values from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.POST, schema)
53 return self._makeproxy(req.POST, schema)
5554
5655 def load_cookies(self, req, schema):
5756 """Return cookies from the request."""
6564
6665 def load_files(self, req, schema):
6766 """Return files from the request as a MultiDictProxy."""
68 return MultiDictProxy(req.FILES, schema)
67 return self._makeproxy(req.FILES, schema)
6968
7069 def get_request_from_view_args(self, view, args, kwargs):
7170 # The first argument is either `self` or `request`
55 import marshmallow as ma
66
77 from webargs import core
8 from webargs.multidictproxy import MultiDictProxy
98
109 HTTP_422 = "422 Unprocessable Entity"
1110
9695
9796 def load_querystring(self, req, schema):
9897 """Return query params from the request as a MultiDictProxy."""
99 return MultiDictProxy(req.params, schema)
98 return self._makeproxy(req.params, schema)
10099
101100 def load_form(self, req, schema):
102101 """Return form values from the request as a MultiDictProxy
108107 form = parse_form_body(req)
109108 if form is core.missing:
110109 return form
111 return MultiDictProxy(form, schema)
110 return self._makeproxy(form, schema)
112111
113112 def load_media(self, req, schema):
114113 """Return data unpacked and parsed by one of Falcon's media handlers.
5454 """
5555
5656 delimiter: str = ","
57 # delimited fields set is_multiple=False for webargs.core.is_multiple
58 is_multiple: bool = False
5759
5860 def _serialize(self, value, attr, obj, **kwargs):
5961 # serializing will start with parent-class serialization, so that we correctly
1919 uid=uid, per_page=args["per_page"]
2020 )
2121 """
22 import typing
23
2224 import flask
2325 from werkzeug.exceptions import HTTPException
2426
2527 import marshmallow as ma
2628
2729 from webargs import core
28 from webargs.multidictproxy import MultiDictProxy
2930
3031
3132 def abort(http_status_code, exc=None, **kwargs):
4950 class FlaskParser(core.Parser):
5051 """Flask request argument parser."""
5152
52 DEFAULT_UNKNOWN_BY_LOCATION = {
53 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
5354 "view_args": ma.RAISE,
5455 "path": ma.RAISE,
5556 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
7980
8081 def load_querystring(self, req, schema):
8182 """Return query params from the request as a MultiDictProxy."""
82 return MultiDictProxy(req.args, schema)
83 return self._makeproxy(req.args, schema)
8384
8485 def load_form(self, req, schema):
8586 """Return form values from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.form, schema)
87 return self._makeproxy(req.form, schema)
8788
8889 def load_headers(self, req, schema):
8990 """Return headers from the request as a MultiDictProxy."""
90 return MultiDictProxy(req.headers, schema)
91 return self._makeproxy(req.headers, schema)
9192
9293 def load_cookies(self, req, schema):
9394 """Return cookies from the request."""
9596
9697 def load_files(self, req, schema):
9798 """Return files from the request as a MultiDictProxy."""
98 return MultiDictProxy(req.files, schema)
99 return self._makeproxy(req.files, schema)
99100
100101 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
101102 """Handles errors during parsing. Aborts the current HTTP request and
00 from collections.abc import Mapping
1 import typing
12
23 import marshmallow as ma
3
4 from webargs.core import missing, is_multiple
54
65
76 class MultiDictProxy(Mapping):
1413 In all other cases, __getitem__ proxies directly to the input multidict.
1514 """
1615
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 ):
1825 self.data = multidict
26 self.known_multi_fields = known_multi_fields
1927 self.multiple_keys = self._collect_multiple_keys(schema)
2028
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):
2339 result = set()
2440 for name, field in schema.fields.items():
25 if not is_multiple(field):
41 if not self._is_multiple(field):
2642 continue
2743 result.add(field.data_key if field.data_key is not None else name)
2844 return result
2945
3046 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:
3349 return val
3450 if hasattr(self.data, "getlist"):
3551 return self.data.getlist(key)
2424 server.serve_forever()
2525 """
2626 import functools
27 import typing
2728 from collections.abc import Mapping
2829
2930 from webob.multidict import MultiDict
3334
3435 from webargs import core
3536 from webargs.core import json
36 from webargs.multidictproxy import MultiDictProxy
3737
3838
3939 def is_json_request(req):
4343 class PyramidParser(core.Parser):
4444 """Pyramid request argument parser."""
4545
46 DEFAULT_UNKNOWN_BY_LOCATION = {
46 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
4747 "matchdict": ma.RAISE,
4848 "path": ma.RAISE,
4949 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
6666
6767 def load_querystring(self, req, schema):
6868 """Return query params from the request as a MultiDictProxy."""
69 return MultiDictProxy(req.GET, schema)
69 return self._makeproxy(req.GET, schema)
7070
7171 def load_form(self, req, schema):
7272 """Return form values from the request as a MultiDictProxy."""
73 return MultiDictProxy(req.POST, schema)
73 return self._makeproxy(req.POST, schema)
7474
7575 def load_cookies(self, req, schema):
7676 """Return cookies from the request as a MultiDictProxy."""
77 return MultiDictProxy(req.cookies, schema)
77 return self._makeproxy(req.cookies, schema)
7878
7979 def load_headers(self, req, schema):
8080 """Return headers from the request as a MultiDictProxy."""
81 return MultiDictProxy(req.headers, schema)
81 return self._makeproxy(req.headers, schema)
8282
8383 def load_files(self, req, schema):
8484 """Return files from the request as a MultiDictProxy."""
8585 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)
8787
8888 def load_matchdict(self, req, schema):
8989 """Return the request's ``matchdict`` as a MultiDictProxy."""
90 return MultiDictProxy(req.matchdict, schema)
90 return self._makeproxy(req.matchdict, schema)
9191
9292 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
9393 """Handles errors during parsing. Aborts the current HTTP request and
9696
9797 def load_querystring(self, req, schema):
9898 """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 )
100102
101103 def load_form(self, req, schema):
102104 """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 )
104108
105109 def load_headers(self, req, schema):
106110 """Return headers from the request as a MultiDictProxy."""
107 return WebArgsTornadoMultiDictProxy(req.headers, schema)
111 return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy)
108112
109113 def load_cookies(self, req, schema):
110114 """Return cookies from the request as a MultiDictProxy."""
111115 # use the specialized subclass specifically for handling Tornado
112116 # cookies
113 return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema)
117 return self._makeproxy(
118 req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy
119 )
114120
115121 def load_files(self, req, schema):
116122 """Return files from the request as a MultiDictProxy."""
117 return WebArgsTornadoMultiDictProxy(req.files, schema)
123 return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy)
118124
119125 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
120126 """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
00 import datetime
1 import typing
12 from unittest import mock
23
34 import pytest
3435 """A minimal parser implementation that parses mock requests."""
3536
3637 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)
3842
3943 def load_json(self, req, schema):
4044 return req.json
10311035 parser.parse(args, web_request)
10321036
10331037
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
10341128 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
10351129 def validate(value):
10361130 raise ValidationError("Something went wrong.")
11331227 p = CustomParser()
11341228 ret = p.parse(argmap, web_request)
11351229 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"]}
2929 # issues in which `mypy` running on every file standalone won't catch things
3030 [testenv:mypy]
3131 deps = mypy
32 extras = frameworks
3233 commands = mypy src/
3334
3435 [testenv:docs]