├── .github
├── dependabot.yml
└── workflows
│ └── build-release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── CHANGELOG.rst
├── CONTRIBUTING.rst
├── LICENSE
├── README.rst
├── RELEASING.md
├── docs
├── Makefile
├── _static
│ └── logo.png
├── _templates
│ ├── side-primary.html
│ └── side-secondary.html
├── _themes
│ ├── LICENSE
│ ├── README
│ ├── flask
│ │ ├── layout.html
│ │ ├── relations.html
│ │ ├── static
│ │ │ └── flasky.css_t
│ │ └── theme.conf
│ ├── flask_small
│ │ ├── layout.html
│ │ ├── static
│ │ │ └── flasky.css_t
│ │ └── theme.conf
│ └── flask_theme_support.py
├── changelog.rst
├── conf.py
├── index.rst
├── license.rst
└── make.bat
├── pyproject.toml
├── src
└── flask_marshmallow
│ ├── __init__.py
│ ├── fields.py
│ ├── py.typed
│ ├── schema.py
│ ├── sqla.py
│ └── validate.py
├── tests
├── __init__.py
├── conftest.py
├── test_core.py
├── test_fields.py
├── test_io.py
├── test_sqla.py
└── test_validate.py
└── tox.ini
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "monthly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/build-release.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on:
3 | push:
4 | branches: ["dev", "*.x-line"]
5 | tags: ["*"]
6 | pull_request:
7 |
8 | jobs:
9 | tests:
10 | name: ${{ matrix.name }}
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | include:
16 | - { name: "3.9", python: "3.9", tox: py39 }
17 | - { name: "3.13", python: "3.13", tox: py313 }
18 | - { name: "lowest", python: "3.9", tox: py39-lowest }
19 | - { name: "dev", python: "3.13", tox: py313-marshmallowdev }
20 | steps:
21 | - uses: actions/checkout@v4.0.0
22 | - uses: actions/setup-python@v5
23 | with:
24 | python-version: ${{ matrix.python }}
25 | - run: python -m pip install tox
26 | - run: python -m tox -e ${{ matrix.tox }}
27 | build:
28 | name: Build package
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: actions/checkout@v4
32 | - uses: actions/setup-python@v5
33 | with:
34 | python-version: "3.13"
35 | - name: Install pypa/build
36 | run: python -m pip install build
37 | - name: Build a binary wheel and a source tarball
38 | run: python -m build
39 | - name: Install twine
40 | run: python -m pip install twine
41 | - name: Check build
42 | run: python -m twine check --strict dist/*
43 | - name: Store the distribution packages
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: python-package-distributions
47 | path: dist/
48 | # this duplicates pre-commit.ci, so only run it on tags
49 | # it guarantees that linting is passing prior to a release
50 | lint-pre-release:
51 | runs-on: ubuntu-latest
52 | if: startsWith(github.ref, 'refs/tags')
53 | steps:
54 | - uses: actions/checkout@v4.0.0
55 | - uses: actions/setup-python@v5
56 | with:
57 | python-version: "3.13"
58 | - run: python -m pip install tox
59 | - run: python -m tox -elint
60 | publish-to-pypi:
61 | name: PyPI release
62 | if: startsWith(github.ref, 'refs/tags/')
63 | needs: [build, tests, lint-pre-release]
64 | runs-on: ubuntu-latest
65 | environment:
66 | name: pypi
67 | url: https://pypi.org/p/flask-marshmallow
68 | permissions:
69 | id-token: write
70 | steps:
71 | - name: Download all the dists
72 | uses: actions/download-artifact@v4
73 | with:
74 | name: python-package-distributions
75 | path: dist/
76 | - name: Publish distribution to PyPI
77 | uses: pypa/gh-action-pypi-publish@release/v1
78 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | pip-wheel-metadata
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | htmlcov
28 | .tox
29 | nosetests.xml
30 | .cache
31 | .pytest_cache
32 |
33 | # Translations
34 | *.mo
35 |
36 | # Mr Developer
37 | .mr.developer.cfg
38 |
39 | # IDE
40 | .project
41 | .pydevproject
42 | .idea
43 |
44 | # Coverage
45 | cover
46 | .coveragerc
47 |
48 | # Sphinx
49 | docs/_build
50 | README.html
51 |
52 | *.ipynb
53 | .ipynb_checkpoints
54 |
55 | Vagrantfile
56 | .vagrant
57 |
58 | *.db
59 | *.ai
60 | .konchrc
61 | _sandbox
62 | pylintrc
63 |
64 | # Virtualenvs
65 | env
66 | venv
67 |
68 | # pyenv
69 | .python-version
70 |
71 | # pytest
72 | .pytest_cache
73 |
74 | # Other
75 | .directory
76 | *.pprof
77 |
78 | # mypy
79 | .mypy_cache/
80 | .dmypy.json
81 | dmypy.json
82 |
83 | # ruff
84 | .ruff_cache
85 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autoupdate_schedule: monthly
3 | repos:
4 | - repo: https://github.com/astral-sh/ruff-pre-commit
5 | rev: v0.11.12
6 | hooks:
7 | - id: ruff
8 | - id: ruff-format
9 | - repo: https://github.com/python-jsonschema/check-jsonschema
10 | rev: 0.33.0
11 | hooks:
12 | - id: check-github-workflows
13 | - id: check-readthedocs
14 | - repo: https://github.com/asottile/blacken-docs
15 | rev: 1.19.1
16 | hooks:
17 | - id: blacken-docs
18 | additional_dependencies: [black==24.10.0]
19 | - repo: https://github.com/pre-commit/mirrors-mypy
20 | rev: v1.16.0
21 | hooks:
22 | - id: mypy
23 | additional_dependencies: ["marshmallow>=3,<4", "Flask"]
24 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | sphinx:
3 | configuration: docs/conf.py
4 | formats:
5 | - pdf
6 | build:
7 | os: ubuntu-22.04
8 | tools:
9 | python: "3.13"
10 | python:
11 | install:
12 | - method: pip
13 | path: .
14 | extra_requirements:
15 | - docs
16 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 1.3.0 (2025-01-06)
5 | ******************
6 |
7 | Support:
8 |
9 | * Support Python 3.9-3.13 (:pr:`347`).
10 | * Support marshmallow 4.0.0 (:pr:`347`).
11 |
12 | 1.2.1 (2024-03-18)
13 | ******************
14 |
15 | Bug fixes:
16 |
17 | * Fix `File` field when it receives an empty value (:pr:`301`, :issue:`apiflask/apiflask#551`).
18 | Thanks :user:`uncle-lv`.
19 | * Fix `validate.FileSize` to handle `SpooledTemporaryFile` (:pr:`300`).
20 | Thanks again :user:`uncle-lv`.
21 |
22 | 1.2.0 (2024-02-05)
23 | ******************
24 |
25 | Features:
26 |
27 | * Performance improvement to `validate.FileSize` (:pr:`293`).
28 | Thanks :user:`uncle-lv`.
29 |
30 | 1.1.0 (2024-01-16)
31 | ******************
32 |
33 | Features:
34 |
35 | * Add type coverage (:pr:`290`).
36 |
37 | 1.0.0 (2024-01-16)
38 | ******************
39 |
40 | Features:
41 |
42 | * Add field ``fields.File``, ``validate.FileType``, and ``validate.FileSize``
43 | for deserializing uploaded files (:issue:`280`, :pr:`282`).
44 | Thanks :user:`uncle-lv` for the PR
45 | * Add field ``Config`` for serializing Flask configuration values (:issue:`280`, :pr:`281`).
46 | Thanks :user:`greyli` for the PR.
47 |
48 | Support:
49 |
50 | * Support marshmallow-sqlalchemy>=0.29.0.
51 | * Test against Python 3.12.
52 | * Drop support for Python 3.7.
53 |
54 | Other changes:
55 |
56 | * *Backwards-incompatible*: Remove ```flask_marshmallow.__version__``
57 | and ``flask_marshmallow.__version_info__`` attributes (:pr:`284`).
58 | Use feature detection or ``importlib.metadata.version("flask-marshmallow")`` instead.
59 |
60 | 0.15.0 (2023-04-05)
61 | *******************
62 |
63 | * Changes to supported software versions.
64 |
65 | * python3.6 or later and marshmallow>=3.0.0 are now required
66 | * Add support for python3.11
67 | * For ``sqlalchemy`` integration, marshmallow-sqlalchemy>=0.28.2 and
68 | flask-sqlalchemy>=3.0.0 are now required
69 |
70 | * *Backwards-incompatible*: ``URLFor`` and ``AbsoluteURLFor`` now do not accept
71 | parameters for ``flask.url_for`` as top-level parameters. They must always be
72 | passed in the ``values`` dictionary, as explained in the v0.14.0 changelog.
73 |
74 | Bug fixes:
75 |
76 | * Address distutils deprecation warning in Python 3.10 (:pr:`242`).
77 | Thanks :user:`GabrielLins64` for the PR.
78 |
79 | 0.14.0 (2020-09-27)
80 | *******************
81 |
82 | * Add ``values`` argument to ``URLFor`` and ``AbsoluteURLFor`` for passing values to ``flask.url_for``.
83 | This prevents unrelated parameters from getting passed (:issue:`52`, :issue:`67`).
84 | Thanks :user:`AlrasheedA` for the PR.
85 |
86 | Deprecation:
87 |
88 | * Passing params to ``flask.url_for`` via ``URLFor``'s and ``AbsoluteURLFor``'s constructor
89 | params is deprecated. Pass ``values`` instead.
90 |
91 | .. code-block:: python
92 |
93 | # flask-marshmallow<0.14.0
94 |
95 |
96 | class UserSchema(ma.Schema):
97 | _links = ma.Hyperlinks(
98 | {
99 | "self": ma.URLFor("user_detail", id=""),
100 | }
101 | )
102 |
103 |
104 | # flask-marshmallow>=0.14.0
105 |
106 |
107 | class UserSchema(ma.Schema):
108 | _links = ma.Hyperlinks(
109 | {
110 | "self": ma.URLFor("user_detail", values=dict(id="")),
111 | }
112 | )
113 |
114 | 0.13.0 (2020-06-07)
115 | *******************
116 |
117 | Bug fixes:
118 |
119 | * Fix compatibility with marshmallow-sqlalchemy<0.22.0 (:issue:`189`).
120 | Thanks :user:`PatrickRic` for reporting.
121 |
122 | Other changes:
123 |
124 | * Remove unused ``flask_marshmallow.sqla.SchemaOpts``.
125 |
126 | 0.12.0 (2020-04-26)
127 | *******************
128 |
129 | * *Breaking change*: ``ma.ModelSchema`` and ``ma.TableSchema`` are removed, since these are deprecated upstream.
130 |
131 | .. warning::
132 | It is highly recommended that you use the newer ``ma.SQLAlchemySchema`` and ``ma.SQLAlchemyAutoSchema`` classes
133 | instead of ``ModelSchema`` and ``TableSchema``. See the release notes for `marshmallow-sqlalchemy 0.22.0 `_
134 | for instructions on how to migrate.
135 |
136 | If you need to use ``ModelSchema`` and ``TableSchema`` for the time being, you'll need to import these directly from ``marshmallow_sqlalchemy``.
137 |
138 | .. code-block:: python
139 |
140 | from flask import Flask
141 | from flask_sqlalchemy import SQLAlchemy
142 | from flask_marshmallow import Marshmallow
143 |
144 | app = Flask(__name__)
145 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/test.db"
146 |
147 | db = SQLAlchemy(app)
148 | ma = Marshmallow(app)
149 |
150 | # flask-marshmallow<0.12.0
151 |
152 |
153 | class AuthorSchema(ma.ModelSchema):
154 | class Meta:
155 | model = Author
156 |
157 |
158 | # flask-marshmallow>=0.12.0 (recommended)
159 |
160 |
161 | class AuthorSchema(ma.SQLAlchemyAutoSchema):
162 | class Meta:
163 | model = Author
164 | load_instance = True
165 |
166 |
167 | # flask-marshmallow>=0.12.0 (not recommended)
168 |
169 | from marshmallow_sqlalchemy import ModelSchema
170 |
171 |
172 | class AuthorSchema(ModelSchema):
173 | class Meta:
174 | model = Author
175 | sql_session = db.session
176 |
177 | Bug fixes:
178 |
179 | * Fix binding Flask-SQLAlchemy's scoped session to ``ma.SQLAlchemySchema`` and ``ma.SQLAlchemyAutoSchema``.
180 | (:issue:`180`). Thanks :user:`fnalonso` for reporting.
181 |
182 | 0.11.0 (2020-02-09)
183 | *******************
184 |
185 | Features:
186 |
187 | * Add support for ``SQLAlchemySchema``, ``SQLAlchemyAutoSchema``, and ``auto_field``
188 | from marshmallow-sqlalchemy>=0.22.0 (:pr:`166`).
189 |
190 | Bug fixes:
191 |
192 | * Properly restrict marshmallow-sqlalchemy version based on Python version (:pr:`158`).
193 |
194 | Other changes:
195 |
196 | * Test against Python 3.8.
197 |
198 | 0.10.1 (2019-05-05)
199 | *******************
200 |
201 | Bug fixes:
202 |
203 | * marshmallow 3.0.0rc6 compatibility (:pr:`134`).
204 |
205 | 0.10.0 (2019-03-09)
206 | *******************
207 |
208 | Features:
209 |
210 | * Add `ma.TableSchema` (:pr:`124`).
211 | * SQLAlchemy requirements can be installed with ``pip install
212 | 'flask-marshmallow[sqlalchemy]'``.
213 |
214 |
215 | Bug fixes:
216 |
217 | * ``URLFor``, ``AbsoluteURLFor``, and ``HyperlinkRelated`` serialize to ``None`` if a passed attribute value is ``None`` (:issue:`18`, :issue:`68`, :pr:`72`).
218 | Thanks :user:`RobinRamuel`, :user:`ocervell`, and :user:`feigner` for reporting.
219 |
220 | Support:
221 |
222 | * Test against Python 3.7.
223 | * Drop support for Python 3.4. Only Python 2.7 and >=3.5 are supported.
224 |
225 | 0.9.0 (2018-04-29)
226 | ******************
227 |
228 | * Add support for marshmallow 3 beta. Thanks :user:`SBillion` for the PR.
229 | * Drop support for Python 3.3. Only Python 2.7 and >=3.4 are supported.
230 | * Updated documentation to fix example ``ma.URLFor`` target.
231 |
232 | 0.8.0 (2017-05-28)
233 | ******************
234 |
235 | * Fix compatibility with marshmallow>=3.0.
236 |
237 | Support:
238 |
239 | * *Backwards-incompatible*: Drop support for marshmallow<=2.0.0.
240 | * Test against Python 3.6.
241 |
242 | 0.7.0 (2016-06-28)
243 | ******************
244 |
245 | * ``many`` argument to ``Schema.jsonify`` defaults to value of the ``Schema`` instance's ``many`` attribute (:issue:`42`). Thanks :user:`singingwolfboy`.
246 | * Attach `HyperlinkRelated` to `Marshmallow` instances. Thanks :user:`singingwolfboy` for reporting.
247 |
248 | Support:
249 |
250 | * Upgrade to invoke>=0.13.0.
251 | * Updated documentation to reference `HyperlinkRelated` instead of `HyperlinkModelSchema`. Thanks :user:`singingwolfboy`.
252 | * Updated documentation links to readthedocs.io subdomain. Thanks :user:`adamchainz`.
253 |
254 | 0.6.2 (2015-09-16)
255 | ******************
256 |
257 | * Fix compatibility with marshmallow>=2.0.0rc2.
258 |
259 | Support:
260 |
261 | * Tested against Python 3.5.
262 |
263 | 0.6.1 (2015-09-06)
264 | ******************
265 |
266 | * Fix compatibility with marshmallow-sqlalchemy>=0.4.0 (:issue:`25`). Thanks :user:`svenstaro` for reporting.
267 |
268 | Support:
269 |
270 | * Include docs in release tarballs.
271 |
272 | 0.6.0 (2015-05-02)
273 | ******************
274 |
275 | Features:
276 |
277 | - Add Flask-SQLAlchemy/marshmallow-sqlalchemy support via the ``ModelSchema`` and ``HyperlinkModelSchema`` classes.
278 | - ``Schema.jsonify`` now takes the same arguments as ``marshmallow.Schema.dump``. Additional keyword arguments are passed to ``flask.jsonify``.
279 | - ``Hyperlinks`` field supports serializing a list of hyperlinks (:issue:`11`). Thanks :user:`royrusso` for the suggestion.
280 |
281 |
282 | Deprecation/Removal:
283 |
284 | - Remove support for ``MARSHMALLOW_DATEFORMAT`` and ``MARSHMALLOW_STRICT`` config options.
285 |
286 | Other changes:
287 |
288 | - Drop support for marshmallow<1.2.0.
289 |
290 | 0.5.1 (2015-04-27)
291 | ******************
292 |
293 | * Fix compatibility with marshmallow>=2.0.0.
294 |
295 | 0.5.0 (2015-03-29)
296 | ******************
297 |
298 | * *Backwards-incompatible*: Remove ``flask_marshmallow.SchemaOpts`` class and remove support for ``MARSHMALLOW_DATEFORMAT`` and ``MARSHMALLOW_STRICT`` (:issue:`8`). Prevents a ``RuntimeError`` when instantiating a ``Schema`` outside of a request context.
299 |
300 | 0.4.0 (2014-12-22)
301 | ******************
302 |
303 | * *Backwards-incompatible*: Rename ``URL`` and ``AbsoluteURL`` to ``URLFor`` and ``AbsoluteURLFor``, respectively, to prevent overriding marshmallow's ``URL`` field (:issue:`6`). Thanks :user:`svenstaro` for the suggestion.
304 | * Fix bug that raised an error when deserializing ``Hyperlinks`` and ``URL`` fields (:issue:`9`). Thanks :user:`raj-kesavan` for reporting.
305 |
306 | Deprecation:
307 |
308 | * ``Schema.jsonify`` is deprecated. Use ``flask.jsonify`` on the result of ``Schema.dump`` instead.
309 | * The ``MARSHMALLOW_DATEFORMAT`` and ``MARSHMALLOW_STRICT`` config values are deprecated. Use a base ``Schema`` class instead (:issue:`8`).
310 |
311 | 0.3.0 (2014-10-19)
312 | ******************
313 |
314 | * Supports marshmallow >= 1.0.0-a.
315 |
316 | 0.2.0 (2014-05-12)
317 | ******************
318 |
319 | * Implementation as a proper class-based Flask extension.
320 | * Serializer and fields classes are available from the ``Marshmallow`` object.
321 |
322 | 0.1.0 (2014-04-25)
323 | ******************
324 |
325 | * First release.
326 | * ``Hyperlinks``, ``URL``, and ``AbsoluteURL`` fields implemented.
327 | * ``Serializer#jsonify`` implemented.
328 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Contributing Guidelines
2 | =======================
3 |
4 |
5 | Questions, Feature Requests, Bug Reports, and Feedback…
6 | -------------------------------------------------------
7 |
8 | …should all be reported on the `GitHub Issue Tracker`_ .
9 |
10 | .. _`GitHub Issue Tracker`: https://github.com/marshmallow-code/flask-marshmallow/issues?state=open
11 |
12 |
13 | Contributing Code
14 | -----------------
15 |
16 | In General
17 | ++++++++++
18 |
19 | - `PEP 8`_, when sensible.
20 | - Test ruthlessly. Write docs for new features.
21 | - Even more important than Test-Driven Development--*Human-Driven Development*.
22 |
23 | .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/
24 |
25 | In Particular
26 | +++++++++++++
27 |
28 |
29 | Setting Up for Local Development
30 | ********************************
31 |
32 | 1. Fork flask-marshmallow_ on GitHub.
33 |
34 | ::
35 |
36 | $ git clone https://github.com/marshmallow-code/flask-marshmallow.git
37 | $ cd flask-marshmallow
38 |
39 | 2. Install development requirements. **It is highly recommended that you use a virtualenv.**
40 | Use the following command to install an editable version of
41 | flask-marshmallow along with its development requirements.
42 |
43 | ::
44 |
45 | # After activating your virtualenv
46 | $ pip install -e '.[dev]'
47 |
48 | 3. (Optional, but recommended) Install the pre-commit hooks, which will format and lint your git staged files.
49 |
50 | ::
51 |
52 | # The pre-commit CLI was installed above
53 | $ pre-commit install
54 |
55 | .. note::
56 |
57 | flask-marshmallow uses `black `_ for code formatting, which is only compatible with Python>=3.6. Therefore, the ``pre-commit install`` command will only work if you have the ``python3.6`` interpreter installed.
58 |
59 | Git Branch Structure
60 | ********************
61 |
62 | flask-marshmallow abides by the following branching model:
63 |
64 |
65 | ``dev``
66 | Current development branch. **New features should branch off here**.
67 |
68 | ``X.Y-line``
69 | Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.**. The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes.
70 |
71 | **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes.
72 |
73 | Pull Requests
74 | **************
75 |
76 | 1. Create a new local branch.
77 |
78 | ::
79 |
80 | # For a new feature
81 | $ git checkout -b name-of-feature dev
82 |
83 | # For a bugfix
84 | $ git checkout -b fix-something 1.2-line
85 |
86 | 2. Commit your changes. Write `good commit messages `_.
87 |
88 | ::
89 |
90 | $ git commit -m "Detailed commit message"
91 | $ git push origin name-of-feature
92 |
93 | 3. Before submitting a pull request, check the following:
94 |
95 | - If the pull request adds functionality, it is tested and the docs are updated.
96 | - You've added yourself to ``AUTHORS.rst``.
97 |
98 | 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `Travis CI `_ build must be passing before your pull request is merged.
99 |
100 | Running Tests
101 | *************
102 |
103 | To run all tests: ::
104 |
105 | $ pytest
106 |
107 | To run syntax checks: ::
108 |
109 | $ tox -e lint
110 |
111 | (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): ::
112 |
113 | $ tox
114 |
115 | Documentation
116 | *************
117 |
118 | Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_.
119 |
120 | To build the docs in "watch" mode: ::
121 |
122 | $ tox -e watch-docs
123 |
124 | Changes in the `docs/` directory will automatically trigger a rebuild.
125 |
126 | Contributing Examples
127 | *********************
128 |
129 | Have a usage example you'd like to share? Feel free to add it to the `examples `_ directory and send a pull request.
130 |
131 |
132 | .. _Sphinx: http://sphinx.pocoo.org/
133 | .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html
134 | .. _flask-marshmallow: https://github.com/marshmallow-code/flask-marshmallow
135 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Steven Loria and contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | *****************
2 | Flask-Marshmallow
3 | *****************
4 |
5 | |pypi-package| |build-status| |docs| |marshmallow-support|
6 |
7 | Flask + marshmallow for beautiful APIs
8 | ======================================
9 |
10 | Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs. It also (optionally) integrates with `Flask-SQLAlchemy `_.
11 |
12 | Get it now
13 | ----------
14 | ::
15 |
16 | pip install flask-marshmallow
17 |
18 |
19 | Create your app.
20 |
21 | .. code-block:: python
22 |
23 | from flask import Flask
24 | from flask_marshmallow import Marshmallow
25 |
26 | app = Flask(__name__)
27 | ma = Marshmallow(app)
28 |
29 | Write your models.
30 |
31 | .. code-block:: python
32 |
33 | from your_orm import Model, Column, Integer, String, DateTime
34 |
35 |
36 | class User(Model):
37 | email = Column(String)
38 | password = Column(String)
39 | date_created = Column(DateTime, auto_now_add=True)
40 |
41 |
42 | Define your output format with marshmallow.
43 |
44 | .. code-block:: python
45 |
46 |
47 | class UserSchema(ma.Schema):
48 | email = ma.Email()
49 | date_created = ma.DateTime()
50 |
51 | # Smart hyperlinking
52 | _links = ma.Hyperlinks(
53 | {
54 | "self": ma.URLFor("user_detail", values=dict(id="")),
55 | "collection": ma.URLFor("users"),
56 | }
57 | )
58 |
59 |
60 | user_schema = UserSchema()
61 | users_schema = UserSchema(many=True)
62 |
63 |
64 | Output the data in your views.
65 |
66 | .. code-block:: python
67 |
68 | @app.route("/api/users/")
69 | def users():
70 | all_users = User.all()
71 | return users_schema.dump(all_users)
72 |
73 |
74 | @app.route("/api/users/")
75 | def user_detail(id):
76 | user = User.get(id)
77 | return user_schema.dump(user)
78 |
79 |
80 | # {
81 | # "email": "fred@queen.com",
82 | # "date_created": "Fri, 25 Apr 2014 06:02:56 -0000",
83 | # "_links": {
84 | # "self": "/api/users/42",
85 | # "collection": "/api/users/"
86 | # }
87 | # }
88 |
89 |
90 | http://flask-marshmallow.readthedocs.io/
91 | ========================================
92 |
93 | Learn More
94 | ==========
95 |
96 | To learn more about marshmallow, check out its `docs `_.
97 |
98 |
99 |
100 | Project Links
101 | =============
102 |
103 | - Docs: https://flask-marshmallow.readthedocs.io/
104 | - Changelog: http://flask-marshmallow.readthedocs.io/en/latest/changelog.html
105 | - PyPI: https://pypi.org/project/flask-marshmallow/
106 | - Issues: https://github.com/marshmallow-code/flask-marshmallow/issues
107 |
108 | License
109 | =======
110 |
111 | MIT licensed. See the bundled `LICENSE `_ file for more details.
112 |
113 |
114 | .. _Flask: http://flask.pocoo.org
115 | .. _marshmallow: http://marshmallow.readthedocs.io
116 |
117 | .. |pypi-package| image:: https://badgen.net/pypi/v/flask-marshmallow
118 | :target: https://pypi.org/project/flask-marshmallow/
119 | :alt: Latest version
120 |
121 | .. |build-status| image:: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml/badge.svg
122 | :target: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml
123 | :alt: Build status
124 |
125 | .. |docs| image:: https://readthedocs.org/projects/flask-marshmallow/badge/
126 | :target: https://flask-marshmallow.readthedocs.io/
127 | :alt: Documentation
128 |
129 | .. |marshmallow-support| image:: https://badgen.net/badge/marshmallow/3,4?list=1
130 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
131 | :alt: marshmallow 3|4 compatible
132 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | 1. Bump version in `pyproject.toml` and update the changelog
4 | with today's date.
5 | 2. Commit: `git commit -m "Bump version and update changelog"`
6 | 3. Tag the commit: `git tag x.y.z`
7 | 4. Push: `git push --tags origin dev`. CI will take care of the
8 | PyPI release.
9 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(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/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marshmallow-code/flask-marshmallow/3c93f12ed1532fdc167aee33fe94763a8268d07b/docs/_static/logo.png
--------------------------------------------------------------------------------
/docs/_templates/side-primary.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 | Flask + marshmallow for beautiful APIs
13 |
14 |
15 | Useful Links
16 |
21 |
22 | Stay Informed
23 |
24 |
26 |
--------------------------------------------------------------------------------
/docs/_templates/side-secondary.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 | Flask + marshmallow for beautiful APIs
16 |
17 |
18 |
19 | Useful Links
20 |
25 |
--------------------------------------------------------------------------------
/docs/_themes/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 by Armin Ronacher.
2 |
3 | Some rights reserved.
4 |
5 | Redistribution and use in source and binary forms of the theme, with or
6 | without modification, are permitted provided that the following conditions
7 | are met:
8 |
9 | * Redistributions of source code must retain the above copyright
10 | notice, this list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above
13 | copyright notice, this list of conditions and the following
14 | disclaimer in the documentation and/or other materials provided
15 | with the distribution.
16 |
17 | * The names of the contributors may not be used to endorse or
18 | promote products derived from this software without specific
19 | prior written permission.
20 |
21 | We kindly ask you to only use these themes in an unmodified manner just
22 | for Flask and Flask-related products, not for unrelated projects. If you
23 | like the visual style and want to use it for your own projects, please
24 | consider making some larger changes to the themes (such as changing
25 | font faces, sizes, colors or margins).
26 |
27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
37 | POSSIBILITY OF SUCH DAMAGE.
38 |
--------------------------------------------------------------------------------
/docs/_themes/README:
--------------------------------------------------------------------------------
1 | Flask Sphinx Styles
2 | ===================
3 |
4 | This repository contains sphinx styles for Flask and Flask related
5 | projects. To use this style in your Sphinx documentation, follow
6 | this guide:
7 |
8 | 1. put this folder as _themes into your docs folder. Alternatively
9 | you can also use git submodules to check out the contents there.
10 | 2. add this to your conf.py:
11 |
12 | sys.path.append(os.path.abspath('_themes'))
13 | html_theme_path = ['_themes']
14 | html_theme = 'flask'
15 |
16 | The following themes exist:
17 |
18 | - 'flask' - the standard flask documentation theme for large
19 | projects
20 | - 'flask_small' - small one-page theme. Intended to be used by
21 | very small addon libraries for flask.
22 |
23 | The following options exist for the flask_small theme:
24 |
25 | [options]
26 | index_logo = '' filename of a picture in _static
27 | to be used as replacement for the
28 | h1 in the index.rst file.
29 | index_logo_height = 120px height of the index logo
30 | github_fork = '' repository name on github for the
31 | "fork me" badge
32 |
--------------------------------------------------------------------------------
/docs/_themes/flask/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "basic/layout.html" %}
2 | {%- block extrahead %}
3 | {{ super() }}
4 | {% if theme_touch_icon %}
5 |
6 | {% endif %}
7 |
8 | {% endblock %}
9 | {%- block relbar2 %}{% endblock %}
10 | {% block header %}
11 | {{ super() }}
12 | {% if pagename == 'index' %}
13 |
14 | {% endif %}
15 | {% endblock %}
16 | {%- block footer %}
17 |
21 | {% if pagename == 'index' %}
22 |
23 | {% endif %}
24 | {%- endblock %}
25 |
--------------------------------------------------------------------------------
/docs/_themes/flask/relations.html:
--------------------------------------------------------------------------------
1 | Related Topics
2 |
20 |
--------------------------------------------------------------------------------
/docs/_themes/flask/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * :copyright: Copyright 2010 by Armin Ronacher.
6 | * :license: Flask Design License, see LICENSE for details.
7 | */
8 |
9 | {% set page_width = '940px' %}
10 | {% set sidebar_width = '220px' %}
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | background-color: white;
20 | color: #000;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.document {
26 | width: {{ page_width }};
27 | margin: 30px auto 0 auto;
28 | }
29 |
30 | div.documentwrapper {
31 | float: left;
32 | width: 100%;
33 | }
34 |
35 | div.bodywrapper {
36 | margin: 0 0 0 {{ sidebar_width }};
37 | }
38 |
39 | div.sphinxsidebar {
40 | width: {{ sidebar_width }};
41 | }
42 |
43 | hr {
44 | border: 1px solid #B1B4B6;
45 | }
46 |
47 | div.body {
48 | background-color: #ffffff;
49 | color: #3E4349;
50 | padding: 0 30px 0 30px;
51 | }
52 |
53 | img.floatingflask {
54 | padding: 0 0 10px 10px;
55 | float: right;
56 | }
57 |
58 | div.footer {
59 | width: {{ page_width }};
60 | margin: 20px auto 30px auto;
61 | font-size: 14px;
62 | color: #888;
63 | text-align: right;
64 | }
65 |
66 | div.footer a {
67 | color: #888;
68 | }
69 |
70 | div.related {
71 | display: none;
72 | }
73 |
74 | div.sphinxsidebar a {
75 | color: #444;
76 | text-decoration: none;
77 | border-bottom: 1px dotted #999;
78 | }
79 |
80 | div.sphinxsidebar a:hover {
81 | border-bottom: 1px solid #999;
82 | }
83 |
84 | div.sphinxsidebar {
85 | font-size: 14px;
86 | line-height: 1.5;
87 | }
88 |
89 | div.sphinxsidebarwrapper {
90 | padding: 18px 10px;
91 | }
92 |
93 | div.sphinxsidebarwrapper p.logo {
94 | padding: 0 0 20px 0;
95 | margin: 0;
96 | text-align: center;
97 | }
98 |
99 | div.sphinxsidebar h3,
100 | div.sphinxsidebar h4 {
101 | font-family: 'Garamond', 'Georgia', serif;
102 | color: #444;
103 | font-size: 24px;
104 | font-weight: normal;
105 | margin: 0 0 5px 0;
106 | padding: 0;
107 | }
108 |
109 | div.sphinxsidebar h4 {
110 | font-size: 20px;
111 | }
112 |
113 | div.sphinxsidebar h3 a {
114 | color: #444;
115 | }
116 |
117 | div.sphinxsidebar p.logo a,
118 | div.sphinxsidebar h3 a,
119 | div.sphinxsidebar p.logo a:hover,
120 | div.sphinxsidebar h3 a:hover {
121 | border: none;
122 | }
123 |
124 | div.sphinxsidebar p {
125 | color: #555;
126 | margin: 10px 0;
127 | }
128 |
129 | div.sphinxsidebar ul {
130 | margin: 10px 0;
131 | padding: 0;
132 | color: #000;
133 | }
134 |
135 | div.sphinxsidebar input {
136 | border: 1px solid #ccc;
137 | font-family: 'Georgia', serif;
138 | font-size: 1em;
139 | }
140 |
141 | /* -- body styles ----------------------------------------------------------- */
142 |
143 | a {
144 | color: #004B6B;
145 | text-decoration: underline;
146 | }
147 |
148 | a:hover {
149 | color: #6D4100;
150 | text-decoration: underline;
151 | }
152 |
153 | div.body h1,
154 | div.body h2,
155 | div.body h3,
156 | div.body h4,
157 | div.body h5,
158 | div.body h6 {
159 | font-family: 'Garamond', 'Georgia', serif;
160 | font-weight: normal;
161 | margin: 30px 0px 10px 0px;
162 | padding: 0;
163 | }
164 |
165 | {% if theme_index_logo %}
166 | div.indexwrapper h1 {
167 | text-indent: -999999px;
168 | background: url({{ theme_index_logo }}) no-repeat center center;
169 | height: {{ theme_index_logo_height }};
170 | }
171 | {% endif %}
172 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
173 | div.body h2 { font-size: 180%; }
174 | div.body h3 { font-size: 150%; }
175 | div.body h4 { font-size: 130%; }
176 | div.body h5 { font-size: 100%; }
177 | div.body h6 { font-size: 100%; }
178 |
179 | a.headerlink {
180 | color: #ddd;
181 | padding: 0 4px;
182 | text-decoration: none;
183 | }
184 |
185 | a.headerlink:hover {
186 | color: #444;
187 | background: #eaeaea;
188 | }
189 |
190 | div.body p, div.body dd, div.body li {
191 | line-height: 1.4em;
192 | }
193 |
194 | div.admonition {
195 | background: #fafafa;
196 | margin: 20px -30px;
197 | padding: 10px 30px;
198 | border-top: 1px solid #ccc;
199 | border-bottom: 1px solid #ccc;
200 | }
201 |
202 | div.admonition tt.xref, div.admonition a tt {
203 | border-bottom: 1px solid #fafafa;
204 | }
205 |
206 | dd div.admonition {
207 | margin-left: -60px;
208 | padding-left: 60px;
209 | }
210 |
211 | div.admonition p.admonition-title {
212 | font-family: 'Garamond', 'Georgia', serif;
213 | font-weight: normal;
214 | font-size: 24px;
215 | margin: 0 0 10px 0;
216 | padding: 0;
217 | line-height: 1;
218 | }
219 |
220 | div.admonition p.last {
221 | margin-bottom: 0;
222 | }
223 |
224 | div.highlight {
225 | background-color: white;
226 | }
227 |
228 | dt:target, .highlight {
229 | background: #FAF3E8;
230 | }
231 |
232 | div.note {
233 | background-color: #eee;
234 | border: 1px solid #ccc;
235 | }
236 |
237 | div.seealso {
238 | background-color: #ffc;
239 | border: 1px solid #ff6;
240 | }
241 |
242 | div.topic {
243 | background-color: #eee;
244 | }
245 |
246 | p.admonition-title {
247 | display: inline;
248 | }
249 |
250 | p.admonition-title:after {
251 | content: ":";
252 | }
253 |
254 | pre, tt {
255 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
256 | font-size: 0.9em;
257 | }
258 |
259 | img.screenshot {
260 | }
261 |
262 | tt.descname, tt.descclassname {
263 | font-size: 0.95em;
264 | }
265 |
266 | tt.descname {
267 | padding-right: 0.08em;
268 | }
269 |
270 | img.screenshot {
271 | -moz-box-shadow: 2px 2px 4px #eee;
272 | -webkit-box-shadow: 2px 2px 4px #eee;
273 | box-shadow: 2px 2px 4px #eee;
274 | }
275 |
276 | table.docutils {
277 | border: 1px solid #888;
278 | -moz-box-shadow: 2px 2px 4px #eee;
279 | -webkit-box-shadow: 2px 2px 4px #eee;
280 | box-shadow: 2px 2px 4px #eee;
281 | }
282 |
283 | table.docutils td, table.docutils th {
284 | border: 1px solid #888;
285 | padding: 0.25em 0.7em;
286 | }
287 |
288 | table.field-list, table.footnote {
289 | border: none;
290 | -moz-box-shadow: none;
291 | -webkit-box-shadow: none;
292 | box-shadow: none;
293 | }
294 |
295 | table.footnote {
296 | margin: 15px 0;
297 | width: 100%;
298 | border: 1px solid #eee;
299 | background: #fdfdfd;
300 | font-size: 0.9em;
301 | }
302 |
303 | table.footnote + table.footnote {
304 | margin-top: -15px;
305 | border-top: none;
306 | }
307 |
308 | table.field-list th {
309 | padding: 0 0.8em 0 0;
310 | }
311 |
312 | table.field-list td {
313 | padding: 0;
314 | }
315 |
316 | table.footnote td.label {
317 | width: 0px;
318 | padding: 0.3em 0 0.3em 0.5em;
319 | }
320 |
321 | table.footnote td {
322 | padding: 0.3em 0.5em;
323 | }
324 |
325 | dl {
326 | margin: 0;
327 | padding: 0;
328 | }
329 |
330 | dl dd {
331 | margin-left: 30px;
332 | }
333 |
334 | blockquote {
335 | margin: 0 0 0 30px;
336 | padding: 0;
337 | }
338 |
339 | ul, ol {
340 | margin: 10px 0 10px 30px;
341 | padding: 0;
342 | }
343 |
344 | pre {
345 | background: #eee;
346 | padding: 7px 30px;
347 | margin: 15px -30px;
348 | line-height: 1.3em;
349 | }
350 |
351 | dl pre, blockquote pre, li pre {
352 | margin-left: -60px;
353 | padding-left: 60px;
354 | }
355 |
356 | dl dl pre {
357 | margin-left: -90px;
358 | padding-left: 90px;
359 | }
360 |
361 | tt {
362 | background-color: #ecf0f3;
363 | color: #222;
364 | /* padding: 1px 2px; */
365 | }
366 |
367 | tt.xref, a tt {
368 | background-color: #FBFBFB;
369 | border-bottom: 1px solid white;
370 | }
371 |
372 | a.reference {
373 | text-decoration: none;
374 | border-bottom: 1px dotted #004B6B;
375 | }
376 |
377 | a.reference:hover {
378 | border-bottom: 1px solid #6D4100;
379 | }
380 |
381 | a.footnote-reference {
382 | text-decoration: none;
383 | font-size: 0.7em;
384 | vertical-align: top;
385 | border-bottom: 1px dotted #004B6B;
386 | }
387 |
388 | a.footnote-reference:hover {
389 | border-bottom: 1px solid #6D4100;
390 | }
391 |
392 | a:hover tt {
393 | background: #EEE;
394 | }
395 |
396 |
397 | @media screen and (max-width: 870px) {
398 |
399 | div.sphinxsidebar {
400 | display: none;
401 | }
402 |
403 | div.document {
404 | width: 100%;
405 |
406 | }
407 |
408 | div.documentwrapper {
409 | margin-left: 0;
410 | margin-top: 0;
411 | margin-right: 0;
412 | margin-bottom: 0;
413 | }
414 |
415 | div.bodywrapper {
416 | margin-top: 0;
417 | margin-right: 0;
418 | margin-bottom: 0;
419 | margin-left: 0;
420 | }
421 |
422 | ul {
423 | margin-left: 0;
424 | }
425 |
426 | .document {
427 | width: auto;
428 | }
429 |
430 | .footer {
431 | width: auto;
432 | }
433 |
434 | .bodywrapper {
435 | margin: 0;
436 | }
437 |
438 | .footer {
439 | width: auto;
440 | }
441 |
442 | .github {
443 | display: none;
444 | }
445 |
446 |
447 |
448 | }
449 |
450 |
451 |
452 | @media screen and (max-width: 875px) {
453 |
454 | body {
455 | margin: 0;
456 | padding: 20px 30px;
457 | }
458 |
459 | div.documentwrapper {
460 | float: none;
461 | background: white;
462 | }
463 |
464 | div.sphinxsidebar {
465 | display: block;
466 | float: none;
467 | width: 102.5%;
468 | margin: 50px -30px -20px -30px;
469 | padding: 10px 20px;
470 | background: #333;
471 | color: white;
472 | }
473 |
474 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
475 | div.sphinxsidebar h3 a {
476 | color: white;
477 | }
478 |
479 | div.sphinxsidebar a {
480 | color: #aaa;
481 | }
482 |
483 | div.sphinxsidebar p.logo {
484 | display: none;
485 | }
486 |
487 | div.document {
488 | width: 100%;
489 | margin: 0;
490 | }
491 |
492 | div.related {
493 | display: block;
494 | margin: 0;
495 | padding: 10px 0 20px 0;
496 | }
497 |
498 | div.related ul,
499 | div.related ul li {
500 | margin: 0;
501 | padding: 0;
502 | }
503 |
504 | div.footer {
505 | display: none;
506 | }
507 |
508 | div.bodywrapper {
509 | margin: 0;
510 | }
511 |
512 | div.body {
513 | min-height: 0;
514 | padding: 0;
515 | }
516 |
517 | .rtd_doc_footer {
518 | display: none;
519 | }
520 |
521 | .document {
522 | width: auto;
523 | }
524 |
525 | .footer {
526 | width: auto;
527 | }
528 |
529 | .footer {
530 | width: auto;
531 | }
532 |
533 | .github {
534 | display: none;
535 | }
536 | }
537 |
538 |
539 | /* scrollbars */
540 |
541 | ::-webkit-scrollbar {
542 | width: 6px;
543 | height: 6px;
544 | }
545 |
546 | ::-webkit-scrollbar-button:start:decrement,
547 | ::-webkit-scrollbar-button:end:increment {
548 | display: block;
549 | height: 10px;
550 | }
551 |
552 | ::-webkit-scrollbar-button:vertical:increment {
553 | background-color: #fff;
554 | }
555 |
556 | ::-webkit-scrollbar-track-piece {
557 | background-color: #eee;
558 | -webkit-border-radius: 3px;
559 | }
560 |
561 | ::-webkit-scrollbar-thumb:vertical {
562 | height: 50px;
563 | background-color: #ccc;
564 | -webkit-border-radius: 3px;
565 | }
566 |
567 | ::-webkit-scrollbar-thumb:horizontal {
568 | width: 50px;
569 | background-color: #ccc;
570 | -webkit-border-radius: 3px;
571 | }
572 |
573 | /* misc. */
574 |
575 | .revsys-inline {
576 | display: none!important;
577 | }
--------------------------------------------------------------------------------
/docs/_themes/flask/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = flasky.css
4 | pygments_style = flask_theme_support.FlaskyStyle
5 |
6 | [options]
7 | index_logo = ''
8 | index_logo_height = 120px
9 | touch_icon =
10 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "basic/layout.html" %}
2 | {% block header %}
3 | {{ super() }}
4 | {% if pagename == 'index' %}
5 |
6 | {% endif %}
7 | {% endblock %}
8 | {% block footer %}
9 | {% if pagename == 'index' %}
10 |
11 | {% endif %}
12 | {% endblock %}
13 | {# do not display relbars #}
14 | {% block relbar1 %}{% endblock %}
15 | {% block relbar2 %}
16 | {% if theme_github_fork %}
17 |
19 | {% endif %}
20 | {% endblock %}
21 | {% block sidebar1 %}{% endblock %}
22 | {% block sidebar2 %}{% endblock %}
23 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- flasky theme based on nature theme.
6 | *
7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | color: #000;
20 | background: white;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.documentwrapper {
26 | float: left;
27 | width: 100%;
28 | }
29 |
30 | div.bodywrapper {
31 | margin: 40px auto 0 auto;
32 | width: 700px;
33 | }
34 |
35 | hr {
36 | border: 1px solid #B1B4B6;
37 | }
38 |
39 | div.body {
40 | background-color: #ffffff;
41 | color: #3E4349;
42 | padding: 0 30px 30px 30px;
43 | }
44 |
45 | img.floatingflask {
46 | padding: 0 0 10px 10px;
47 | float: right;
48 | }
49 |
50 | div.footer {
51 | text-align: right;
52 | color: #888;
53 | padding: 10px;
54 | font-size: 14px;
55 | width: 650px;
56 | margin: 0 auto 40px auto;
57 | }
58 |
59 | div.footer a {
60 | color: #888;
61 | text-decoration: underline;
62 | }
63 |
64 | div.related {
65 | line-height: 32px;
66 | color: #888;
67 | }
68 |
69 | div.related ul {
70 | padding: 0 0 0 10px;
71 | }
72 |
73 | div.related a {
74 | color: #444;
75 | }
76 |
77 | /* -- body styles ----------------------------------------------------------- */
78 |
79 | a {
80 | color: #004B6B;
81 | text-decoration: underline;
82 | }
83 |
84 | a:hover {
85 | color: #6D4100;
86 | text-decoration: underline;
87 | }
88 |
89 | div.body {
90 | padding-bottom: 40px; /* saved for footer */
91 | }
92 |
93 | div.body h1,
94 | div.body h2,
95 | div.body h3,
96 | div.body h4,
97 | div.body h5,
98 | div.body h6 {
99 | font-family: 'Garamond', 'Georgia', serif;
100 | font-weight: normal;
101 | margin: 30px 0px 10px 0px;
102 | padding: 0;
103 | }
104 |
105 | {% if theme_index_logo %}
106 | div.indexwrapper h1 {
107 | text-indent: -999999px;
108 | background: url({{ theme_index_logo }}) no-repeat center center;
109 | height: {{ theme_index_logo_height }};
110 | }
111 | {% endif %}
112 |
113 | div.body h2 { font-size: 180%; }
114 | div.body h3 { font-size: 150%; }
115 | div.body h4 { font-size: 130%; }
116 | div.body h5 { font-size: 100%; }
117 | div.body h6 { font-size: 100%; }
118 |
119 | a.headerlink {
120 | color: white;
121 | padding: 0 4px;
122 | text-decoration: none;
123 | }
124 |
125 | a.headerlink:hover {
126 | color: #444;
127 | background: #eaeaea;
128 | }
129 |
130 | div.body p, div.body dd, div.body li {
131 | line-height: 1.4em;
132 | }
133 |
134 | div.admonition {
135 | background: #fafafa;
136 | margin: 20px -30px;
137 | padding: 10px 30px;
138 | border-top: 1px solid #ccc;
139 | border-bottom: 1px solid #ccc;
140 | }
141 |
142 | div.admonition p.admonition-title {
143 | font-family: 'Garamond', 'Georgia', serif;
144 | font-weight: normal;
145 | font-size: 24px;
146 | margin: 0 0 10px 0;
147 | padding: 0;
148 | line-height: 1;
149 | }
150 |
151 | div.admonition p.last {
152 | margin-bottom: 0;
153 | }
154 |
155 | div.highlight{
156 | background-color: white;
157 | }
158 |
159 | dt:target, .highlight {
160 | background: #FAF3E8;
161 | }
162 |
163 | div.note {
164 | background-color: #eee;
165 | border: 1px solid #ccc;
166 | }
167 |
168 | div.seealso {
169 | background-color: #ffc;
170 | border: 1px solid #ff6;
171 | }
172 |
173 | div.topic {
174 | background-color: #eee;
175 | }
176 |
177 | div.warning {
178 | background-color: #ffe4e4;
179 | border: 1px solid #f66;
180 | }
181 |
182 | p.admonition-title {
183 | display: inline;
184 | }
185 |
186 | p.admonition-title:after {
187 | content: ":";
188 | }
189 |
190 | pre, tt {
191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
192 | font-size: 0.85em;
193 | }
194 |
195 | img.screenshot {
196 | }
197 |
198 | tt.descname, tt.descclassname {
199 | font-size: 0.95em;
200 | }
201 |
202 | tt.descname {
203 | padding-right: 0.08em;
204 | }
205 |
206 | img.screenshot {
207 | -moz-box-shadow: 2px 2px 4px #eee;
208 | -webkit-box-shadow: 2px 2px 4px #eee;
209 | box-shadow: 2px 2px 4px #eee;
210 | }
211 |
212 | table.docutils {
213 | border: 1px solid #888;
214 | -moz-box-shadow: 2px 2px 4px #eee;
215 | -webkit-box-shadow: 2px 2px 4px #eee;
216 | box-shadow: 2px 2px 4px #eee;
217 | }
218 |
219 | table.docutils td, table.docutils th {
220 | border: 1px solid #888;
221 | padding: 0.25em 0.7em;
222 | }
223 |
224 | table.field-list, table.footnote {
225 | border: none;
226 | -moz-box-shadow: none;
227 | -webkit-box-shadow: none;
228 | box-shadow: none;
229 | }
230 |
231 | table.footnote {
232 | margin: 15px 0;
233 | width: 100%;
234 | border: 1px solid #eee;
235 | }
236 |
237 | table.field-list th {
238 | padding: 0 0.8em 0 0;
239 | }
240 |
241 | table.field-list td {
242 | padding: 0;
243 | }
244 |
245 | table.footnote td {
246 | padding: 0.5em;
247 | }
248 |
249 | dl {
250 | margin: 0;
251 | padding: 0;
252 | }
253 |
254 | dl dd {
255 | margin-left: 30px;
256 | }
257 |
258 | pre {
259 | padding: 0;
260 | margin: 15px -30px;
261 | padding: 8px;
262 | line-height: 1.3em;
263 | padding: 7px 30px;
264 | background: #eee;
265 | border-radius: 2px;
266 | -moz-border-radius: 2px;
267 | -webkit-border-radius: 2px;
268 | }
269 |
270 | dl pre {
271 | margin-left: -60px;
272 | padding-left: 60px;
273 | }
274 |
275 | tt {
276 | background-color: #ecf0f3;
277 | color: #222;
278 | /* padding: 1px 2px; */
279 | }
280 |
281 | tt.xref, a tt {
282 | background-color: #FBFBFB;
283 | }
284 |
285 | a:hover tt {
286 | background: #EEE;
287 | }
288 |
289 | /* mods by sloria */
290 | /* hack to make header links consistently styled */
291 | .reference.internal em {
292 | font-style: normal;
293 | }
294 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = flasky.css
4 | nosidebar = true
5 | pygments_style = flask_theme_support.FlaskyStyle
6 |
7 | [options]
8 | index_logo = 'logo.png'
9 | index_logo_height = 120px
10 |
--------------------------------------------------------------------------------
/docs/_themes/flask_theme_support.py:
--------------------------------------------------------------------------------
1 | # flasky extensions. flasky pygments style based on tango style
2 | from pygments.style import Style
3 | from pygments.token import (
4 | Comment,
5 | Error,
6 | Generic,
7 | Keyword,
8 | Literal,
9 | Name,
10 | Number,
11 | Operator,
12 | Other,
13 | Punctuation,
14 | String,
15 | Whitespace,
16 | )
17 |
18 |
19 | class FlaskyStyle(Style):
20 | background_color = "#f8f8f8"
21 | default_style = ""
22 |
23 | styles = {
24 | # No corresponding class for the following:
25 | # Text: "", # class: ''
26 | Whitespace: "underline #f8f8f8", # class: 'w'
27 | Error: "#a40000 border:#ef2929", # class: 'err'
28 | Other: "#000000", # class 'x'
29 | Comment: "italic #8f5902", # class: 'c'
30 | Comment.Preproc: "noitalic", # class: 'cp'
31 | Keyword: "bold #004461", # class: 'k'
32 | Keyword.Constant: "bold #004461", # class: 'kc'
33 | Keyword.Declaration: "bold #004461", # class: 'kd'
34 | Keyword.Namespace: "bold #004461", # class: 'kn'
35 | Keyword.Pseudo: "bold #004461", # class: 'kp'
36 | Keyword.Reserved: "bold #004461", # class: 'kr'
37 | Keyword.Type: "bold #004461", # class: 'kt'
38 | Operator: "#582800", # class: 'o'
39 | Operator.Word: "bold #004461", # class: 'ow' - like keywords
40 | Punctuation: "bold #000000", # class: 'p'
41 | # because special names such as Name.Class, Name.Function, etc.
42 | # are not recognized as such later in the parsing, we choose them
43 | # to look the same as ordinary variables.
44 | Name: "#000000", # class: 'n'
45 | Name.Attribute: "#c4a000", # class: 'na' - to be revised
46 | Name.Builtin: "#004461", # class: 'nb'
47 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
48 | Name.Class: "#000000", # class: 'nc' - to be revised
49 | Name.Constant: "#000000", # class: 'no' - to be revised
50 | Name.Decorator: "#888", # class: 'nd' - to be revised
51 | Name.Entity: "#ce5c00", # class: 'ni'
52 | Name.Exception: "bold #cc0000", # class: 'ne'
53 | Name.Function: "#000000", # class: 'nf'
54 | Name.Property: "#000000", # class: 'py'
55 | Name.Label: "#f57900", # class: 'nl'
56 | Name.Namespace: "#000000", # class: 'nn' - to be revised
57 | Name.Other: "#000000", # class: 'nx'
58 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword
59 | Name.Variable: "#000000", # class: 'nv' - to be revised
60 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised
61 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised
62 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
63 | Number: "#990000", # class: 'm'
64 | Literal: "#000000", # class: 'l'
65 | Literal.Date: "#000000", # class: 'ld'
66 | String: "#4e9a06", # class: 's'
67 | String.Backtick: "#4e9a06", # class: 'sb'
68 | String.Char: "#4e9a06", # class: 'sc'
69 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment
70 | String.Double: "#4e9a06", # class: 's2'
71 | String.Escape: "#4e9a06", # class: 'se'
72 | String.Heredoc: "#4e9a06", # class: 'sh'
73 | String.Interpol: "#4e9a06", # class: 'si'
74 | String.Other: "#4e9a06", # class: 'sx'
75 | String.Regex: "#4e9a06", # class: 'sr'
76 | String.Single: "#4e9a06", # class: 's1'
77 | String.Symbol: "#4e9a06", # class: 'ss'
78 | Generic: "#000000", # class: 'g'
79 | Generic.Deleted: "#a40000", # class: 'gd'
80 | Generic.Emph: "italic #000000", # class: 'ge'
81 | Generic.Error: "#ef2929", # class: 'gr'
82 | Generic.Heading: "bold #000080", # class: 'gh'
83 | Generic.Inserted: "#00A000", # class: 'gi'
84 | Generic.Output: "#888", # class: 'go'
85 | Generic.Prompt: "#745334", # class: 'gp'
86 | Generic.Strong: "bold #000000", # class: 'gs'
87 | Generic.Subheading: "bold #800080", # class: 'gu'
88 | Generic.Traceback: "bold #a40000", # class: 'gt'
89 | }
90 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. _changelog:
2 |
3 | .. include:: ../CHANGELOG.rst
4 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 | import os
3 | import sys
4 |
5 | sys.path.append(os.path.abspath("_themes"))
6 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_issues"]
7 |
8 | intersphinx_mapping = {
9 | "python": ("http://python.readthedocs.io/en/latest/", None),
10 | "flask": ("http://flask.pocoo.org/docs/latest/", None),
11 | "flask-sqlalchemy": ("http://flask-sqlalchemy.pocoo.org/latest/", None),
12 | "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None),
13 | "marshmallow-sqlalchemy": (
14 | "http://marshmallow-sqlalchemy.readthedocs.io/en/latest/",
15 | None,
16 | ),
17 | }
18 |
19 | primary_domain = "py"
20 | default_role = "py:obj"
21 |
22 | issues_github_path = "marshmallow-code/flask-marshmallow"
23 |
24 | # Add any paths that contain templates here, relative to this directory.
25 | templates_path = ["_templates"]
26 |
27 | # The suffix of source filenames.
28 | source_suffix = ".rst"
29 | # The master toctree document.
30 | master_doc = "index"
31 |
32 | # General information about the project.
33 | project = "Flask-Marshmallow"
34 | copyright = "Steven Loria and contributors"
35 |
36 |
37 | version = release = importlib.metadata.version("flask-marshmallow")
38 | exclude_patterns = ["_build"]
39 | # The name of the Pygments (syntax highlighting) style to use.
40 | pygments_style = "flask_theme_support.FlaskyStyle"
41 | html_theme = "flask_small"
42 | html_theme_path = ["_themes"]
43 | html_static_path = ["_static"]
44 | html_sidebars = {
45 | "index": ["side-primary.html", "searchbox.html"],
46 | "**": ["side-secondary.html", "localtoc.html", "relations.html", "searchbox.html"],
47 | }
48 |
49 | htmlhelp_basename = "flask-marshmallowdoc"
50 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | *********************************************************
2 | Flask-Marshmallow: Flask + marshmallow for beautiful APIs
3 | *********************************************************
4 |
5 | :ref:`changelog ` //
6 | `github `_ //
7 | `pypi `_ //
8 | `issues `_
9 |
10 |
11 | Flask + marshmallow for beautiful APIs
12 | ======================================
13 |
14 | Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs. It also (optionally) integrates with `Flask-SQLAlchemy `_.
15 |
16 | Get it now
17 | ----------
18 | ::
19 |
20 | pip install flask-marshmallow
21 |
22 |
23 | Create your app.
24 |
25 | .. code-block:: python
26 |
27 | from flask import Flask
28 | from flask_marshmallow import Marshmallow
29 |
30 | app = Flask(__name__)
31 | ma = Marshmallow(app)
32 |
33 | Write your models.
34 |
35 | .. code-block:: python
36 |
37 | from your_orm import Model, Column, Integer, String, DateTime
38 |
39 |
40 | class User(Model):
41 | email = Column(String)
42 | password = Column(String)
43 | date_created = Column(DateTime, auto_now_add=True)
44 |
45 |
46 | Define your output format with marshmallow.
47 |
48 | .. code-block:: python
49 |
50 |
51 | class UserSchema(ma.Schema):
52 | email = ma.Email()
53 | date_created = ma.DateTime()
54 |
55 | # Smart hyperlinking
56 | _links = ma.Hyperlinks(
57 | {
58 | "self": ma.URLFor("user_detail", values=dict(id="")),
59 | "collection": ma.URLFor("users"),
60 | }
61 | )
62 |
63 |
64 | user_schema = UserSchema()
65 | users_schema = UserSchema(many=True)
66 |
67 |
68 | Output the data in your views.
69 |
70 | .. code-block:: python
71 |
72 | @app.route("/api/users/")
73 | def users():
74 | all_users = User.all()
75 | return users_schema.dump(all_users)
76 |
77 |
78 | @app.route("/api/users/")
79 | def user_detail(id):
80 | user = User.get(id)
81 | return user_schema.dump(user)
82 |
83 |
84 | # {
85 | # "email": "fred@queen.com",
86 | # "date_created": "Fri, 25 Apr 2014 06:02:56 -0000",
87 | # "_links": {
88 | # "self": "/api/users/42",
89 | # "collection": "/api/users/"
90 | # }
91 | # }
92 |
93 |
94 |
95 | Optional Flask-SQLAlchemy Integration
96 | -------------------------------------
97 |
98 | Flask-Marshmallow includes useful extras for integrating with `Flask-SQLAlchemy `_ and `marshmallow-sqlalchemy `_.
99 |
100 | To enable SQLAlchemy integration, make sure that both Flask-SQLAlchemy and marshmallow-sqlalchemy are installed. ::
101 |
102 | pip install -U flask-sqlalchemy marshmallow-sqlalchemy
103 |
104 | Next, initialize the `~flask_sqlalchemy.SQLAlchemy` and `~flask_marshmallow.Marshmallow` extensions, in that order.
105 |
106 | .. code-block:: python
107 |
108 | from flask import Flask
109 | from flask_sqlalchemy import SQLAlchemy
110 | from flask_marshmallow import Marshmallow
111 |
112 | app = Flask(__name__)
113 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/test.db"
114 |
115 | # Order matters: Initialize SQLAlchemy before Marshmallow
116 | db = SQLAlchemy(app)
117 | ma = Marshmallow(app)
118 |
119 | .. admonition:: Note on initialization order
120 |
121 | Flask-SQLAlchemy **must** be initialized before Flask-Marshmallow.
122 |
123 |
124 | Declare your models like normal.
125 |
126 |
127 | .. code-block:: python
128 |
129 | class Author(db.Model):
130 | id = db.Column(db.Integer, primary_key=True)
131 | name = db.Column(db.String(255))
132 |
133 |
134 | class Book(db.Model):
135 | id = db.Column(db.Integer, primary_key=True)
136 | title = db.Column(db.String(255))
137 | author_id = db.Column(db.Integer, db.ForeignKey("author.id"))
138 | author = db.relationship("Author", backref="books")
139 |
140 |
141 | Generate marshmallow `Schemas ` from your models using `~flask_marshmallow.sqla.SQLAlchemySchema` or `~flask_marshmallow.sqla.SQLAlchemyAutoSchema`.
142 |
143 | .. code-block:: python
144 |
145 | class AuthorSchema(ma.SQLAlchemySchema):
146 | class Meta:
147 | model = Author
148 |
149 | id = ma.auto_field()
150 | name = ma.auto_field()
151 | books = ma.auto_field()
152 |
153 |
154 | class BookSchema(ma.SQLAlchemyAutoSchema):
155 | class Meta:
156 | model = Book
157 | include_fk = True
158 |
159 | You can now use your schema to dump and load your ORM objects.
160 |
161 |
162 | .. code-block:: python
163 |
164 | db.create_all()
165 | author_schema = AuthorSchema()
166 | book_schema = BookSchema()
167 | author = Author(name="Chuck Paluhniuk")
168 | book = Book(title="Fight Club", author=author)
169 | db.session.add(author)
170 | db.session.add(book)
171 | db.session.commit()
172 | author_schema.dump(author)
173 | # {'id': 1, 'name': 'Chuck Paluhniuk', 'books': [1]}
174 |
175 |
176 | `~flask_marshmallow.sqla.SQLAlchemySchema` is nearly identical in API to `marshmallow_sqlalchemy.SQLAlchemySchema` with the following exceptions:
177 |
178 | - By default, `~flask_marshmallow.sqla.SQLAlchemySchema` uses the scoped session created by Flask-SQLAlchemy.
179 | - `~flask_marshmallow.sqla.SQLAlchemySchema` subclasses `flask_marshmallow.Schema`, so it includes the `~flask_marshmallow.Schema.jsonify` method.
180 |
181 | Note: By default, Flask's `jsonify` method sorts the list of keys and returns consistent results to ensure that external HTTP caches aren't trashed. As a side effect, this will override `ordered=True `_
182 | in the SQLAlchemySchema's `class Meta` (if you set it). To disable this, set `JSON_SORT_KEYS=False` in your Flask app config. In production it's recommended to let `jsonify` sort the keys and not set `ordered=True` in your `~flask_marshmallow.sqla.SQLAlchemySchema` in order to minimize generation time and maximize cacheability of the results.
183 |
184 | You can also use `ma.HyperlinkRelated ` fields if you want relationships to be represented by hyperlinks rather than primary keys.
185 |
186 |
187 | .. code-block:: python
188 |
189 | class BookSchema(ma.SQLAlchemyAutoSchema):
190 | class Meta:
191 | model = Book
192 |
193 | author = ma.HyperlinkRelated("author_detail")
194 |
195 | .. code-block:: python
196 |
197 | with app.test_request_context():
198 | print(book_schema.dump(book))
199 | # {'id': 1, 'title': 'Fight Club', 'author': '/authors/1'}
200 |
201 | The first argument to the `~flask_marshmallow.sqla.HyperlinkRelated` constructor is the name of the view used to generate the URL, just as you would pass it to the `~flask.url_for` function. If your models and views use the ``id`` attribute
202 | as a primary key, you're done; otherwise, you must specify the name of the
203 | attribute used as the primary key.
204 |
205 | To represent a one-to-many relationship, wrap the `~flask_marshmallow.sqla.HyperlinkRelated` instance in a `marshmallow.fields.List` field, like this:
206 |
207 | .. code-block:: python
208 |
209 | class AuthorSchema(ma.SQLAlchemyAutoSchema):
210 | class Meta:
211 | model = Author
212 |
213 | books = ma.List(ma.HyperlinkRelated("book_detail"))
214 |
215 | .. code-block:: python
216 |
217 | with app.test_request_context():
218 | print(author_schema.dump(author))
219 | # {'id': 1, 'name': 'Chuck Paluhniuk', 'books': ['/books/1']}
220 |
221 |
222 | API
223 | ===
224 |
225 | .. automodule:: flask_marshmallow
226 | :members:
227 |
228 | .. automodule:: flask_marshmallow.fields
229 | :members:
230 |
231 | .. automodule:: flask_marshmallow.validate
232 | :members:
233 |
234 | .. automodule:: flask_marshmallow.sqla
235 | :members:
236 |
237 |
238 | Useful Links
239 | ============
240 |
241 | - `Flask docs`_
242 | - `marshmallow docs`_
243 |
244 | .. _marshmallow docs: http://marshmallow.readthedocs.io
245 |
246 | .. _Flask docs: http://flask.pocoo.org/docs/
247 |
248 | Project Info
249 | ============
250 |
251 | .. toctree::
252 | :maxdepth: 1
253 |
254 | license
255 | changelog
256 |
257 |
258 | .. _marshmallow: http://marshmallow.readthedocs.io
259 |
260 | .. _Flask: http://flask.pocoo.org
261 |
--------------------------------------------------------------------------------
/docs/license.rst:
--------------------------------------------------------------------------------
1 | *******
2 | License
3 | *******
4 |
5 | .. literalinclude:: ../LICENSE
6 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | goto end
41 | )
42 |
43 | if "%1" == "clean" (
44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
45 | del /q /s %BUILDDIR%\*
46 | goto end
47 | )
48 |
49 |
50 | %SPHINXBUILD% 2> nul
51 | if errorlevel 9009 (
52 | echo.
53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
54 | echo.installed, then set the SPHINXBUILD environment variable to point
55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
56 | echo.may add the Sphinx directory to PATH.
57 | echo.
58 | echo.If you don't have Sphinx installed, grab it from
59 | echo.http://sphinx-doc.org/
60 | exit /b 1
61 | )
62 |
63 | if "%1" == "html" (
64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
68 | goto end
69 | )
70 |
71 | if "%1" == "dirhtml" (
72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
76 | goto end
77 | )
78 |
79 | if "%1" == "singlehtml" (
80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
84 | goto end
85 | )
86 |
87 | if "%1" == "pickle" (
88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can process the pickle files.
92 | goto end
93 | )
94 |
95 | if "%1" == "json" (
96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished; now you can process the JSON files.
100 | goto end
101 | )
102 |
103 | if "%1" == "htmlhelp" (
104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can run HTML Help Workshop with the ^
108 | .hhp project file in %BUILDDIR%/htmlhelp.
109 | goto end
110 | )
111 |
112 | if "%1" == "qthelp" (
113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
117 | .qhcp project file in %BUILDDIR%/qthelp, like this:
118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc
121 | goto end
122 | )
123 |
124 | if "%1" == "devhelp" (
125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished.
129 | goto end
130 | )
131 |
132 | if "%1" == "epub" (
133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
137 | goto end
138 | )
139 |
140 | if "%1" == "latex" (
141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
145 | goto end
146 | )
147 |
148 | if "%1" == "latexpdf" (
149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
150 | cd %BUILDDIR%/latex
151 | make all-pdf
152 | cd %BUILDDIR%/..
153 | echo.
154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
155 | goto end
156 | )
157 |
158 | if "%1" == "latexpdfja" (
159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
160 | cd %BUILDDIR%/latex
161 | make all-pdf-ja
162 | cd %BUILDDIR%/..
163 | echo.
164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
165 | goto end
166 | )
167 |
168 | if "%1" == "text" (
169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
170 | if errorlevel 1 exit /b 1
171 | echo.
172 | echo.Build finished. The text files are in %BUILDDIR%/text.
173 | goto end
174 | )
175 |
176 | if "%1" == "man" (
177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
178 | if errorlevel 1 exit /b 1
179 | echo.
180 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
181 | goto end
182 | )
183 |
184 | if "%1" == "texinfo" (
185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
189 | goto end
190 | )
191 |
192 | if "%1" == "gettext" (
193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
197 | goto end
198 | )
199 |
200 | if "%1" == "changes" (
201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.The overview file is in %BUILDDIR%/changes.
205 | goto end
206 | )
207 |
208 | if "%1" == "linkcheck" (
209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Link check complete; look for any errors in the above output ^
213 | or in %BUILDDIR%/linkcheck/output.txt.
214 | goto end
215 | )
216 |
217 | if "%1" == "doctest" (
218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.Testing of doctests in the sources finished, look at the ^
222 | results in %BUILDDIR%/doctest/output.txt.
223 | goto end
224 | )
225 |
226 | if "%1" == "xml" (
227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
228 | if errorlevel 1 exit /b 1
229 | echo.
230 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
231 | goto end
232 | )
233 |
234 | if "%1" == "pseudoxml" (
235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
239 | goto end
240 | )
241 |
242 | :end
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "flask-marshmallow"
3 | version = "1.3.0"
4 | description = "Flask + marshmallow for beautiful APIs"
5 | readme = "README.rst"
6 | license = { file = "LICENSE" }
7 | maintainers = [
8 | { name = "Steven Loria", email = "sloria1@gmail.com" },
9 | { name = "Stephen Rosen", email = "sirosen0@gmail.com" },
10 | ]
11 | classifiers = [
12 | "Environment :: Web Environment",
13 | "Intended Audience :: Developers",
14 | "License :: OSI Approved :: MIT License",
15 | "Natural Language :: English",
16 | "Programming Language :: Python :: 3",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: 3.10",
19 | "Programming Language :: Python :: 3.11",
20 | "Programming Language :: Python :: 3.12",
21 | "Programming Language :: Python :: 3.13",
22 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
23 | ]
24 | requires-python = ">=3.9"
25 | dependencies = ["Flask>=2.2", "marshmallow>=3.0.0"]
26 |
27 | [project.urls]
28 | Issues = "https://github.com/marshmallow-code/flask-marshmallow/issues"
29 | Funding = "https://opencollective.com/marshmallow"
30 |
31 | [project.optional-dependencies]
32 | docs = [
33 | "marshmallow-sqlalchemy>=0.19.0",
34 | "Sphinx==8.2.3",
35 | "sphinx-issues==5.0.1",
36 | ]
37 | tests = ["flask-marshmallow[sqlalchemy]", "pytest"]
38 | dev = ["flask-marshmallow[tests]", "tox", "pre-commit>=3.5,<5.0"]
39 | sqlalchemy = ["flask-sqlalchemy>=3.0.0", "marshmallow-sqlalchemy>=0.29.0"]
40 |
41 | [build-system]
42 | requires = ["flit_core<4"]
43 | build-backend = "flit_core.buildapi"
44 |
45 | [tool.flit.sdist]
46 | include = ["docs/", "tests/", "CHANGELOG.rst", "CONTRIBUTING.rst", "tox.ini"]
47 | exclude = ["docs/_build/"]
48 |
49 | [tool.ruff]
50 | src = ["src"]
51 | fix = true
52 | show-fixes = true
53 | output-format = "full"
54 |
55 | [tool.ruff.format]
56 | docstring-code-format = true
57 |
58 | [tool.ruff.lint]
59 | select = [
60 | "B", # flake8-bugbear
61 | "E", # pycodestyle error
62 | "F", # pyflakes
63 | "I", # isort
64 | "UP", # pyupgrade
65 | "W", # pycodestyle warning
66 | ]
67 |
68 | [tool.pytest.ini_options]
69 | filterwarnings = [
70 | "error",
71 | "ignore:distutils Version classes are deprecated\\. Use packaging.version instead\\.:DeprecationWarning:marshmallow",
72 | ]
73 |
--------------------------------------------------------------------------------
/src/flask_marshmallow/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | flask_marshmallow
3 | ~~~~~~~~~~~~~~~~~
4 |
5 | Integrates the marshmallow serialization/deserialization library
6 | with your Flask application.
7 | """
8 |
9 | import typing
10 | import warnings
11 |
12 | from marshmallow import exceptions
13 |
14 | try:
15 | # Available in marshmallow 3 only
16 | from marshmallow import pprint # noqa: F401
17 | except ImportError:
18 | _has_pprint = False
19 | else:
20 | _has_pprint = True
21 | from marshmallow import fields as base_fields
22 |
23 | from . import fields
24 | from .schema import Schema
25 |
26 | if typing.TYPE_CHECKING:
27 | from flask import Flask
28 |
29 | has_sqla = False
30 | try:
31 | import flask_sqlalchemy # noqa: F401
32 | except ImportError:
33 | has_sqla = False
34 | else:
35 | try:
36 | from . import sqla
37 | except ImportError:
38 | warnings.warn(
39 | "Flask-SQLAlchemy integration requires "
40 | "marshmallow-sqlalchemy to be installed.",
41 | stacklevel=2,
42 | )
43 | else:
44 | has_sqla = True
45 |
46 | __all__ = [
47 | "EXTENSION_NAME",
48 | "Marshmallow",
49 | "Schema",
50 | "fields",
51 | "exceptions",
52 | ]
53 | if _has_pprint:
54 | __all__.append("pprint")
55 |
56 | EXTENSION_NAME = "flask-marshmallow"
57 |
58 |
59 | def _attach_fields(obj):
60 | """Attach all the marshmallow fields classes to ``obj``, including
61 | Flask-Marshmallow's custom fields.
62 | """
63 | for attr in base_fields.__all__:
64 | if not hasattr(obj, attr):
65 | setattr(obj, attr, getattr(base_fields, attr))
66 | for attr in fields.__all__:
67 | setattr(obj, attr, getattr(fields, attr))
68 |
69 |
70 | class Marshmallow:
71 | """Wrapper class that integrates Marshmallow with a Flask application.
72 |
73 | To use it, instantiate with an application::
74 |
75 | from flask import Flask
76 |
77 | app = Flask(__name__)
78 | ma = Marshmallow(app)
79 |
80 | The object provides access to the :class:`Schema` class,
81 | all fields in :mod:`marshmallow.fields`, as well as the Flask-specific
82 | fields in :mod:`flask_marshmallow.fields`.
83 |
84 | You can declare schema like so::
85 |
86 | class BookSchema(ma.Schema):
87 | id = ma.Integer(dump_only=True)
88 | title = ma.String(required=True)
89 | author = ma.Nested(AuthorSchema)
90 |
91 | links = ma.Hyperlinks(
92 | {
93 | "self": ma.URLFor("book_detail", values=dict(id="")),
94 | "collection": ma.URLFor("book_list"),
95 | }
96 | )
97 |
98 |
99 | In order to integrate with Flask-SQLAlchemy, this extension must be initialized
100 | *after* `flask_sqlalchemy.SQLAlchemy`. ::
101 |
102 | db = SQLAlchemy(app)
103 | ma = Marshmallow(app)
104 |
105 | This gives you access to `ma.SQLAlchemySchema` and `ma.SQLAlchemyAutoSchema`, which
106 | generate marshmallow `~marshmallow.Schema` classes
107 | based on the passed in model or table. ::
108 |
109 | class AuthorSchema(ma.SQLAlchemyAutoSchema):
110 | class Meta:
111 | model = Author
112 |
113 | :param Flask app: The Flask application object.
114 | """
115 |
116 | def __init__(self, app: typing.Optional["Flask"] = None):
117 | self.Schema = Schema
118 | if has_sqla:
119 | self.SQLAlchemySchema = sqla.SQLAlchemySchema
120 | self.SQLAlchemyAutoSchema = sqla.SQLAlchemyAutoSchema
121 | self.auto_field = sqla.auto_field
122 | self.HyperlinkRelated = sqla.HyperlinkRelated
123 | _attach_fields(self)
124 | if app is not None:
125 | self.init_app(app)
126 |
127 | def init_app(self, app: "Flask"):
128 | """Initializes the application with the extension.
129 |
130 | :param Flask app: The Flask application object.
131 | """
132 | app.extensions = getattr(app, "extensions", {})
133 |
134 | # If using Flask-SQLAlchemy, attach db.session to SQLAlchemySchema
135 | if has_sqla and "sqlalchemy" in app.extensions:
136 | db = app.extensions["sqlalchemy"]
137 | SQLAlchemySchemaOpts = typing.cast(
138 | sqla.SQLAlchemySchemaOpts, self.SQLAlchemySchema.OPTIONS_CLASS
139 | )
140 | SQLAlchemySchemaOpts.session = db.session
141 | SQLAlchemyAutoSchemaOpts = typing.cast(
142 | sqla.SQLAlchemyAutoSchemaOpts, self.SQLAlchemySchema.OPTIONS_CLASS
143 | )
144 | SQLAlchemyAutoSchemaOpts.session = db.session
145 | app.extensions[EXTENSION_NAME] = self
146 |
--------------------------------------------------------------------------------
/src/flask_marshmallow/fields.py:
--------------------------------------------------------------------------------
1 | """
2 | flask_marshmallow.fields
3 | ~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | Custom, Flask-specific fields.
6 |
7 | See the `marshmallow.fields` module for the list of all fields available from the
8 | marshmallow library.
9 | """
10 |
11 | from __future__ import annotations
12 |
13 | import re
14 | import typing
15 | from collections.abc import Sequence
16 |
17 | from flask import current_app, url_for
18 | from marshmallow import fields, missing
19 |
20 | __all__ = [
21 | "URLFor",
22 | "UrlFor",
23 | "AbsoluteURLFor",
24 | "AbsoluteUrlFor",
25 | "Hyperlinks",
26 | "File",
27 | "Config",
28 | ]
29 |
30 |
31 | _tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
32 |
33 |
34 | def _tpl(val: str) -> str | None:
35 | """Return value within ``< >`` if possible, else return ``None``."""
36 | match = _tpl_pattern.match(val)
37 | if match:
38 | return match.groups()[0]
39 | return None
40 |
41 |
42 | def _get_value(obj, key, default=missing):
43 | """Slightly-modified version of marshmallow.utils.get_value.
44 | If a dot-delimited ``key`` is passed and any attribute in the
45 | path is `None`, return `None`.
46 | """
47 | if "." in key:
48 | return _get_value_for_keys(obj, key.split("."), default)
49 | else:
50 | return _get_value_for_key(obj, key, default)
51 |
52 |
53 | def _get_value_for_keys(obj, keys, default):
54 | if len(keys) == 1:
55 | return _get_value_for_key(obj, keys[0], default)
56 | else:
57 | value = _get_value_for_key(obj, keys[0], default)
58 | # XXX This differs from the marshmallow implementation
59 | if value is None:
60 | return None
61 | return _get_value_for_keys(value, keys[1:], default)
62 |
63 |
64 | def _get_value_for_key(obj, key, default):
65 | if not hasattr(obj, "__getitem__"):
66 | return getattr(obj, key, default)
67 |
68 | try:
69 | return obj[key]
70 | except (KeyError, IndexError, TypeError, AttributeError):
71 | return getattr(obj, key, default)
72 |
73 |
74 | class URLFor(fields.Field):
75 | """Field that outputs the URL for an endpoint. Acts identically to
76 | Flask's ``url_for`` function, except that arguments can be pulled from the
77 | object to be serialized, and ``**values`` should be passed to the ``values``
78 | parameter.
79 |
80 | Usage: ::
81 |
82 | url = URLFor("author_get", values=dict(id=""))
83 | https_url = URLFor(
84 | "author_get",
85 | values=dict(id="", _scheme="https", _external=True),
86 | )
87 |
88 | :param str endpoint: Flask endpoint name.
89 | :param dict values: Same keyword arguments as Flask's url_for, except string
90 | arguments enclosed in `< >` will be interpreted as attributes to pull
91 | from the object.
92 | :param kwargs: keyword arguments to pass to marshmallow field (e.g. ``required``).
93 | """
94 |
95 | _CHECK_ATTRIBUTE = False
96 |
97 | def __init__(
98 | self,
99 | endpoint: str,
100 | values: dict[str, typing.Any] | None = None,
101 | **kwargs,
102 | ):
103 | self.endpoint = endpoint
104 | self.values = values or {}
105 | fields.Field.__init__(self, **kwargs)
106 |
107 | def _serialize(self, value, key, obj):
108 | """Output the URL for the endpoint, given the kwargs passed to
109 | ``__init__``.
110 | """
111 | param_values = {}
112 | for name, attr_tpl in self.values.items():
113 | attr_name = _tpl(str(attr_tpl))
114 | if attr_name:
115 | attribute_value = _get_value(obj, attr_name, default=missing)
116 | if attribute_value is None:
117 | return None
118 | if attribute_value is not missing:
119 | param_values[name] = attribute_value
120 | else:
121 | raise AttributeError(
122 | f"{attr_name!r} is not a valid attribute of {obj!r}"
123 | )
124 | else:
125 | param_values[name] = attr_tpl
126 | return url_for(self.endpoint, **param_values)
127 |
128 |
129 | UrlFor = URLFor
130 |
131 |
132 | class AbsoluteURLFor(URLFor):
133 | """Field that outputs the absolute URL for an endpoint."""
134 |
135 | def __init__(
136 | self,
137 | endpoint: str,
138 | values: dict[str, typing.Any] | None = None,
139 | **kwargs,
140 | ):
141 | if values:
142 | values["_external"] = True
143 | else:
144 | values = {"_external": True}
145 | URLFor.__init__(self, endpoint=endpoint, values=values, **kwargs)
146 |
147 |
148 | AbsoluteUrlFor = AbsoluteURLFor
149 |
150 |
151 | def _rapply(d: dict | typing.Iterable, func: typing.Callable, *args, **kwargs):
152 | """Apply a function to all values in a dictionary or
153 | list of dictionaries, recursively.
154 | """
155 | if isinstance(d, (tuple, list)):
156 | return [_rapply(each, func, *args, **kwargs) for each in d]
157 | if isinstance(d, dict):
158 | return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()}
159 | else:
160 | return func(d, *args, **kwargs)
161 |
162 |
163 | def _url_val(val: typing.Any, key: str, obj: typing.Any, **kwargs):
164 | """Function applied by `HyperlinksField` to get the correct value in the
165 | schema.
166 | """
167 | if isinstance(val, URLFor):
168 | return val.serialize(key, obj, **kwargs)
169 | else:
170 | return val
171 |
172 |
173 | class Hyperlinks(fields.Field):
174 | """Field that outputs a dictionary of hyperlinks,
175 | given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor`
176 | objects as values.
177 |
178 | Example: ::
179 |
180 | _links = Hyperlinks(
181 | {
182 | "self": URLFor("author", values=dict(id="")),
183 | "collection": URLFor("author_list"),
184 | }
185 | )
186 |
187 | `URLFor` objects can be nested within the dictionary. ::
188 |
189 | _links = Hyperlinks(
190 | {
191 | "self": {
192 | "href": URLFor("book", values=dict(id="")),
193 | "title": "book detail",
194 | }
195 | }
196 | )
197 |
198 | :param dict schema: A dict that maps names to
199 | :class:`~flask_marshmallow.fields.URLFor` fields.
200 | """
201 |
202 | _CHECK_ATTRIBUTE = False
203 |
204 | def __init__(self, schema: dict[str, URLFor | str], **kwargs):
205 | self.schema = schema
206 | fields.Field.__init__(self, **kwargs)
207 |
208 | def _serialize(self, value, attr, obj):
209 | return _rapply(self.schema, _url_val, key=attr, obj=obj)
210 |
211 |
212 | class File(fields.Field):
213 | """A binary file field for uploaded files.
214 |
215 | Examples: ::
216 |
217 | class ImageSchema(Schema):
218 | image = File(required=True)
219 | """
220 |
221 | def __init__(self, *args, **kwargs):
222 | super().__init__(*args, **kwargs)
223 | # Metadata used by apispec
224 | self.metadata["type"] = "string"
225 | self.metadata["format"] = "binary"
226 |
227 | default_error_messages = {"invalid": "Not a valid file."}
228 |
229 | def deserialize(
230 | self,
231 | value: typing.Any,
232 | attr: str | None = None,
233 | data: typing.Mapping[str, typing.Any] | None = None,
234 | **kwargs,
235 | ):
236 | if isinstance(value, Sequence) and len(value) == 0:
237 | value = missing
238 | return super().deserialize(value, attr, data, **kwargs)
239 |
240 | def _deserialize(self, value, attr, data, **kwargs):
241 | from werkzeug.datastructures import FileStorage
242 |
243 | if not isinstance(value, FileStorage):
244 | raise self.make_error("invalid")
245 | return value
246 |
247 |
248 | class Config(fields.Field):
249 | """A field for Flask configuration values.
250 |
251 | Examples: ::
252 |
253 | from flask import Flask
254 |
255 | app = Flask(__name__)
256 | app.config["API_TITLE"] = "Pet API"
257 |
258 |
259 | class FooSchema(Schema):
260 | user = String()
261 | title = Config("API_TITLE")
262 |
263 | This field should only be used in an output schema. A ``ValueError`` will
264 | be raised if the config key is not found in the app config.
265 |
266 | :param str key: The key of the configuration value.
267 | """
268 |
269 | _CHECK_ATTRIBUTE = False
270 |
271 | def __init__(self, key: str, **kwargs):
272 | fields.Field.__init__(self, **kwargs)
273 | self.key = key
274 |
275 | def _serialize(self, value, attr, obj, **kwargs):
276 | if self.key not in current_app.config:
277 | raise ValueError(f"The key {self.key!r} is not found in the app config.")
278 | return current_app.config[self.key]
279 |
--------------------------------------------------------------------------------
/src/flask_marshmallow/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marshmallow-code/flask-marshmallow/3c93f12ed1532fdc167aee33fe94763a8268d07b/src/flask_marshmallow/py.typed
--------------------------------------------------------------------------------
/src/flask_marshmallow/schema.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing
4 |
5 | import flask
6 | import marshmallow as ma
7 |
8 | if typing.TYPE_CHECKING:
9 | from flask.wrappers import Response
10 |
11 |
12 | class Schema(ma.Schema):
13 | """Base serializer with which to define custom serializers.
14 |
15 | See `marshmallow.Schema` for more details about the `Schema` API.
16 | """
17 |
18 | def jsonify(
19 | self, obj: typing.Any, many: bool | None = None, *args, **kwargs
20 | ) -> Response:
21 | """Return a JSON response containing the serialized data.
22 |
23 |
24 | :param obj: Object to serialize.
25 | :param bool many: Whether `obj` should be serialized as an instance
26 | or as a collection. If None, defaults to the value of the
27 | `many` attribute on this Schema.
28 | :param kwargs: Additional keyword arguments passed to `flask.jsonify`.
29 |
30 | .. versionchanged:: 0.6.0
31 | Takes the same arguments as `marshmallow.Schema.dump`. Additional
32 | keyword arguments are passed to `flask.jsonify`.
33 |
34 | .. versionchanged:: 0.6.3
35 | The `many` argument for this method defaults to the value of
36 | the `many` attribute on the Schema. Previously, the `many`
37 | argument of this method defaulted to False, regardless of the
38 | value of `Schema.many`.
39 | """
40 | if many is None:
41 | many = self.many
42 | data = self.dump(obj, many=many)
43 | return flask.jsonify(data, *args, **kwargs)
44 |
--------------------------------------------------------------------------------
/src/flask_marshmallow/sqla.py:
--------------------------------------------------------------------------------
1 | """
2 | flask_marshmallow.sqla
3 | ~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | Integration with Flask-SQLAlchemy and marshmallow-sqlalchemy. Provides
6 | `SQLAlchemySchema ` and
7 | `SQLAlchemyAutoSchema ` classes
8 | that use the scoped session from Flask-SQLAlchemy.
9 | """
10 |
11 | from __future__ import annotations
12 |
13 | from urllib import parse
14 |
15 | import marshmallow_sqlalchemy as msqla
16 | from flask import current_app, url_for
17 | from marshmallow.exceptions import ValidationError
18 |
19 | from .schema import Schema
20 |
21 |
22 | class DummySession:
23 | """Placeholder session object."""
24 |
25 | pass
26 |
27 |
28 | class FlaskSQLAlchemyOptsMixin:
29 | session = DummySession()
30 |
31 | def __init__(self, meta, **kwargs):
32 | if not hasattr(meta, "sqla_session"):
33 | meta.sqla_session = self.session
34 | super().__init__(meta, **kwargs)
35 |
36 |
37 | # SQLAlchemySchema and SQLAlchemyAutoSchema are available in newer ma-sqla versions
38 | if hasattr(msqla, "SQLAlchemySchema"):
39 |
40 | class SQLAlchemySchemaOpts(FlaskSQLAlchemyOptsMixin, msqla.SQLAlchemySchemaOpts):
41 | pass
42 |
43 | class SQLAlchemySchema(msqla.SQLAlchemySchema, Schema):
44 | """SQLAlchemySchema that associates a schema with a model via the
45 | `model` class Meta option, which should be a
46 | ``db.Model`` class from `flask_sqlalchemy`. Uses the
47 | scoped session from Flask-SQLAlchemy by default.
48 |
49 | See `marshmallow_sqlalchemy.SQLAlchemySchema` for more details
50 | on the `SQLAlchemySchema` API.
51 | """
52 |
53 | OPTIONS_CLASS = SQLAlchemySchemaOpts
54 |
55 | else:
56 | SQLAlchemySchema = None # type: ignore
57 |
58 | if hasattr(msqla, "SQLAlchemyAutoSchema"):
59 |
60 | class SQLAlchemyAutoSchemaOpts(
61 | FlaskSQLAlchemyOptsMixin, msqla.SQLAlchemyAutoSchemaOpts
62 | ):
63 | pass
64 |
65 | class SQLAlchemyAutoSchema(msqla.SQLAlchemyAutoSchema, Schema):
66 | """SQLAlchemyAutoSchema that automatically generates marshmallow fields
67 | from a SQLAlchemy model's or table's column.
68 | Uses the scoped session from Flask-SQLAlchemy by default.
69 |
70 | See `marshmallow_sqlalchemy.SQLAlchemyAutoSchema` for more details
71 | on the `SQLAlchemyAutoSchema` API.
72 | """
73 |
74 | OPTIONS_CLASS = SQLAlchemyAutoSchemaOpts
75 |
76 | else:
77 | SQLAlchemyAutoSchema = None # type: ignore
78 |
79 | auto_field = getattr(msqla, "auto_field", None)
80 |
81 |
82 | class HyperlinkRelated(msqla.fields.Related):
83 | """Field that generates hyperlinks to indicate references between models,
84 | rather than primary keys.
85 |
86 | :param str endpoint: Flask endpoint name for generated hyperlink.
87 | :param str url_key: The attribute containing the reference's primary
88 | key. Defaults to "id".
89 | :param bool external: Set to `True` if absolute URLs should be used,
90 | instead of relative URLs.
91 | """
92 |
93 | def __init__(
94 | self, endpoint: str, url_key: str = "id", external: bool = False, **kwargs
95 | ):
96 | super().__init__(**kwargs)
97 | self.endpoint = endpoint
98 | self.url_key = url_key
99 | self.external = external
100 |
101 | def _serialize(self, value, attr, obj):
102 | if value is None:
103 | return None
104 | key = super()._serialize(value, attr, obj)
105 | kwargs = {self.url_key: key}
106 | return url_for(self.endpoint, _external=self.external, **kwargs)
107 |
108 | def _deserialize(self, value, *args, **kwargs):
109 | if self.external:
110 | parsed = parse.urlparse(value)
111 | value = parsed.path
112 | endpoint, kwargs = self.adapter.match(value)
113 | if endpoint != self.endpoint:
114 | raise ValidationError(
115 | f'Parsed endpoint "{endpoint}" from URL "{value}"; expected '
116 | f'"{self.endpoint}"'
117 | )
118 | if self.url_key not in kwargs:
119 | raise ValidationError(
120 | f'URL pattern "{self.url_key}" not found in {kwargs!r}'
121 | )
122 | return super()._deserialize(kwargs[self.url_key], *args, **kwargs)
123 |
124 | @property
125 | def adapter(self):
126 | return current_app.url_map.bind("")
127 |
--------------------------------------------------------------------------------
/src/flask_marshmallow/validate.py:
--------------------------------------------------------------------------------
1 | """
2 | flask_marshmallow.validate
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | Custom validation classes for various types of data.
6 | """
7 |
8 | from __future__ import annotations
9 |
10 | import io
11 | import os
12 | import re
13 | import typing
14 | from tempfile import SpooledTemporaryFile
15 |
16 | from marshmallow.exceptions import ValidationError
17 | from marshmallow.validate import Validator as Validator
18 | from werkzeug.datastructures import FileStorage
19 |
20 |
21 | def _get_filestorage_size(file: FileStorage) -> int:
22 | """Return the size of the FileStorage object in bytes."""
23 | stream = file.stream
24 | if isinstance(stream, io.BytesIO):
25 | return stream.getbuffer().nbytes
26 |
27 | if isinstance(stream, SpooledTemporaryFile):
28 | return os.stat(stream.fileno()).st_size
29 |
30 | size = len(file.read())
31 | file.stream.seek(0)
32 | return size
33 |
34 |
35 | # This function is copied from loguru with few modifications.
36 | # https://github.com/Delgan/loguru/blob/master/loguru/_string_parsers.py#L35
37 | def _parse_size(size: str) -> float:
38 | """Return the value which the ``size`` represents in bytes."""
39 | size = size.strip()
40 | reg = re.compile(r"([e\+\-\.\d]+)\s*([kmgtpezy])?(i)?(b)", flags=re.I)
41 |
42 | match = reg.fullmatch(size)
43 |
44 | if not match:
45 | raise ValueError(f"Invalid size value: '{size!r}'")
46 |
47 | s, u, i, b = match.groups()
48 |
49 | try:
50 | s = float(s)
51 | except ValueError as e:
52 | raise ValueError(f"Invalid float value while parsing size: '{s!r}'") from e
53 |
54 | u = "kmgtpezy".index(u.lower()) + 1 if u else 0
55 | i = 1024 if i else 1000
56 | b = {"b": 8, "B": 1}[b] if b else 1
57 | return s * i**u / b
58 |
59 |
60 | class FileSize(Validator):
61 | """Validator which succeeds if the file passed to it is within the specified
62 | size range. If ``min`` is not specified, or is specified as `None`,
63 | no lower bound exists. If ``max`` is not specified, or is specified as `None`,
64 | no upper bound exists. The inclusivity of the bounds (if they exist)
65 | is configurable.
66 | If ``min_inclusive`` is not specified, or is specified as `True`, then
67 | the ``min`` bound is included in the range. If ``max_inclusive`` is not specified,
68 | or is specified as `True`, then the ``max`` bound is included in the range.
69 |
70 | Example: ::
71 |
72 | class ImageSchema(Schema):
73 | image = File(required=True, validate=FileSize(min="1 MiB", max="2 MiB"))
74 |
75 | :param min: The minimum size (lower bound). If not provided, minimum
76 | size will not be checked.
77 | :param max: The maximum size (upper bound). If not provided, maximum
78 | size will not be checked.
79 | :param min_inclusive: Whether the ``min`` bound is included in the range.
80 | :param max_inclusive: Whether the ``max`` bound is included in the range.
81 | :param error: Error message to raise in case of a validation error.
82 | Can be interpolated with `{input}`, `{min}` and `{max}`.
83 | """
84 |
85 | message_min = "Must be {min_op} {{min}}."
86 | message_max = "Must be {max_op} {{max}}."
87 | message_all = "Must be {min_op} {{min}} and {max_op} {{max}}."
88 |
89 | message_gte = "greater than or equal to"
90 | message_gt = "greater than"
91 | message_lte = "less than or equal to"
92 | message_lt = "less than"
93 |
94 | def __init__(
95 | self,
96 | min: str | None = None,
97 | max: str | None = None,
98 | min_inclusive: bool = True,
99 | max_inclusive: bool = True,
100 | error: str | None = None,
101 | ):
102 | self.min = min
103 | self.max = max
104 | self.min_size = _parse_size(self.min) if self.min else None
105 | self.max_size = _parse_size(self.max) if self.max else None
106 | self.min_inclusive = min_inclusive
107 | self.max_inclusive = max_inclusive
108 | self.error = error
109 |
110 | self.message_min = self.message_min.format(
111 | min_op=self.message_gte if self.min_inclusive else self.message_gt
112 | )
113 | self.message_max = self.message_max.format(
114 | max_op=self.message_lte if self.max_inclusive else self.message_lt
115 | )
116 | self.message_all = self.message_all.format(
117 | min_op=self.message_gte if self.min_inclusive else self.message_gt,
118 | max_op=self.message_lte if self.max_inclusive else self.message_lt,
119 | )
120 |
121 | def _repr_args(self):
122 | return (
123 | f"min={self.min!r}, max={self.max!r}, "
124 | f"min_inclusive={self.min_inclusive!r}, "
125 | f"max_inclusive={self.max_inclusive!r}"
126 | )
127 |
128 | def _format_error(self, value, message):
129 | return (self.error or message).format(input=value, min=self.min, max=self.max)
130 |
131 | def __call__(self, value):
132 | if not isinstance(value, FileStorage):
133 | raise TypeError(
134 | f"A FileStorage object is required, not {type(value).__name__!r}"
135 | )
136 |
137 | file_size = _get_filestorage_size(value)
138 | if self.min_size is not None and (
139 | file_size < self.min_size
140 | if self.min_inclusive
141 | else file_size <= self.min_size
142 | ):
143 | message = self.message_min if self.max is None else self.message_all
144 | raise ValidationError(self._format_error(value, message))
145 |
146 | if self.max_size is not None and (
147 | file_size > self.max_size
148 | if self.max_inclusive
149 | else file_size >= self.max_size
150 | ):
151 | message = self.message_max if self.min is None else self.message_all
152 | raise ValidationError(self._format_error(value, message))
153 |
154 | return value
155 |
156 |
157 | class FileType(Validator):
158 | """Validator which succeeds if the uploaded file is allowed by a given list
159 | of extensions.
160 |
161 | Example: ::
162 |
163 | class ImageSchema(Schema):
164 | image = File(required=True, validate=FileType([".png"]))
165 |
166 | :param accept: A sequence of allowed extensions.
167 | :param error: Error message to raise in case of a validation error.
168 | Can be interpolated with ``{input}`` and ``{extensions}``.
169 | """
170 |
171 | default_message = "Not an allowed file type. Allowed file types: [{extensions}]"
172 |
173 | def __init__(
174 | self,
175 | accept: typing.Iterable[str],
176 | error: str | None = None,
177 | ):
178 | self.allowed_types = {ext.lower() for ext in accept}
179 | self.error = error or self.default_message
180 |
181 | def _format_error(self, value):
182 | return (self.error or self.default_message).format(
183 | input=value, extensions="".join(self.allowed_types)
184 | )
185 |
186 | def __call__(self, value):
187 | if not isinstance(value, FileStorage):
188 | raise TypeError(
189 | f"A FileStorage object is required, not {type(value).__name__!r}"
190 | )
191 |
192 | _, extension = (
193 | os.path.splitext(value.filename) if value.filename else (None, None)
194 | )
195 | if extension is None or extension.lower() not in self.allowed_types:
196 | raise ValidationError(self._format_error(value))
197 |
198 | return value
199 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marshmallow-code/flask-marshmallow/3c93f12ed1532fdc167aee33fe94763a8268d07b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Pytest fixtures for the test suite."""
2 |
3 | import pytest
4 | from flask import Flask
5 |
6 | from flask_marshmallow import Marshmallow
7 |
8 | _app = Flask(__name__)
9 | _app.testing = True
10 |
11 |
12 | class Bunch(dict):
13 | def __init__(self, *args, **kwargs):
14 | super().__init__(*args, **kwargs)
15 | self.__dict__ = self
16 |
17 |
18 | # Models
19 |
20 |
21 | class Author(Bunch):
22 | pass
23 |
24 |
25 | class Book(Bunch):
26 | pass
27 |
28 |
29 | @pytest.fixture
30 | def mockauthor():
31 | author = Author(id=123, name="Fred Douglass")
32 | return author
33 |
34 |
35 | @pytest.fixture
36 | def mockauthorlist():
37 | a1 = Author(id=1, name="Alice")
38 | a2 = Author(id=2, name="Bob")
39 | a3 = Author(id=3, name="Carol")
40 | return [a1, a2, a3]
41 |
42 |
43 | @pytest.fixture
44 | def mockbook(mockauthor):
45 | book = Book(id=42, author=mockauthor, title="Legend of Bagger Vance")
46 | return book
47 |
48 |
49 | @_app.route("/author/")
50 | def author(id):
51 | return "Steven Pressfield"
52 |
53 |
54 | @_app.route("/authors/")
55 | def authors():
56 | return "Steven Pressfield, Chuck Paluhniuk"
57 |
58 |
59 | @_app.route("/books/")
60 | def books():
61 | return "Legend of Bagger Vance, Fight Club"
62 |
63 |
64 | @_app.route("/books/")
65 | def book(id):
66 | return "Legend of Bagger Vance"
67 |
68 |
69 | @pytest.fixture(scope="function")
70 | def app():
71 | ctx = _app.test_request_context()
72 | ctx.push()
73 |
74 | yield _app
75 |
76 | ctx.pop()
77 |
78 |
79 | @pytest.fixture(scope="function")
80 | def ma(app):
81 | return Marshmallow(app)
82 |
83 |
84 | @pytest.fixture
85 | def schemas(ma):
86 | class AuthorSchema(ma.Schema):
87 | id = ma.Integer()
88 | name = ma.String()
89 | absolute_url = ma.AbsoluteURLFor("author", values={"id": ""})
90 |
91 | links = ma.Hyperlinks(
92 | {
93 | "self": ma.URLFor("author", values={"id": ""}),
94 | "collection": ma.URLFor("authors"),
95 | }
96 | )
97 |
98 | class BookSchema(ma.Schema):
99 | id = ma.Integer()
100 | title = ma.String()
101 | author = ma.Nested(AuthorSchema)
102 |
103 | links = ma.Hyperlinks(
104 | {
105 | "self": ma.URLFor("book", values={"id": ""}),
106 | "collection": ma.URLFor("books"),
107 | }
108 | )
109 |
110 | # So we can access schemas.AuthorSchema, etc.
111 | return Bunch(**locals())
112 |
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask import Flask, url_for
4 | from werkzeug.wrappers import Response
5 |
6 | from flask_marshmallow import Marshmallow
7 |
8 |
9 | def test_deferred_initialization():
10 | app = Flask(__name__)
11 | m = Marshmallow()
12 | m.init_app(app)
13 |
14 | assert "flask-marshmallow" in app.extensions
15 |
16 |
17 | def test_schema(app, schemas, mockauthor):
18 | s = schemas.AuthorSchema()
19 | result = s.dump(mockauthor)
20 | assert result["id"] == mockauthor.id
21 | assert result["name"] == mockauthor.name
22 | assert result["absolute_url"] == url_for("author", id=mockauthor.id, _external=True)
23 | links = result["links"]
24 | assert links["self"] == url_for("author", id=mockauthor.id)
25 | assert links["collection"] == url_for("authors")
26 |
27 |
28 | def test_jsonify_instance(app, schemas, mockauthor):
29 | s = schemas.AuthorSchema()
30 | resp = s.jsonify(mockauthor)
31 | assert isinstance(resp, Response)
32 | assert resp.content_type == "application/json"
33 | obj = json.loads(resp.get_data(as_text=True))
34 | assert isinstance(obj, dict)
35 |
36 |
37 | def test_jsonify_collection(app, schemas, mockauthorlist):
38 | s = schemas.AuthorSchema()
39 | resp = s.jsonify(mockauthorlist, many=True)
40 | assert isinstance(resp, Response)
41 | assert resp.content_type == "application/json"
42 | obj = json.loads(resp.get_data(as_text=True))
43 | assert isinstance(obj, list)
44 |
45 |
46 | def test_jsonify_collection_via_schema_attr(app, schemas, mockauthorlist):
47 | s = schemas.AuthorSchema(many=True)
48 | resp = s.jsonify(mockauthorlist)
49 | assert isinstance(resp, Response)
50 | assert resp.content_type == "application/json"
51 | obj = json.loads(resp.get_data(as_text=True))
52 | assert isinstance(obj, list)
53 |
54 |
55 | def test_links_within_nested_object(app, schemas, mockbook):
56 | s = schemas.BookSchema()
57 | result = s.dump(mockbook)
58 | assert result["title"] == mockbook.title
59 | author = result["author"]
60 | assert author["links"]["self"] == url_for("author", id=mockbook.author.id)
61 | assert author["links"]["collection"] == url_for("authors")
62 |
--------------------------------------------------------------------------------
/tests/test_fields.py:
--------------------------------------------------------------------------------
1 | import io
2 | from tempfile import SpooledTemporaryFile
3 |
4 | import pytest
5 | from flask import url_for
6 | from marshmallow import missing
7 | from marshmallow.exceptions import ValidationError
8 | from werkzeug.datastructures import FileStorage
9 | from werkzeug.routing import BuildError
10 |
11 | from flask_marshmallow.fields import _tpl
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "template", ["", " ", " ", "< id>", "", "< id >"]
16 | )
17 | def test_tpl(template):
18 | assert _tpl(template) == "id"
19 | assert _tpl(template) == "id"
20 | assert _tpl(template) == "id"
21 |
22 |
23 | def test_url_field(ma, mockauthor):
24 | field = ma.URLFor("author", values=dict(id=""))
25 | result = field.serialize("url", mockauthor)
26 | assert result == url_for("author", id=mockauthor.id)
27 |
28 | mockauthor.id = 0
29 | result = field.serialize("url", mockauthor)
30 | assert result == url_for("author", id=0)
31 |
32 |
33 | def test_url_field_with_invalid_attribute(ma, mockauthor):
34 | field = ma.URLFor("author", values=dict(id=""))
35 | expected_msg = "{!r} is not a valid attribute of {!r}".format(
36 | "not-an-attr", mockauthor
37 | )
38 | with pytest.raises(AttributeError, match=expected_msg):
39 | field.serialize("url", mockauthor)
40 |
41 |
42 | def test_url_field_handles_nested_attribute(ma, mockbook, mockauthor):
43 | field = ma.URLFor("author", values=dict(id=""))
44 | result = field.serialize("url", mockbook)
45 | assert result == url_for("author", id=mockauthor.id)
46 |
47 |
48 | def test_url_field_handles_none_attribute(ma, mockbook, mockauthor):
49 | mockbook.author = None
50 |
51 | field = ma.URLFor("author", values=dict(id=""))
52 | result = field.serialize("url", mockbook)
53 | assert result is None
54 |
55 | field = ma.URLFor("author", values=dict(id=""))
56 | result = field.serialize("url", mockbook)
57 | assert result is None
58 |
59 |
60 | def test_url_field_deserialization(ma):
61 | field = ma.URLFor("author", values=dict(id=""), allow_none=True)
62 | # noop
63 | assert field.deserialize("foo") == "foo"
64 | assert field.deserialize(None) is None
65 |
66 |
67 | def test_invalid_endpoint_raises_build_error(ma, mockauthor):
68 | field = ma.URLFor("badendpoint")
69 | with pytest.raises(BuildError):
70 | field.serialize("url", mockauthor)
71 |
72 |
73 | def test_hyperlinks_field(ma, mockauthor):
74 | field = ma.Hyperlinks(
75 | {
76 | "self": ma.URLFor("author", values={"id": ""}),
77 | "collection": ma.URLFor("authors"),
78 | }
79 | )
80 |
81 | result = field.serialize("_links", mockauthor)
82 | assert result == {
83 | "self": url_for("author", id=mockauthor.id),
84 | "collection": url_for("authors"),
85 | }
86 |
87 |
88 | def test_hyperlinks_field_recurses(ma, mockauthor):
89 | field = ma.Hyperlinks(
90 | {
91 | "self": {
92 | "href": ma.URLFor("author", values={"id": ""}),
93 | "title": "The author",
94 | },
95 | "collection": {"href": ma.URLFor("authors"), "title": "Authors list"},
96 | }
97 | )
98 | result = field.serialize("_links", mockauthor)
99 |
100 | assert result == {
101 | "self": {"href": url_for("author", id=mockauthor.id), "title": "The author"},
102 | "collection": {"href": url_for("authors"), "title": "Authors list"},
103 | }
104 |
105 |
106 | def test_hyperlinks_field_recurses_into_list(ma, mockauthor):
107 | field = ma.Hyperlinks(
108 | [
109 | {"rel": "self", "href": ma.URLFor("author", values={"id": ""})},
110 | {"rel": "collection", "href": ma.URLFor("authors")},
111 | ]
112 | )
113 | result = field.serialize("_links", mockauthor)
114 |
115 | assert result == [
116 | {"rel": "self", "href": url_for("author", id=mockauthor.id)},
117 | {"rel": "collection", "href": url_for("authors")},
118 | ]
119 |
120 |
121 | def test_hyperlinks_field_deserialization(ma):
122 | field = ma.Hyperlinks(
123 | {"href": ma.URLFor("author", values={"id": ""})}, allow_none=True
124 | )
125 | # noop
126 | assert field.deserialize("/author") == "/author"
127 | assert field.deserialize(None) is None
128 |
129 |
130 | def test_absolute_url(ma, mockauthor):
131 | field = ma.AbsoluteURLFor("authors")
132 | result = field.serialize("abs_url", mockauthor)
133 | assert result == url_for("authors", _external=True)
134 |
135 |
136 | def test_absolute_url_deserialization(ma):
137 | field = ma.AbsoluteURLFor("authors", allow_none=True)
138 | assert field.deserialize("foo") == "foo"
139 | assert field.deserialize(None) is None
140 |
141 |
142 | def test_aliases(ma):
143 | from flask_marshmallow.fields import AbsoluteURLFor, AbsoluteUrlFor, URLFor, UrlFor
144 |
145 | assert UrlFor is URLFor
146 | assert AbsoluteUrlFor is AbsoluteURLFor
147 |
148 |
149 | def test_file_field(ma, mockauthor):
150 | field = ma.File()
151 | fs = FileStorage(io.BytesIO(b"test"), "test.jpg")
152 | result = field.deserialize(fs, mockauthor)
153 | assert result == fs
154 |
155 | with SpooledTemporaryFile() as temp:
156 | temp.write(b"temp")
157 | fs = FileStorage(temp, "temp.jpg")
158 | result = field.deserialize(fs, mockauthor)
159 | assert result == fs
160 |
161 | result = field.deserialize("", mockauthor)
162 | assert result is missing
163 |
164 | with pytest.raises(ValidationError, match="Field may not be null."):
165 | field.deserialize(None, mockauthor)
166 |
167 | with pytest.raises(ValidationError, match="Not a valid file."):
168 | field.deserialize("123", mockauthor)
169 |
170 |
171 | def test_config_field(ma, app, mockauthor):
172 | app.config["NAME"] = "test"
173 | field = ma.Config(key="NAME")
174 |
175 | result = field.serialize("config_value", mockauthor)
176 | assert result == "test"
177 |
178 | field = ma.Config(key="DOES_NOT_EXIST")
179 | with pytest.raises(ValueError, match="not found in the app config"):
180 | field.serialize("config_value", mockauthor)
181 |
--------------------------------------------------------------------------------
/tests/test_io.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marshmallow-code/flask-marshmallow/3c93f12ed1532fdc167aee33fe94763a8268d07b/tests/test_io.py
--------------------------------------------------------------------------------
/tests/test_sqla.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flask import Flask, url_for
3 | from flask_sqlalchemy import SQLAlchemy
4 | from marshmallow import ValidationError
5 | from werkzeug.wrappers import Response
6 |
7 | from flask_marshmallow import Marshmallow
8 | from flask_marshmallow.sqla import HyperlinkRelated
9 | from tests.conftest import Bunch
10 |
11 | try:
12 | from marshmallow_sqlalchemy import SQLAlchemySchema # noqa: F401
13 | except ImportError:
14 | has_sqlalchemyschema = False
15 | else:
16 | has_sqlalchemyschema = True
17 |
18 |
19 | requires_sqlalchemyschema = pytest.mark.skipif(
20 | not has_sqlalchemyschema, reason="SQLAlchemySchema not available"
21 | )
22 |
23 |
24 | class TestSQLAlchemy:
25 | @pytest.fixture
26 | def extapp(self):
27 | app_ = Flask("extapp")
28 | app_.testing = True
29 | app_.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
30 | app_.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
31 | SQLAlchemy(app_)
32 | Marshmallow(app_)
33 |
34 | @app_.route("/author/")
35 | def author(id):
36 | return f"...view for author {id}..."
37 |
38 | @app_.route("/book/")
39 | def book(id):
40 | return f"...view for book {id}..."
41 |
42 | ctx = app_.test_request_context()
43 | ctx.push()
44 |
45 | yield app_
46 |
47 | ctx.pop()
48 |
49 | @pytest.fixture
50 | def db(self, extapp):
51 | db = extapp.extensions["sqlalchemy"]
52 | yield db
53 | db.session.close()
54 | db.engine.dispose()
55 |
56 | @pytest.fixture
57 | def extma(self, extapp):
58 | return extapp.extensions["flask-marshmallow"]
59 |
60 | @pytest.fixture
61 | def models(self, db):
62 | class AuthorModel(db.Model):
63 | __tablename__ = "author"
64 | id = db.Column(db.Integer, primary_key=True)
65 | name = db.Column(db.String(255))
66 |
67 | @property
68 | def url(self):
69 | return url_for("author", id=self.id)
70 |
71 | @property
72 | def absolute_url(self):
73 | return url_for("author", id=self.id, _external=True)
74 |
75 | class BookModel(db.Model):
76 | __tablename__ = "book"
77 | id = db.Column(db.Integer, primary_key=True)
78 | title = db.Column(db.String(255))
79 | author_id = db.Column(db.Integer, db.ForeignKey("author.id"))
80 | author = db.relationship("AuthorModel", backref="books")
81 |
82 | @property
83 | def url(self):
84 | return url_for("book", id=self.id)
85 |
86 | @property
87 | def absolute_url(self):
88 | return url_for("book", id=self.id, _external=True)
89 |
90 | db.create_all()
91 | yield Bunch(Author=AuthorModel, Book=BookModel)
92 | db.drop_all()
93 |
94 | def test_can_initialize_extensions(self, extapp):
95 | assert "flask-marshmallow" in extapp.extensions
96 | assert "sqlalchemy" in extapp.extensions
97 |
98 | @requires_sqlalchemyschema
99 | def test_can_declare_sqla_schemas(self, extma, models, db):
100 | class AuthorSchema(extma.SQLAlchemySchema):
101 | class Meta:
102 | model = models.Author
103 |
104 | id = extma.auto_field()
105 | name = extma.auto_field()
106 |
107 | class BookSchema(extma.SQLAlchemySchema):
108 | class Meta:
109 | model = models.Book
110 |
111 | id = extma.auto_field()
112 | title = extma.auto_field()
113 | author_id = extma.auto_field()
114 |
115 | author_schema = AuthorSchema()
116 | book_schema = BookSchema()
117 |
118 | author = models.Author(name="Chuck Paluhniuk")
119 | book = models.Book(title="Fight Club", author=author)
120 |
121 | author_result = author_schema.dump(author)
122 |
123 | assert "id" in author_result
124 | assert "name" in author_result
125 | assert author_result["id"] == author.id
126 | assert author_result["name"] == "Chuck Paluhniuk"
127 | book_result = book_schema.dump(book)
128 |
129 | assert "id" in book_result
130 | assert "title" in book_result
131 | assert book_result["id"] == book.id
132 | assert book_result["title"] == book.title
133 | assert book_result["author_id"] == book.author_id
134 |
135 | resp = author_schema.jsonify(author)
136 | assert isinstance(resp, Response)
137 |
138 | @requires_sqlalchemyschema
139 | def test_can_declare_sqla_auto_schemas(self, extma, models, db):
140 | class AuthorSchema(extma.SQLAlchemyAutoSchema):
141 | class Meta:
142 | model = models.Author
143 |
144 | class BookSchema(extma.SQLAlchemyAutoSchema):
145 | class Meta:
146 | model = models.Book
147 | include_fk = True
148 |
149 | id = extma.auto_field()
150 | title = extma.auto_field()
151 | author_id = extma.auto_field()
152 |
153 | author_schema = AuthorSchema()
154 | book_schema = BookSchema()
155 |
156 | author = models.Author(name="Chuck Paluhniuk")
157 | book = models.Book(title="Fight Club", author=author)
158 |
159 | author_result = author_schema.dump(author)
160 |
161 | assert "id" in author_result
162 | assert "name" in author_result
163 | assert author_result["id"] == author.id
164 | assert author_result["name"] == "Chuck Paluhniuk"
165 | book_result = book_schema.dump(book)
166 |
167 | assert "id" in book_result
168 | assert "title" in book_result
169 | assert book_result["id"] == book.id
170 | assert book_result["title"] == book.title
171 | assert book_result["author_id"] == book.author_id
172 |
173 | resp = author_schema.jsonify(author)
174 | assert isinstance(resp, Response)
175 |
176 | # FIXME: temporarily filter out this warning
177 | # this is triggered by marshmallow-sqlalchemy on sqlalchemy v1.4.x
178 | # on the current version it should be fixed
179 | # in an upcoming marshmallow-sqlalchemy release
180 | @requires_sqlalchemyschema
181 | def test_hyperlink_related_field(self, extma, models, db, extapp):
182 | class BookSchema(extma.SQLAlchemySchema):
183 | class Meta:
184 | model = models.Book
185 |
186 | author = extma.HyperlinkRelated("author")
187 |
188 | book_schema = BookSchema()
189 |
190 | author = models.Author(name="Chuck Paluhniuk")
191 | book = models.Book(title="Fight Club", author=author)
192 | db.session.add(author)
193 | db.session.add(book)
194 | db.session.flush()
195 |
196 | book_result = book_schema.dump(book)
197 |
198 | assert book_result["author"] == author.url
199 |
200 | deserialized = book_schema.load(book_result)
201 | assert deserialized["author"] == author
202 |
203 | @requires_sqlalchemyschema
204 | def test_hyperlink_related_field_serializes_none(self, extma, models):
205 | class BookSchema(extma.SQLAlchemySchema):
206 | class Meta:
207 | model = models.Book
208 |
209 | author = extma.HyperlinkRelated("author")
210 |
211 | book_schema = BookSchema()
212 | book = models.Book(title="Fight Club", author=None)
213 | book_result = book_schema.dump(book)
214 | assert book_result["author"] is None
215 |
216 | @requires_sqlalchemyschema
217 | def test_hyperlink_related_field_errors(self, extma, models, db, extapp):
218 | class BookSchema(extma.SQLAlchemySchema):
219 | class Meta:
220 | model = models.Book
221 |
222 | author = HyperlinkRelated("author")
223 |
224 | book_schema = BookSchema()
225 |
226 | author = models.Author(name="Chuck Paluhniuk")
227 | book = models.Book(title="Fight Club", author=author)
228 | db.session.add(author)
229 | db.session.add(book)
230 | db.session.flush()
231 |
232 | # Deserialization fails on bad endpoint
233 | book_result = book_schema.dump(book)
234 | book_result["author"] = book.url
235 | with pytest.raises(ValidationError) as excinfo:
236 | book_schema.load(book_result)
237 | errors = excinfo.value.messages
238 | assert 'expected "author"' in errors["author"][0]
239 |
240 | # Deserialization fails on bad URL key
241 | book_result = book_schema.dump(book)
242 | book_schema.fields["author"].url_key = "pk"
243 | with pytest.raises(ValidationError) as excinfo:
244 | book_schema.load(book_result)
245 | errors = excinfo.value.messages
246 | assert 'URL pattern "pk" not found' in errors["author"][0]
247 |
248 | @requires_sqlalchemyschema
249 | def test_hyperlink_related_field_external(self, extma, models, db, extapp):
250 | class BookSchema(extma.SQLAlchemySchema):
251 | class Meta:
252 | model = models.Book
253 |
254 | author = HyperlinkRelated("author", external=True)
255 |
256 | book_schema = BookSchema()
257 |
258 | author = models.Author(name="Chuck Paluhniuk")
259 | book = models.Book(title="Fight Club", author=author)
260 | db.session.add(author)
261 | db.session.add(book)
262 | db.session.flush()
263 |
264 | book_result = book_schema.dump(book)
265 |
266 | assert book_result["author"] == author.absolute_url
267 |
268 | deserialized = book_schema.load(book_result)
269 | assert deserialized["author"] == author
270 |
271 | @requires_sqlalchemyschema
272 | def test_hyperlink_related_field_list(self, extma, models, db, extapp):
273 | class AuthorSchema(extma.SQLAlchemySchema):
274 | class Meta:
275 | model = models.Author
276 |
277 | books = extma.List(HyperlinkRelated("book"))
278 |
279 | author_schema = AuthorSchema()
280 |
281 | author = models.Author(name="Chuck Paluhniuk")
282 | book = models.Book(title="Fight Club", author=author)
283 | db.session.add(author)
284 | db.session.add(book)
285 | db.session.flush()
286 |
287 | author_result = author_schema.dump(author)
288 | assert author_result["books"][0] == book.url
289 |
290 | deserialized = author_schema.load(author_result)
291 | assert deserialized["books"][0] == book
292 |
--------------------------------------------------------------------------------
/tests/test_validate.py:
--------------------------------------------------------------------------------
1 | import io
2 | from tempfile import SpooledTemporaryFile
3 |
4 | import pytest
5 | from marshmallow.exceptions import ValidationError
6 | from werkzeug.datastructures import FileStorage
7 |
8 | from flask_marshmallow import validate
9 |
10 |
11 | @pytest.mark.parametrize("size", ["1 KB", "1 KiB", "1 MB", "1 MiB", "1 GB", "1 GiB"])
12 | def test_parse_size(size):
13 | rv = validate._parse_size(size)
14 | if size == "1 KB":
15 | assert rv == 1000
16 | elif size == "1 KiB":
17 | assert rv == 1024
18 | elif size == "1 MB":
19 | assert rv == 1000000
20 | elif size == "1 MiB":
21 | assert rv == 1048576
22 | elif size == "1 GB":
23 | assert rv == 1000000000
24 | elif size == "1 GiB":
25 | assert rv == 1073741824
26 |
27 |
28 | def test_get_filestorage_size():
29 | rv = validate._get_filestorage_size(FileStorage(io.BytesIO(b"".ljust(0))))
30 | assert rv == 0
31 | rv = validate._get_filestorage_size(FileStorage(io.BytesIO(b"".ljust(123))))
32 | assert rv == 123
33 | rv = validate._get_filestorage_size(FileStorage(io.BytesIO(b"".ljust(1024))))
34 | assert rv == 1024
35 | rv = validate._get_filestorage_size(FileStorage(io.BytesIO(b"".ljust(1234))))
36 | assert rv == 1234
37 |
38 | with SpooledTemporaryFile() as temp:
39 | temp.write(b"".ljust(0))
40 | rv = validate._get_filestorage_size(FileStorage(temp))
41 | assert rv == 0
42 |
43 | with SpooledTemporaryFile() as temp:
44 | temp.write(b"".ljust(123))
45 | rv = validate._get_filestorage_size(FileStorage(temp))
46 | assert rv == 123
47 |
48 | with SpooledTemporaryFile() as temp:
49 | temp.write(b"".ljust(1024))
50 | rv = validate._get_filestorage_size(FileStorage(temp))
51 | assert rv == 1024
52 |
53 | with SpooledTemporaryFile() as temp:
54 | temp.write(b"".ljust(1234))
55 | rv = validate._get_filestorage_size(FileStorage(temp))
56 | assert rv == 1234
57 |
58 |
59 | @pytest.mark.parametrize("size", ["wrong_format", "1.2.3 MiB"])
60 | def test_parse_size_wrong_value(size):
61 | if size == "wrong_format":
62 | with pytest.raises(ValueError, match="Invalid size value: "):
63 | validate._parse_size(size)
64 | elif size == "1.2.3 MiB":
65 | with pytest.raises(
66 | ValueError, match="Invalid float value while parsing size: "
67 | ):
68 | validate._parse_size(size)
69 |
70 |
71 | def test_filesize_min():
72 | fs = FileStorage(io.BytesIO(b"".ljust(1024)))
73 | assert validate.FileSize(min="1 KiB", max="2 KiB")(fs) is fs
74 | assert validate.FileSize(min="0 KiB", max="1 KiB")(fs) is fs
75 | assert validate.FileSize()(fs) is fs
76 | assert validate.FileSize(min_inclusive=False, max_inclusive=False)(fs) is fs
77 | assert validate.FileSize(min="1 KiB", max="1 KiB")(fs) is fs
78 |
79 | with pytest.raises(ValidationError, match="Must be greater than or equal to 2 KiB"):
80 | validate.FileSize(min="2 KiB", max="3 KiB")(fs)
81 | with pytest.raises(ValidationError, match="Must be greater than or equal to 2 KiB"):
82 | validate.FileSize(min="2 KiB")(fs)
83 | with pytest.raises(ValidationError, match="Must be greater than 1 KiB"):
84 | validate.FileSize(
85 | min="1 KiB", max="2 KiB", min_inclusive=False, max_inclusive=True
86 | )(fs)
87 | with pytest.raises(ValidationError, match="less than 1 KiB"):
88 | validate.FileSize(
89 | min="1 KiB", max="1 KiB", min_inclusive=True, max_inclusive=False
90 | )(fs)
91 |
92 |
93 | def test_filesize_max():
94 | fs = FileStorage(io.BytesIO(b"".ljust(2048)))
95 | assert validate.FileSize(min="1 KiB", max="2 KiB")(fs) is fs
96 | assert validate.FileSize(max="2 KiB")(fs) is fs
97 | assert validate.FileSize()(fs) is fs
98 | assert validate.FileSize(min_inclusive=False, max_inclusive=False)(fs) is fs
99 | assert validate.FileSize(min="2 KiB", max="2 KiB")(fs) is fs
100 |
101 | with pytest.raises(ValidationError, match="less than or equal to 1 KiB"):
102 | validate.FileSize(min="0 KiB", max="1 KiB")(fs)
103 | with pytest.raises(ValidationError, match="less than or equal to 1 KiB"):
104 | validate.FileSize(max="1 KiB")(fs)
105 | with pytest.raises(ValidationError, match="less than 2 KiB"):
106 | validate.FileSize(
107 | min="1 KiB", max="2 KiB", min_inclusive=True, max_inclusive=False
108 | )(fs)
109 | with pytest.raises(ValidationError, match="greater than 2 KiB"):
110 | validate.FileSize(
111 | min="2 KiB", max="2 KiB", min_inclusive=False, max_inclusive=True
112 | )(fs)
113 |
114 |
115 | def test_filesize_repr():
116 | assert (
117 | repr(
118 | validate.FileSize(
119 | min=None, max=None, error=None, min_inclusive=True, max_inclusive=True
120 | )
121 | )
122 | == "" # noqa: E501
123 | )
124 |
125 | assert (
126 | repr(
127 | validate.FileSize(
128 | min="1 KiB",
129 | max="3 KiB",
130 | error="foo",
131 | min_inclusive=False,
132 | max_inclusive=False,
133 | )
134 | )
135 | == "" # noqa: E501
136 | )
137 |
138 |
139 | def test_filesize_wrongtype():
140 | with pytest.raises(TypeError, match="A FileStorage object is required, not "):
141 | validate.FileSize()(1)
142 |
143 |
144 | def test_filetype():
145 | png_fs = FileStorage(io.BytesIO(b"".ljust(1024)), "test.png")
146 | assert validate.FileType([".png"])(png_fs) is png_fs
147 | assert validate.FileType([".PNG"])(png_fs) is png_fs
148 |
149 | PNG_fs = FileStorage(io.BytesIO(b"".ljust(1024)), "test.PNG")
150 | assert validate.FileType([".png"])(PNG_fs) is PNG_fs
151 | assert validate.FileType([".PNG"])(PNG_fs) is PNG_fs
152 |
153 | with pytest.raises(TypeError, match="A FileStorage object is required, not "):
154 | validate.FileType([".png"])(1)
155 |
156 | with pytest.raises(
157 | ValidationError,
158 | match=r"Not an allowed file type. Allowed file types: \[.*?\]", # noqa: W605
159 | ):
160 | jpg_fs = FileStorage(io.BytesIO(b"".ljust(1024)), "test.jpg")
161 | validate.FileType([".png"])(jpg_fs)
162 |
163 | with pytest.raises(
164 | ValidationError,
165 | match=r"Not an allowed file type. Allowed file types: \[.*?\]", # noqa: W605
166 | ):
167 | no_ext_fs = FileStorage(io.BytesIO(b"".ljust(1024)), "test")
168 | validate.FileType([".png"])(no_ext_fs)
169 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=
3 | lint
4 | py{39,310,311,312,313}
5 | py313-marshmallowdev
6 | py39-lowest
7 | docs
8 |
9 | [testenv]
10 | extras = tests
11 | deps =
12 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
13 | lowest: marshmallow==3.0.0
14 | lowest: marshmallow-sqlalchemy==0.29.0
15 | lowest: flask-sqlalchemy==3.0.0
16 | lowest: flask==2.2
17 | lowest: werkzeug==2.2.2
18 | ; lowest version supported by marshmallow-sqlalchemy
19 | lowest: sqlalchemy==1.4.40
20 | commands = pytest {posargs}
21 |
22 | [testenv:lint]
23 | deps = pre-commit~=3.5
24 | skip_install = true
25 | commands = pre-commit run --all-files
26 |
27 | [testenv:docs]
28 | extras = docs
29 | commands = sphinx-build docs/ docs/_build {posargs}
30 |
31 | ; Below tasks are for development only (not run in CI)
32 |
33 | [testenv:watch-docs]
34 | deps =
35 | sphinx-autobuild
36 | extras = docs
37 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/flask_marshmallow --delay 2
38 |
39 | [testenv:watch-readme]
40 | deps = restview
41 | skip_install = true
42 | commands = restview README.rst
43 |
--------------------------------------------------------------------------------