├── .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 |

3 | Flask-Marshmallow Home

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 |

4 | 5 | Flask-Marshmallow Home

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 | Fork me on GitHub 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 | --------------------------------------------------------------------------------