├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── feature_request.md
│ └── question.md
├── PULL_REQUEST_TEMPLATE
│ └── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── black.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .pyup.yml
├── CHANGELOG.rst
├── CONTRIBUTING.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── bumpr.rc
├── coverage.rc
├── doc
├── Makefile
├── _static
│ ├── apple-180.png
│ ├── favicon-128.png
│ ├── favicon-196.png
│ ├── favicon-512.png
│ ├── favicon-64.png
│ ├── favicon.ico
│ ├── logo-512-nobg.png
│ ├── logo-512.png
│ └── screenshot-apidoc-quickstart.png
├── _themes
│ └── restx
│ │ ├── badges.html
│ │ ├── layout.html
│ │ ├── static
│ │ └── restx.css
│ │ └── theme.conf
├── api.rst
├── conf.py
├── configuration.rst
├── contributing.rst
├── errors.rst
├── example.rst
├── index.rst
├── installation.rst
├── logging.rst
├── make.bat
├── marshalling.rst
├── mask.rst
├── parsing.rst
├── postman.rst
├── quickstart.rst
├── scaling.rst
└── swagger.rst
├── examples
├── resource_class_kwargs
├── todo.py
├── todo_blueprint.py
├── todo_simple.py
├── todomvc.py
├── xml_representation.py
└── zoo_app
│ ├── complex.py
│ ├── requirements.txt
│ └── zoo
│ ├── __init__.py
│ ├── cat.py
│ └── dog.py
├── flask_restx
├── __about__.py
├── __init__.py
├── _http.py
├── api.py
├── apidoc.py
├── cors.py
├── errors.py
├── fields.py
├── inputs.py
├── marshalling.py
├── mask.py
├── model.py
├── namespace.py
├── postman.py
├── representations.py
├── reqparse.py
├── resource.py
├── schemas
│ ├── __init__.py
│ └── oas-2.0.json
├── swagger.py
├── templates
│ ├── swagger-ui-css.html
│ ├── swagger-ui-libs.html
│ └── swagger-ui.html
└── utils.py
├── package.json
├── readthedocs.pip
├── requirements
├── develop.pip
├── doc.pip
├── install.pip
└── test.pip
├── setup.cfg
├── setup.py
├── tasks.py
├── tests
├── __init__.py
├── benchmarks
│ ├── bench_marshalling.py
│ └── bench_swagger.py
├── conftest.py
├── legacy
│ ├── test_api_legacy.py
│ └── test_api_with_blueprint.py
├── postman-v1.schema.json
├── test_accept.py
├── test_api.py
├── test_apidoc.py
├── test_cors.py
├── test_errors.py
├── test_fields.py
├── test_fields_mask.py
├── test_inputs.py
├── test_logging.py
├── test_marshalling.py
├── test_model.py
├── test_namespace.py
├── test_payload.py
├── test_postman.py
├── test_reqparse.py
├── test_schemas.py
├── test_swagger.py
├── test_swagger_utils.py
└── test_utils.py
└── tox.ini
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | # Matches multiple files with brace expansion notation
13 | # Set default charset
14 | [*.{js,py}]
15 | charset = utf-8
16 |
17 | # 4 space indentation
18 | [*.py]
19 | indent_style = space
20 | indent_size = 4
21 | max_line_length = 120
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Tell us how Flask-RESTX is broken
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### ***** **BEFORE LOGGING AN ISSUE** *****
11 |
12 | - Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
13 | - Please check if a similar issue already exists or has been closed before. Seriously, nobody here is getting paid. Help us out and take five minutes to make sure you aren't submitting a duplicate.
14 | - Please review the [guidelines for contributing](https://github.com/python-restx/flask-restx/blob/master/CONTRIBUTING.rst)
15 |
16 | ### **Code**
17 |
18 | ```python
19 | from your_code import your_buggy_implementation
20 | ```
21 |
22 | ### **Repro Steps** (if applicable)
23 | 1. ...
24 | 2. ...
25 | 3. Broken!
26 |
27 | ### **Expected Behavior**
28 | A description of what you expected to happen.
29 |
30 | ### **Actual Behavior**
31 | A description of the unexpected, buggy behavior.
32 |
33 | ### **Error Messages/Stack Trace**
34 | If applicable, add the stack trace produced by the error
35 |
36 | ### **Environment**
37 | - Python version
38 | - Flask version
39 | - Flask-RESTX version
40 | - Other installed Flask extensions
41 |
42 | ### **Additional Context**
43 |
44 | This is your last chance to provide any pertinent details, don't let this opportunity pass you by!
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Ask a question**
11 | A clear and concise question
12 |
13 | **Additional context**
14 | Add any other context or screenshots about the feature request here.
15 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Proposed changes
2 |
3 | At a high level, describe your reasoning for making these changes. If you are fixing a bug or resolving a feature request, **please include a link to the issue**.
4 |
5 | ## Types of changes
6 |
7 | What types of changes does your code introduce?
8 | _Put an `x` in the boxes that apply_
9 |
10 | - [ ] Bugfix (non-breaking change which fixes an issue)
11 | - [ ] New feature (non-breaking change which adds functionality)
12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
13 |
14 | ## Checklist
15 |
16 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._
17 |
18 | - [ ] I have read the [guidelines for contributing](https://github.com/python-restx/flask-restx/blob/master/CONTRIBUTING.rst)
19 | - [ ] All unit tests pass on my local version with my changes
20 | - [ ] I have added tests that prove my fix is effective or that my feature works
21 | - [ ] I have added necessary documentation (if appropriate)
22 |
23 | ## Further comments
24 |
25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...
26 |
--------------------------------------------------------------------------------
/.github/workflows/black.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - uses: psf/black@stable
11 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Set up Python 3.9
11 | uses: actions/setup-python@v1
12 | with:
13 | python-version: 3.9
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install ".[dev]" wheel
20 | - name: Fetch web assets
21 | run: inv assets
22 | - name: Publish
23 | env:
24 | TWINE_USERNAME: "__token__"
25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
26 | run: |
27 | python setup.py sdist bdist_wheel
28 | twine upload dist/*
29 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | pull_request:
4 | branches:
5 | - "*"
6 | push:
7 | branches:
8 | - "*"
9 | schedule:
10 | - cron: "0 1 * * *"
11 | workflow_dispatch:
12 | jobs:
13 | unit-tests:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | python-version: ["3.9", "3.10", "3.11", "pypy3.9", "3.12"]
19 | flask: ["<3.0.0", ">=3.0.0"]
20 | steps:
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v4
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | allow-prereleases: true
26 | - name: Checkout code
27 | uses: actions/checkout@v3
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip
31 | pip install "flask${{ matrix.flask }}"
32 | pip install ".[test]"
33 | - name: Test with inv
34 | run: inv cover qa
35 | - name: Codecov
36 | uses: codecov/codecov-action@v1
37 | with:
38 | file: ./coverage.xml
39 | bench:
40 | needs: unit-tests
41 | runs-on: ubuntu-latest
42 | if: github.event_name == 'pull_request'
43 | steps:
44 | - name: Set up Python 3.9
45 | uses: actions/setup-python@v4
46 | with:
47 | python-version: "3.9"
48 | - name: Checkout ${{ github.base_ref }}
49 | uses: actions/checkout@v3
50 | with:
51 | ref: ${{ github.base_ref}}
52 | path: base
53 | - name: Checkout ${{ github.ref }}
54 | uses: actions/checkout@v3
55 | with:
56 | ref: ${{ github.ref}}
57 | path: ref
58 | - name: Install dev dependencies
59 | run: |
60 | python -m pip install --upgrade pip
61 | pip install -e "base[dev]"
62 | - name: Install ci dependencies for ${{ github.base_ref }}
63 | run: pip install -e "base[ci]"
64 | - name: Benchmarks for ${{ github.base_ref }}
65 | run: |
66 | cd base
67 | inv benchmark --max-time 4 --save
68 | mv .benchmarks ../ref/
69 | - name: Install ci dependencies for ${{ github.ref }}
70 | run: pip install -e "ref[ci]"
71 | - name: Benchmarks for ${{ github.ref }}
72 | run: |
73 | cd ref
74 | inv benchmark --max-time 4 --compare
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | bin/
12 | build/
13 | develop-eggs/
14 | dist/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | cover
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # Installer logs
27 | pip-log.txt
28 | pip-delete-this-directory.txt
29 |
30 | # Unit test / coverage reports
31 | htmlcov/
32 | .tox/
33 | .coverage
34 | .cache
35 | nosetests.xml
36 | coverage.xml
37 | prof/
38 | histograms/
39 | .benchmarks
40 |
41 | # Translations
42 | *.mo
43 |
44 | # Atom
45 | *.cson
46 |
47 | # Mr Developer
48 | .mr.developer.cfg
49 | .project
50 | .pydevproject
51 |
52 | # Rope
53 | .ropeproject
54 |
55 | # Django stuff:
56 | *.log
57 | *.pot
58 |
59 | # Sphinx documentation
60 | doc/_build/
61 |
62 | # Specifics
63 | flask_restx/static
64 | node_modules
65 |
66 | # pyenv
67 | .python-version
68 |
69 | # Jet Brains
70 | .idea
71 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # configure updates globally
2 | # default: all
3 | # allowed: all, insecure, False
4 | # update: all
5 |
6 | # configure dependency pinning globally
7 | # default: True
8 | # allowed: True, False
9 | pin: False
10 |
11 | # set the default branch
12 | # default: empty, the default branch on GitHub
13 | # branch: dev
14 |
15 | # update schedule
16 | # default: empty
17 | # allowed: "every day", "every week", ..
18 | # schedule: "every day"
19 |
20 | # search for requirement files
21 | # default: True
22 | # allowed: True, False
23 | # search: True
24 |
25 | # Specify requirement files by hand, default is empty
26 | # default: empty
27 | # allowed: list
28 | # requirements:
29 | # - requirements/staging.txt:
30 | # # update all dependencies and pin them
31 | # update: all
32 | # pin: True
33 | # - requirements/dev.txt:
34 | # # don't update dependencies, use global 'pin' default
35 | # update: False
36 | # - requirements/prod.txt:
37 | # # update insecure only, pin all
38 | # update: insecure
39 | # pin: True
40 |
41 | # add a label to pull requests, default is not set
42 | # requires private repo permissions, even on public repos
43 | # default: empty
44 | label_prs: update
45 |
46 | # assign users to pull requests, default is not set
47 | # requires private repo permissions, even on public repos
48 | # default: empty
49 | # assignees:
50 | # - carl
51 | # - carlsen
52 |
53 | # configure the branch prefix the bot is using
54 | # default: pyup-
55 | branch_prefix: pyup/
56 |
57 | # set a global prefix for PRs
58 | # default: empty
59 | pr_prefix: "[PyUP]"
60 |
61 | # allow to close stale PRs
62 | # default: True
63 | close_prs: True
64 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | flask-restx is open-source and very open to contributions.
5 |
6 | If you're part of a corporation with an NDA, and you may require updating the license.
7 | See Updating Copyright below
8 |
9 | Submitting issues
10 | -----------------
11 |
12 | Issues are contributions in a way so don't hesitate
13 | to submit reports on the `official bugtracker`_.
14 |
15 | Provide as much informations as possible to specify the issues:
16 |
17 | - the flask-restx version used
18 | - a stacktrace
19 | - installed applications list
20 | - a code sample to reproduce the issue
21 | - ...
22 |
23 |
24 | Submitting patches (bugfix, features, ...)
25 | ------------------------------------------
26 |
27 | If you want to contribute some code:
28 |
29 | 1. fork the `official flask-restx repository`_
30 | 2. Ensure an issue is opened for your feature or bug
31 | 3. create a branch with an explicit name (like ``my-new-feature`` or ``issue-XX``)
32 | 4. do your work in it
33 | 5. Commit your changes. Ensure the commit message includes the issue. Also, if contributing from a corporation, be sure to add a comment with the Copyright information
34 | 6. rebase it on the master branch from the official repository (cleanup your history by performing an interactive rebase)
35 | 7. add your change to the changelog
36 | 8. submit your pull-request
37 | 9. 2 Maintainers should review the code for bugfix and features. 1 maintainer for minor changes (such as docs)
38 | 10. After review, a maintainer a will merge the PR. Maintainers should not merge their own PRs
39 |
40 | There are some rules to follow:
41 |
42 | - your contribution should be documented (if needed)
43 | - your contribution should be tested and the test suite should pass successfully
44 | - your code should be properly formatted (use ``black .`` to format)
45 | - your contribution should support both Python 2 and 3 (use ``tox`` to test)
46 |
47 | You need to install some dependencies to develop on flask-restx:
48 |
49 | .. code-block:: console
50 |
51 | $ pip install -e .[dev]
52 |
53 | An `Invoke `_ ``tasks.py`` is provided to simplify the common tasks:
54 |
55 | .. code-block:: console
56 |
57 | $ inv -l
58 | Available tasks:
59 |
60 | all Run tests, reports and packaging
61 | assets Fetch web assets -- Swagger. Requires NPM (see below)
62 | clean Cleanup all build artifacts
63 | cover Run tests suite with coverage
64 | demo Run the demo
65 | dist Package for distribution
66 | doc Build the documentation
67 | qa Run a quality report
68 | test Run tests suite
69 | tox Run tests against Python versions
70 |
71 | To ensure everything is fine before submission, use ``tox``.
72 | It will run the test suite on all the supported Python version
73 | and ensure the documentation is generating.
74 |
75 | .. code-block:: console
76 |
77 | $ tox
78 |
79 | You also need to ensure your code is compliant with the flask-restx coding standards:
80 |
81 | .. code-block:: console
82 |
83 | $ inv qa
84 |
85 | To ensure everything is fine before committing, you can launch the all in one command:
86 |
87 | .. code-block:: console
88 |
89 | $ inv qa tox
90 |
91 | It will ensure the code meet the coding conventions, runs on every version on python
92 | and the documentation is properly generating.
93 |
94 | .. _official flask-restx repository: https://github.com/python-restx/flask-restx
95 | .. _official bugtracker: https://github.com/python-restx/flask-restx/issues
96 |
97 | Running a local Swagger Server
98 | ------------------------------
99 |
100 | For local development, you may wish to run a local server. running the following will install a swagger server
101 |
102 | .. code-block:: console
103 |
104 | $ inv assets
105 |
106 | NOTE: You'll need `NPM `_ installed to do this.
107 | If you're new to NPM, also check out `nvm `_
108 |
109 | Release process
110 | ---------------
111 |
112 | The new releases are pushed on `Pypi.org `_ automatically
113 | from `GitHub Actions `_ when we add a new tag (unless the
114 | tests are failing).
115 |
116 | In order to prepare a new release, you can use `bumpr `_
117 | which automates a few things.
118 | You first need to install it, then run the ``bumpr`` command. You can then refer
119 | to the `documentation `_
120 | for further details.
121 | For instance, you would run ``bumpr -m`` (replace ``-m`` with ``-p`` or ``-M``
122 | depending the expected version).
123 |
124 | Updating Copyright
125 | ------------------
126 |
127 | If you're a part of a corporation with an NDA, you may be required to update the
128 | LICENSE file. This should be discussed and agreed upon by the project maintainers.
129 |
130 | 1. Check with your legal department first.
131 | 2. Add an appropriate line to the LICENSE file.
132 | 3. When making a commit, add the specific copyright notice.
133 |
134 | Double check with your legal department about their regulations. Not all changes
135 | constitute new or unique work.
136 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Original work Copyright (c) 2013 Twilio, Inc
4 | Modified work Copyright (c) 2014 Axel Haustant
5 | Modified work Copyright (c) 2020 python-restx Authors
6 |
7 | All rights reserved.
8 |
9 | Redistribution and use in source and binary forms, with or without
10 | modification, are permitted provided that the following conditions are met:
11 |
12 | * Redistributions of source code must retain the above copyright notice, this
13 | list of conditions and the following disclaimer.
14 |
15 | * Redistributions in binary form must reproduce the above copyright notice,
16 | this list of conditions and the following disclaimer in the documentation
17 | and/or other materials provided with the distribution.
18 |
19 | * Neither the name of the copyright holder nor the names of its
20 | contributors may be used to endorse or promote products derived from
21 | this software without specific prior written permission.
22 |
23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst MANIFEST.in LICENSE
2 | recursive-include flask_restx *
3 | recursive-include requirements *.pip
4 |
5 | global-exclude *.pyc
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Flask RESTX
3 | ===========
4 |
5 | .. image:: https://github.com/python-restx/flask-restx/workflows/Tests/badge.svg?branch=master&event=push
6 | :target: https://github.com/python-restx/flask-restx/actions?query=workflow%3ATests
7 | :alt: Tests status
8 | .. image:: https://codecov.io/gh/python-restx/flask-restx/branch/master/graph/badge.svg
9 | :target: https://codecov.io/gh/python-restx/flask-restx
10 | :alt: Code coverage
11 | .. image:: https://readthedocs.org/projects/flask-restx/badge/?version=latest
12 | :target: https://flask-restx.readthedocs.io/en/latest/
13 | :alt: Documentation status
14 | .. image:: https://img.shields.io/pypi/l/flask-restx.svg
15 | :target: https://pypi.org/project/flask-restx
16 | :alt: License
17 | .. image:: https://img.shields.io/pypi/pyversions/flask-restx.svg
18 | :target: https://pypi.org/project/flask-restx
19 | :alt: Supported Python versions
20 | .. image:: https://badges.gitter.im/Join%20Chat.svg
21 | :target: https://gitter.im/python-restx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
22 | :alt: Join the chat at https://gitter.im/python-restx
23 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
24 | :target: https://github.com/psf/black
25 | :alt: Code style: black
26 |
27 |
28 | Flask-RESTX is a community driven fork of `Flask-RESTPlus `_.
29 |
30 |
31 | Flask-RESTX is an extension for `Flask`_ that adds support for quickly building REST APIs.
32 | Flask-RESTX encourages best practices with minimal setup.
33 | If you are familiar with Flask, Flask-RESTX should be easy to pick up.
34 | It provides a coherent collection of decorators and tools to describe your API
35 | and expose its documentation properly using `Swagger`_.
36 |
37 |
38 | Compatibility
39 | =============
40 |
41 | Flask-RESTX requires Python 3.9+.
42 |
43 | On Flask Compatibility
44 | ======================
45 |
46 | Flask and Werkzeug moved to versions 2.0 in March 2020. This caused a breaking change in Flask-RESTX.
47 |
48 | .. list-table:: RESTX and Flask / Werkzeug Compatibility
49 | :widths: 25 25 25
50 | :header-rows: 1
51 |
52 |
53 | * - Flask-RESTX version
54 | - Flask version
55 | - Note
56 | * - <= 0.3.0
57 | - < 2.0.0
58 | - unpinned in Flask-RESTX. Pin your projects!
59 | * - == 0.4.0
60 | - < 2.0.0
61 | - pinned in Flask-RESTX.
62 | * - >= 0.5.0
63 | - < 3.0.0
64 | - unpinned, import statements wrapped for compatibility
65 | * - == 1.2.0
66 | - < 3.0.0
67 | - pinned in Flask-RESTX.
68 | * - >= 1.3.0
69 | - >= 2.0.0 (Flask >= 3.0.0 support)
70 | - unpinned, import statements wrapped for compatibility
71 | * - trunk branch in Github
72 | - >= 2.0.0 (Flask >= 3.0.0 support)
73 | - unpinned, will address issues faster than releases.
74 |
75 | Installation
76 | ============
77 |
78 | You can install Flask-RESTX with pip:
79 |
80 | .. code-block:: console
81 |
82 | $ pip install flask-restx
83 |
84 | or with easy_install:
85 |
86 | .. code-block:: console
87 |
88 | $ easy_install flask-restx
89 |
90 |
91 | Quick start
92 | ===========
93 |
94 | With Flask-RESTX, you only import the api instance to route and document your endpoints.
95 |
96 | .. code-block:: python
97 |
98 | from flask import Flask
99 | from flask_restx import Api, Resource, fields
100 |
101 | app = Flask(__name__)
102 | api = Api(app, version='1.0', title='TodoMVC API',
103 | description='A simple TodoMVC API',
104 | )
105 |
106 | ns = api.namespace('todos', description='TODO operations')
107 |
108 | todo = api.model('Todo', {
109 | 'id': fields.Integer(readonly=True, description='The task unique identifier'),
110 | 'task': fields.String(required=True, description='The task details')
111 | })
112 |
113 |
114 | class TodoDAO(object):
115 | def __init__(self):
116 | self.counter = 0
117 | self.todos = []
118 |
119 | def get(self, id):
120 | for todo in self.todos:
121 | if todo['id'] == id:
122 | return todo
123 | api.abort(404, "Todo {} doesn't exist".format(id))
124 |
125 | def create(self, data):
126 | todo = data
127 | todo['id'] = self.counter = self.counter + 1
128 | self.todos.append(todo)
129 | return todo
130 |
131 | def update(self, id, data):
132 | todo = self.get(id)
133 | todo.update(data)
134 | return todo
135 |
136 | def delete(self, id):
137 | todo = self.get(id)
138 | self.todos.remove(todo)
139 |
140 |
141 | DAO = TodoDAO()
142 | DAO.create({'task': 'Build an API'})
143 | DAO.create({'task': '?????'})
144 | DAO.create({'task': 'profit!'})
145 |
146 |
147 | @ns.route('/')
148 | class TodoList(Resource):
149 | '''Shows a list of all todos, and lets you POST to add new tasks'''
150 | @ns.doc('list_todos')
151 | @ns.marshal_list_with(todo)
152 | def get(self):
153 | '''List all tasks'''
154 | return DAO.todos
155 |
156 | @ns.doc('create_todo')
157 | @ns.expect(todo)
158 | @ns.marshal_with(todo, code=201)
159 | def post(self):
160 | '''Create a new task'''
161 | return DAO.create(api.payload), 201
162 |
163 |
164 | @ns.route('/')
165 | @ns.response(404, 'Todo not found')
166 | @ns.param('id', 'The task identifier')
167 | class Todo(Resource):
168 | '''Show a single todo item and lets you delete them'''
169 | @ns.doc('get_todo')
170 | @ns.marshal_with(todo)
171 | def get(self, id):
172 | '''Fetch a given resource'''
173 | return DAO.get(id)
174 |
175 | @ns.doc('delete_todo')
176 | @ns.response(204, 'Todo deleted')
177 | def delete(self, id):
178 | '''Delete a task given its identifier'''
179 | DAO.delete(id)
180 | return '', 204
181 |
182 | @ns.expect(todo)
183 | @ns.marshal_with(todo)
184 | def put(self, id):
185 | '''Update a task given its identifier'''
186 | return DAO.update(id, api.payload)
187 |
188 |
189 | if __name__ == '__main__':
190 | app.run(debug=True)
191 |
192 |
193 | Contributors
194 | ============
195 |
196 | Flask-RESTX is brought to you by @python-restx. Since early 2019 @SteadBytes,
197 | @a-luna, @j5awry, @ziirish volunteered to help @python-restx keep the project up
198 | and running, they did so for a long time! Since the beginning of 2023, the project
199 | is maintained by @peter-doggart with help from @ziirish.
200 | Of course everyone is welcome to contribute and we will be happy to review your
201 | PR's or answer to your issues.
202 |
203 |
204 | Documentation
205 | =============
206 |
207 | The documentation is hosted `on Read the Docs `_
208 |
209 |
210 | .. _Flask: https://flask.palletsprojects.com/
211 | .. _Swagger: https://swagger.io/
212 |
213 |
214 | Contribution
215 | ============
216 | Want to contribute! That's awesome! Check out `CONTRIBUTING.rst! `_
217 |
--------------------------------------------------------------------------------
/bumpr.rc:
--------------------------------------------------------------------------------
1 | [bumpr]
2 | file = flask_restx/__about__.py
3 | vcs = git
4 | commit = true
5 | tag = true
6 | push = true
7 | tests = tox -e py38
8 | clean =
9 | inv clean
10 | files =
11 | README.rst
12 |
13 | [bump]
14 | unsuffix = true
15 |
16 | [prepare]
17 | part = patch
18 | suffix = dev
19 |
20 | [readthedoc]
21 | id = flask-restx
22 |
23 | [replace]
24 | dev = ?branch=master
25 | stable = ?tag={version}
26 |
--------------------------------------------------------------------------------
/coverage.rc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = flask_restx
3 | branch = True
4 | omit =
5 | /tests/*
6 |
7 | [report]
8 | # Regexes for lines to exclude from consideration
9 | exclude_lines =
10 | # Have to re-enable the standard pragma
11 | pragma: no cover
12 |
13 | # Don't complain about missing debug-only code:
14 | def __repr__
15 | if self\.debug
16 |
17 | # Don't complain if tests don't hit defensive assertion code:
18 | raise AssertionError
19 | raise NotImplementedError
20 |
21 | # Don't complain if non-runnable code isn't run:
22 | if 0:
23 | if __name__ == .__main__.:
24 |
25 | ignore_errors = True
26 |
--------------------------------------------------------------------------------
/doc/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 https://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/Flask-RESTX.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-RESTX.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/Flask-RESTX"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-RESTX"
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."
178 |
--------------------------------------------------------------------------------
/doc/_static/apple-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/apple-180.png
--------------------------------------------------------------------------------
/doc/_static/favicon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/favicon-128.png
--------------------------------------------------------------------------------
/doc/_static/favicon-196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/favicon-196.png
--------------------------------------------------------------------------------
/doc/_static/favicon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/favicon-512.png
--------------------------------------------------------------------------------
/doc/_static/favicon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/favicon-64.png
--------------------------------------------------------------------------------
/doc/_static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/favicon.ico
--------------------------------------------------------------------------------
/doc/_static/logo-512-nobg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/logo-512-nobg.png
--------------------------------------------------------------------------------
/doc/_static/logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/logo-512.png
--------------------------------------------------------------------------------
/doc/_static/screenshot-apidoc-quickstart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/doc/_static/screenshot-apidoc-quickstart.png
--------------------------------------------------------------------------------
/doc/_themes/restx/badges.html:
--------------------------------------------------------------------------------
1 |
2 | {% if theme_badges %}
3 |
4 | {% for badge, target, alt in theme_badges %}
5 | 
6 | {% endfor %}
7 | {% endif %}
8 |
--------------------------------------------------------------------------------
/doc/_themes/restx/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "alabaster/layout.html" %}
2 |
3 | {%- block extrahead %}
4 | {% if theme_favicons %}
5 | {% for size, file in theme_favicons.items() %}
6 |
7 | {% endfor %}
8 | {% endif %}
9 | {{ super() }}
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/doc/_themes/restx/static/restx.css:
--------------------------------------------------------------------------------
1 | @import url("alabaster.css");
2 |
3 | .sphinxsidebar p.badge a {
4 | border: none;
5 | }
6 |
7 | .sphinxsidebar hr.badges {
8 | border: 0;
9 | border-bottom: 1px dashed #aaa;
10 | background: none;
11 | /*width: 100%;*/
12 | }
13 |
--------------------------------------------------------------------------------
/doc/_themes/restx/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = alabaster
3 | stylesheet = restx.css
4 |
5 | [options]
6 | favicons=
7 | badges=
8 |
--------------------------------------------------------------------------------
/doc/api.rst:
--------------------------------------------------------------------------------
1 | .. _api:
2 |
3 | API
4 | ===
5 |
6 | .. currentmodule:: flask_restx
7 |
8 | Core
9 | ----
10 |
11 | .. autoclass:: Api
12 | :members:
13 | :inherited-members:
14 |
15 | .. autoclass:: Namespace
16 | :members:
17 |
18 |
19 | .. autoclass:: Resource
20 | :members:
21 | :inherited-members:
22 |
23 |
24 | Models
25 | ------
26 |
27 | .. autoclass:: flask_restx.Model
28 | :members:
29 |
30 | All fields accept a ``required`` boolean and a ``description`` string in ``kwargs``.
31 |
32 | .. automodule:: flask_restx.fields
33 | :members:
34 |
35 |
36 | Serialization
37 | -------------
38 | .. currentmodule:: flask_restx
39 |
40 | .. autofunction:: marshal
41 |
42 | .. autofunction:: marshal_with
43 |
44 | .. autofunction:: marshal_with_field
45 |
46 | .. autoclass:: flask_restx.mask.Mask
47 | :members:
48 |
49 | .. autofunction:: flask_restx.mask.apply
50 |
51 |
52 | Request parsing
53 | ---------------
54 |
55 | .. automodule:: flask_restx.reqparse
56 | :members:
57 |
58 | Inputs
59 | ~~~~~~
60 |
61 | .. automodule:: flask_restx.inputs
62 | :members:
63 |
64 |
65 | Errors
66 | ------
67 |
68 | .. automodule:: flask_restx.errors
69 | :members:
70 |
71 | .. autoexception:: flask_restx.fields.MarshallingError
72 |
73 | .. autoexception:: flask_restx.mask.MaskError
74 |
75 | .. autoexception:: flask_restx.mask.ParseError
76 |
77 |
78 | Schemas
79 | -------
80 |
81 | .. automodule:: flask_restx.schemas
82 | :members:
83 |
84 |
85 | Internals
86 | ---------
87 |
88 | These are internal classes or helpers.
89 | Most of the time you shouldn't have to deal directly with them.
90 |
91 | .. autoclass:: flask_restx.api.SwaggerView
92 |
93 | .. autoclass:: flask_restx.swagger.Swagger
94 |
95 | .. autoclass:: flask_restx.postman.PostmanCollectionV1
96 |
97 | .. automodule:: flask_restx.utils
98 | :members:
99 |
--------------------------------------------------------------------------------
/doc/configuration.rst:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | Flask-RESTX provides the following `Flask configuration values `_:
5 |
6 | Note: Values with no additional description should be covered in more detail
7 | elsewhere in the documentation. If not, please open an issue on GitHub.
8 |
9 | .. py:data:: RESTX_JSON
10 |
11 | Provide global configuration options for JSON serialisation as a :class:`dict`
12 | of :func:`json.dumps` keyword arguments.
13 |
14 | .. py:data:: RESTX_VALIDATE
15 |
16 | Whether to enforce payload validation by default when using the
17 | ``@api.expect()`` decorator. See the `@api.expect()
18 | `__ documentation for details.
19 | This setting defaults to ``False``.
20 |
21 | .. py:data:: RESTX_MASK_HEADER
22 |
23 | Choose the name of the *Header* that will contain the masks to apply to your
24 | answer. See the `Fields masks `__ documentation for details.
25 | This setting defaults to ``X-Fields``.
26 |
27 | .. py:data:: RESTX_MASK_SWAGGER
28 |
29 | Whether to enable the mask documentation in your swagger or not. See the
30 | `mask usage `__ documentation for details.
31 | This setting defaults to ``True``.
32 |
33 | .. py:data:: RESTX_INCLUDE_ALL_MODELS
34 |
35 | This option allows you to include all defined models in the generated Swagger
36 | documentation, even if they are not explicitly used in either ``expect`` nor
37 | ``marshal_with`` decorators.
38 | This setting defaults to ``False``.
39 |
40 | .. py:data:: BUNDLE_ERRORS
41 |
42 | Bundle all the validation errors instead of returning only the first one
43 | encountered. See the `Error Handling `__ section
44 | of the documentation for details.
45 | This setting defaults to ``False``.
46 |
47 | .. py:data:: ERROR_404_HELP
48 |
49 | .. py:data:: HTTP_BASIC_AUTH_REALM
50 |
51 | .. py:data:: SWAGGER_VALIDATOR_URL
52 |
53 | .. py:data:: SWAGGER_UI_DOC_EXPANSION
54 |
55 | .. py:data:: SWAGGER_UI_OPERATION_ID
56 |
57 | .. py:data:: SWAGGER_UI_REQUEST_DURATION
58 |
59 | .. py:data:: SWAGGER_UI_OAUTH_APP_NAME
60 |
61 | .. py:data:: SWAGGER_UI_OAUTH_CLIENT_ID
62 |
63 | .. py:data:: SWAGGER_UI_OAUTH_REALM
64 |
65 | .. py:data:: SWAGGER_SUPPORTED_SUBMIT_METHODS
66 |
--------------------------------------------------------------------------------
/doc/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/doc/errors.rst:
--------------------------------------------------------------------------------
1 | Error handling
2 | ==============
3 |
4 | .. currentmodule:: flask_restx
5 |
6 | HTTPException handling
7 | ----------------------
8 |
9 | Werkzeug HTTPException are automatically properly seriliazed
10 | reusing the description attribute.
11 |
12 | .. code-block:: python
13 |
14 | from werkzeug.exceptions import BadRequest
15 | raise BadRequest()
16 |
17 | will return a 400 HTTP code and output
18 |
19 | .. code-block:: json
20 |
21 | {
22 | "message": "The browser (or proxy) sent a request that this server could not understand."
23 | }
24 |
25 | whereas this:
26 |
27 | .. code-block:: python
28 |
29 | from werkzeug.exceptions import BadRequest
30 | raise BadRequest('My custom message')
31 |
32 | will output
33 |
34 | .. code-block:: json
35 |
36 | {
37 | "message": "My custom message"
38 | }
39 |
40 | You can attach extras attributes to the output by providing a data attribute to your exception.
41 |
42 | .. code-block:: python
43 |
44 | from werkzeug.exceptions import BadRequest
45 | e = BadRequest('My custom message')
46 | e.data = {'custom': 'value'}
47 | raise e
48 |
49 | will output
50 |
51 | .. code-block:: json
52 |
53 | {
54 | "message": "My custom message",
55 | "custom": "value"
56 | }
57 |
58 | The Flask abort helper
59 | ----------------------
60 |
61 | The :meth:`abort ` helper
62 | properly wraps errors into a :exc:`~werkzeug.exceptions.HTTPException`
63 | so it will have the same behavior.
64 |
65 | .. code-block:: python
66 |
67 | from flask import abort
68 | abort(400)
69 |
70 | will return a 400 HTTP code and output
71 |
72 | .. code-block:: json
73 |
74 | {
75 | "message": "The browser (or proxy) sent a request that this server could not understand."
76 | }
77 |
78 | whereas this:
79 |
80 | .. code-block:: python
81 |
82 | from flask import abort
83 | abort(400, 'My custom message')
84 |
85 | will output
86 |
87 | .. code-block:: json
88 |
89 | {
90 | "message": "My custom message"
91 | }
92 |
93 |
94 | The Flask-RESTX abort helper
95 | -------------------------------
96 |
97 | The :func:`errors.abort` and the :meth:`Namespace.abort` helpers
98 | works like the original Flask :func:`flask.abort`
99 | but it will also add the keyword arguments to the response.
100 |
101 | .. code-block:: python
102 |
103 | from flask_restx import abort
104 | abort(400, custom='value')
105 |
106 | will return a 400 HTTP code and output
107 |
108 | .. code-block:: json
109 |
110 | {
111 | "message": "The browser (or proxy) sent a request that this server could not understand.",
112 | "custom": "value"
113 | }
114 |
115 | whereas this:
116 |
117 | .. code-block:: python
118 |
119 | from flask import abort
120 | abort(400, 'My custom message', custom='value')
121 |
122 | will output
123 |
124 | .. code-block:: json
125 |
126 | {
127 | "message": "My custom message",
128 | "custom": "value"
129 | }
130 |
131 |
132 | The ``@api.errorhandler`` decorator
133 | -----------------------------------
134 |
135 | The :meth:`@api.errorhandler ` decorator
136 | allows you to register a specific handler for a given exception (or any exceptions inherited from it), in the same manner
137 | that you can do with Flask/Blueprint :meth:`@errorhandler ` decorator.
138 |
139 | .. code-block:: python
140 |
141 | @api.errorhandler(RootException)
142 | def handle_root_exception(error):
143 | '''Return a custom message and 400 status code'''
144 | return {'message': 'What you want'}, 400
145 |
146 |
147 | @api.errorhandler(CustomException)
148 | def handle_custom_exception(error):
149 | '''Return a custom message and 400 status code'''
150 | return {'message': 'What you want'}, 400
151 |
152 |
153 | @api.errorhandler(AnotherException)
154 | def handle_another_exception(error):
155 | '''Return a custom message and 500 status code'''
156 | return {'message': error.specific}
157 |
158 |
159 | @api.errorhandler(FakeException)
160 | def handle_fake_exception_with_header(error):
161 | '''Return a custom message and 400 status code'''
162 | return {'message': error.message}, 400, {'My-Header': 'Value'}
163 |
164 |
165 | @api.errorhandler(NoResultFound)
166 | def handle_no_result_exception(error):
167 | '''Return a custom not found error message and 404 status code'''
168 | return {'message': error.specific}, 404
169 |
170 |
171 | .. note ::
172 |
173 | A "NoResultFound" error with description is required by the OpenAPI 2.0 spec. The docstring in the error handle function is output in the swagger.json as the description.
174 |
175 | You can also document the error:
176 |
177 | .. code-block:: python
178 |
179 | @api.errorhandler(FakeException)
180 | @api.marshal_with(error_fields, code=400)
181 | @api.header('My-Header', 'Some description')
182 | def handle_fake_exception_with_header(error):
183 | '''This is a custom error'''
184 | return {'message': error.message}, 400, {'My-Header': 'Value'}
185 |
186 |
187 | @api.route('/test/')
188 | class TestResource(Resource):
189 | def get(self):
190 | '''
191 | Do something
192 |
193 | :raises CustomException: In case of something
194 | '''
195 | pass
196 |
197 | In this example, the ``:raise:`` docstring will be automatically extracted
198 | and the response 400 will be documented properly.
199 |
200 |
201 | It also allows for overriding the default error handler when used without parameter:
202 |
203 | .. code-block:: python
204 |
205 | @api.errorhandler
206 | def default_error_handler(error):
207 | '''Default error handler'''
208 | return {'message': str(error)}, getattr(error, 'code', 500)
209 |
210 | .. note ::
211 |
212 | Flask-RESTX will return a message in the error response by default.
213 | If a custom response is required as an error and the message field is not needed,
214 | it can be disabled by setting ``ERROR_INCLUDE_MESSAGE`` to ``False`` in your application config.
215 |
216 | Error handlers can also be registered on namespaces. An error handler registered on a namespace
217 | will override one registered on the api.
218 |
219 |
220 | .. code-block:: python
221 |
222 | ns = Namespace('cats', description='Cats related operations')
223 |
224 | @ns.errorhandler
225 | def specific_namespace_error_handler(error):
226 | '''Namespace error handler'''
227 | return {'message': str(error)}, getattr(error, 'code', 500)
228 |
--------------------------------------------------------------------------------
/doc/example.rst:
--------------------------------------------------------------------------------
1 | Full example
2 | ============
3 |
4 | Here is a full example of a `TodoMVC `_ API.
5 |
6 | .. code-block:: python
7 |
8 | from flask import Flask
9 | from flask_restx import Api, Resource, fields
10 | from werkzeug.middleware.proxy_fix import ProxyFix
11 |
12 | app = Flask(__name__)
13 | app.wsgi_app = ProxyFix(app.wsgi_app)
14 | api = Api(app, version='1.0', title='TodoMVC API',
15 | description='A simple TodoMVC API',
16 | )
17 |
18 | ns = api.namespace('todos', description='TODO operations')
19 |
20 | todo = api.model('Todo', {
21 | 'id': fields.Integer(readonly=True, description='The task unique identifier'),
22 | 'task': fields.String(required=True, description='The task details')
23 | })
24 |
25 |
26 | class TodoDAO(object):
27 | def __init__(self):
28 | self.counter = 0
29 | self.todos = []
30 |
31 | def get(self, id):
32 | for todo in self.todos:
33 | if todo['id'] == id:
34 | return todo
35 | api.abort(404, "Todo {} doesn't exist".format(id))
36 |
37 | def create(self, data):
38 | todo = data
39 | todo['id'] = self.counter = self.counter + 1
40 | self.todos.append(todo)
41 | return todo
42 |
43 | def update(self, id, data):
44 | todo = self.get(id)
45 | todo.update(data)
46 | return todo
47 |
48 | def delete(self, id):
49 | todo = self.get(id)
50 | self.todos.remove(todo)
51 |
52 |
53 | DAO = TodoDAO()
54 | DAO.create({'task': 'Build an API'})
55 | DAO.create({'task': '?????'})
56 | DAO.create({'task': 'profit!'})
57 |
58 |
59 | @ns.route('/')
60 | class TodoList(Resource):
61 | '''Shows a list of all todos, and lets you POST to add new tasks'''
62 | @ns.doc('list_todos')
63 | @ns.marshal_list_with(todo)
64 | def get(self):
65 | '''List all tasks'''
66 | return DAO.todos
67 |
68 | @ns.doc('create_todo')
69 | @ns.expect(todo)
70 | @ns.marshal_with(todo, code=201)
71 | def post(self):
72 | '''Create a new task'''
73 | return DAO.create(api.payload), 201
74 |
75 |
76 | @ns.route('/')
77 | @ns.response(404, 'Todo not found')
78 | @ns.param('id', 'The task identifier')
79 | class Todo(Resource):
80 | '''Show a single todo item and lets you delete them'''
81 | @ns.doc('get_todo')
82 | @ns.marshal_with(todo)
83 | def get(self, id):
84 | '''Fetch a given resource'''
85 | return DAO.get(id)
86 |
87 | @ns.doc('delete_todo')
88 | @ns.response(204, 'Todo deleted')
89 | def delete(self, id):
90 | '''Delete a task given its identifier'''
91 | DAO.delete(id)
92 | return '', 204
93 |
94 | @ns.expect(todo)
95 | @ns.marshal_with(todo)
96 | def put(self, id):
97 | '''Update a task given its identifier'''
98 | return DAO.update(id, api.payload)
99 |
100 |
101 | if __name__ == '__main__':
102 | app.run(debug=True)
103 |
104 |
105 |
106 | You can find other examples in the `github repository examples folder`_.
107 |
108 | .. _github repository examples folder: https://github.com/python-restx/flask-restx/tree/master/examples
109 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. Flask-RESTX documentation master file, created by
2 | sphinx-quickstart on Wed Aug 13 17:07:14 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Flask-RESTX's documentation!
7 | =======================================
8 |
9 | Flask-RESTX is an extension for Flask that adds support for quickly building REST APIs.
10 | Flask-RESTX encourages best practices with minimal setup.
11 | If you are familiar with Flask, Flask-RESTX should be easy to pick up.
12 | It provides a coherent collection of decorators and tools to describe your API
13 | and expose its documentation properly (using Swagger).
14 |
15 | Flask-RESTX is a community driven fork of `Flask-RESTPlus
16 | `_
17 |
18 |
19 | Why did we fork?
20 | ================
21 |
22 | The community has decided to fork the project due to lack of response from the
23 | original author @noirbizarre. We have been discussing this eventuality for
24 | `a long time `_.
25 |
26 | Things evolved a bit since that discussion and a few of us have been granted
27 | maintainers access to the github project, but only the original author has
28 | access rights on the PyPi project. As such, we been unable to make any actual
29 | releases. To prevent this project from dying out, we have forked it to continue
30 | development and to support our users.
31 |
32 |
33 | Compatibility
34 | =============
35 |
36 | Flask-RESTX requires Python 3.9+.
37 |
38 |
39 | Installation
40 | ============
41 |
42 | You can install Flask-RESTX with pip:
43 |
44 | .. code-block:: console
45 |
46 | $ pip install flask-restx
47 |
48 | or with easy_install:
49 |
50 | .. code-block:: console
51 |
52 | $ easy_install flask-restx
53 |
54 |
55 | Documentation
56 | =============
57 |
58 | This part of the documentation will show you how to get started in using
59 | Flask-RESTX with Flask.
60 |
61 | .. toctree::
62 | :maxdepth: 2
63 |
64 | installation
65 | quickstart
66 | marshalling
67 | parsing
68 | errors
69 | mask
70 | swagger
71 | logging
72 | postman
73 | scaling
74 | example
75 | configuration
76 |
77 |
78 | API Reference
79 | -------------
80 |
81 | If you are looking for information on a specific function, class or
82 | method, this part of the documentation is for you.
83 |
84 | .. toctree::
85 | :maxdepth: 2
86 |
87 | api
88 |
89 | Additional Notes
90 | ----------------
91 |
92 | .. toctree::
93 | :maxdepth: 2
94 |
95 | contributing
96 |
97 |
98 | Indices and tables
99 | ==================
100 |
101 | * :ref:`genindex`
102 | * :ref:`modindex`
103 | * :ref:`search`
104 |
--------------------------------------------------------------------------------
/doc/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | Installation
4 | ============
5 |
6 | Install Flask-RESTX with ``pip``:
7 |
8 | .. code-block:: console
9 |
10 | pip install flask-restx
11 |
12 |
13 | The development version can be downloaded from
14 | `GitHub `_.
15 |
16 | .. code-block:: console
17 |
18 | git clone https://github.com/python-restx/flask-restx.git
19 | cd flask-restx
20 | pip install -e .[dev,test]
21 |
22 |
23 | Flask-RESTX requires Python version 3.9+.
24 | It's also working with PyPy and PyPy3.
25 |
--------------------------------------------------------------------------------
/doc/logging.rst:
--------------------------------------------------------------------------------
1 | Logging
2 | ===============
3 |
4 | Flask-RESTX extends `Flask's logging `_
5 | by providing each ``API`` and ``Namespace`` it's own standard Python :class:`logging.Logger` instance.
6 | This allows separation of logging on a per namespace basis to allow more fine-grained detail and configuration.
7 |
8 | By default, these loggers inherit configuration from the Flask application object logger.
9 |
10 | .. code-block:: python
11 |
12 | import logging
13 |
14 | import flask
15 |
16 | from flask_restx import Api, Resource
17 |
18 | # configure root logger
19 | logging.basicConfig(level=logging.INFO)
20 |
21 | app = flask.Flask(__name__)
22 |
23 | api = Api(app)
24 |
25 |
26 | # each of these loggers uses configuration from app.logger
27 | ns1 = api.namespace('api/v1', description='test')
28 | ns2 = api.namespace('api/v2', description='test')
29 |
30 |
31 | @ns1.route('/my-resource')
32 | class MyResource(Resource):
33 | def get(self):
34 | # will log
35 | ns1.logger.info("hello from ns1")
36 | return {"message": "hello"}
37 |
38 |
39 | @ns2.route('/my-resource')
40 | class MyNewResource(Resource):
41 | def get(self):
42 | # won't log due to INFO log level from app.logger
43 | ns2.logger.debug("hello from ns2")
44 | return {"message": "hello"}
45 |
46 |
47 | Loggers can be configured individually to override the configuration from the Flask
48 | application object logger. In the above example, ``ns2`` log level can be set to
49 | ``DEBUG`` individually:
50 |
51 | .. code-block:: python
52 |
53 | # ns1 will have log level INFO from app.logger
54 | ns1 = api.namespace('api/v1', description='test')
55 |
56 | # ns2 will have log level DEBUG
57 | ns2 = api.namespace('api/v2', description='test')
58 | ns2.logger.setLevel(logging.DEBUG)
59 |
60 |
61 | @ns1.route('/my-resource')
62 | class MyResource(Resource):
63 | def get(self):
64 | # will log
65 | ns1.logger.info("hello from ns1")
66 | return {"message": "hello"}
67 |
68 |
69 | @ns2.route('/my-resource')
70 | class MyNewResource(Resource):
71 | def get(self):
72 | # will log
73 | ns2.logger.debug("hello from ns2")
74 | return {"message": "hello"}
75 |
76 |
77 | Adding additional handlers:
78 |
79 |
80 | .. code-block:: python
81 |
82 | # configure a file handler for ns1 only
83 | ns1 = api.namespace('api/v1')
84 | fh = logging.FileHandler("v1.log")
85 | ns1.logger.addHandler(fh)
86 |
87 | ns2 = api.namespace('api/v2')
88 |
89 |
90 | @ns1.route('/my-resource')
91 | class MyResource(Resource):
92 | def get(self):
93 | # will log to *both* v1.log file and app.logger handlers
94 | ns1.logger.info("hello from ns1")
95 | return {"message": "hello"}
96 |
97 |
98 | @ns2.route('/my-resource')
99 | class MyNewResource(Resource):
100 | def get(self):
101 | # will log to *only* app.logger handlers
102 | ns2.logger.info("hello from ns2")
103 | return {"message": "hello"}
--------------------------------------------------------------------------------
/doc/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.https://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\Flask-RESTX.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-RESTX.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
243 |
--------------------------------------------------------------------------------
/doc/mask.rst:
--------------------------------------------------------------------------------
1 | Fields masks
2 | ============
3 |
4 | Flask-RESTX support partial object fetching (aka. fields mask)
5 | by supplying a custom header in the request.
6 |
7 | By default the header is ``X-Fields``
8 | but it can be changed with the ``RESTX_MASK_HEADER`` parameter.
9 |
10 | Syntax
11 | ------
12 |
13 | The syntax is actually quite simple.
14 | You just provide a coma separated list of field names,
15 | optionally wrapped in brackets.
16 |
17 | .. code-block:: python
18 |
19 | # These two mask are equivalents
20 | mask = '{name,age}'
21 | # or
22 | mask = 'name,age'
23 | data = requests.get('/some/url/', headers={'X-Fields': mask})
24 | assert len(data) == 2
25 | assert 'name' in data
26 | assert 'age' in data
27 |
28 | To specify a nested fields mask,
29 | simply provide it in bracket following the field name:
30 |
31 | .. code-block:: python
32 |
33 | mask = '{name, age, pet{name}}'
34 |
35 | Nesting specification works with nested object or list of objects:
36 |
37 | .. code-block:: python
38 |
39 | # Will apply the mask {name} to each pet
40 | # in the pets list.
41 | mask = '{name, age, pets{name}}'
42 |
43 | There is a special star token meaning "all remaining fields".
44 | It allows to only specify nested filtering:
45 |
46 | .. code-block:: python
47 |
48 | # Will apply the mask {name} to each pet
49 | # in the pets list and take all other root fields
50 | # without filtering.
51 | mask = '{pets{name},*}'
52 |
53 | # Will not filter anything
54 | mask = '*'
55 |
56 |
57 | Usage
58 | -----
59 |
60 | By default, each time you use ``api.marshal`` or ``@api.marshal_with``,
61 | the mask will be automatically applied if the header is present.
62 |
63 | The header will be exposed as a Swagger parameter each time you use the
64 | ``@api.marshal_with`` decorator.
65 |
66 | As Swagger does not permit exposing a global header once
67 | it can make your Swagger specifications a lot more verbose.
68 | You can disable this behavior by setting ``RESTX_MASK_SWAGGER`` to ``False``.
69 |
70 | You can also specify a default mask that will be applied if no header mask is found.
71 |
72 | .. code-block:: python
73 |
74 | class MyResource(Resource):
75 | @api.marshal_with(my_model, mask='name,age')
76 | def get(self):
77 | pass
78 |
79 |
80 | Default mask can also be handled at model level:
81 |
82 | .. code-block:: python
83 |
84 | model = api.model('Person', {
85 | 'name': fields.String,
86 | 'age': fields.Integer,
87 | 'boolean': fields.Boolean,
88 | }, mask='{name,age}')
89 |
90 |
91 | It will be exposed into the model `x-mask` vendor field:
92 |
93 | .. code-block:: JSON
94 |
95 | {"definitions": {
96 | "Test": {
97 | "properties": {
98 | "age": {"type": "integer"},
99 | "boolean": {"type": "boolean"},
100 | "name": {"type": "string"}
101 | },
102 | "x-mask": "{name,age}"
103 | }
104 | }}
105 |
106 | To override default masks, you need to give another mask or pass `*` as mask.
107 |
--------------------------------------------------------------------------------
/doc/postman.rst:
--------------------------------------------------------------------------------
1 | Postman
2 | =======
3 |
4 | To help you testing, you can export your API as a `Postman`_ collection.
5 |
6 | .. code-block:: python
7 |
8 | from flask import json
9 |
10 | from myapp import api
11 |
12 | urlvars = False # Build query strings in URLs
13 | swagger = True # Export Swagger specifications
14 | data = api.as_postman(urlvars=urlvars, swagger=swagger)
15 | print(json.dumps(data))
16 |
17 |
18 | .. _Postman: https://www.getpostman.com/
19 |
--------------------------------------------------------------------------------
/doc/scaling.rst:
--------------------------------------------------------------------------------
1 | .. _scaling:
2 |
3 | Scaling your project
4 | ====================
5 |
6 | .. currentmodule:: flask_restx
7 |
8 | This page covers building a slightly more complex Flask-RESTX app that will
9 | cover out some best practices when setting up a real-world Flask-RESTX-based API.
10 | The :ref:`quickstart` section is great for getting started with your first Flask-RESTX app,
11 | so if you're new to Flask-RESTX you'd be better off checking that out first.
12 |
13 |
14 | Multiple namespaces
15 | -------------------
16 |
17 | There are many different ways to organize your Flask-RESTX app,
18 | but here we'll describe one that scales pretty well with larger apps
19 | and maintains a nice level of organization.
20 |
21 | Flask-RESTX provides a way to use almost the same pattern as Flask's `blueprint`.
22 | The main idea is to split your app into reusable namespaces.
23 |
24 | Here's an example directory structure::
25 |
26 | project/
27 | ├── app.py
28 | ├── core
29 | │ ├── __init__.py
30 | │ ├── utils.py
31 | │ └── ...
32 | └── apis
33 | ├── __init__.py
34 | ├── namespace1.py
35 | ├── namespace2.py
36 | ├── ...
37 | └── namespaceX.py
38 |
39 |
40 | The `app` module will serve as a main application entry point following one of the classic
41 | Flask patterns (See :doc:`flask:patterns/packages` and :doc:`flask:patterns/appfactories`).
42 |
43 | The `core` module is an example, it contains the business logic.
44 | In fact, you call it whatever you want, and there can be many packages.
45 |
46 | The `apis` package will be your main API entry point that you need to import and register on the application,
47 | whereas the namespaces modules are reusable namespaces designed like you would do with Flask's Blueprint.
48 |
49 | A namespace module contains models and resources declarations.
50 | For example:
51 |
52 | .. code-block:: Python
53 |
54 | from flask_restx import Namespace, Resource, fields
55 |
56 | api = Namespace('cats', description='Cats related operations')
57 |
58 | cat = api.model('Cat', {
59 | 'id': fields.String(required=True, description='The cat identifier'),
60 | 'name': fields.String(required=True, description='The cat name'),
61 | })
62 |
63 | CATS = [
64 | {'id': 'felix', 'name': 'Felix'},
65 | ]
66 |
67 | @api.route('/')
68 | class CatList(Resource):
69 | @api.doc('list_cats')
70 | @api.marshal_list_with(cat)
71 | def get(self):
72 | '''List all cats'''
73 | return CATS
74 |
75 | @api.route('/')
76 | @api.param('id', 'The cat identifier')
77 | @api.response(404, 'Cat not found')
78 | class Cat(Resource):
79 | @api.doc('get_cat')
80 | @api.marshal_with(cat)
81 | def get(self, id):
82 | '''Fetch a cat given its identifier'''
83 | for cat in CATS:
84 | if cat['id'] == id:
85 | return cat
86 | api.abort(404)
87 |
88 |
89 | The `apis.__init__` module should aggregate them:
90 |
91 | .. code-block:: Python
92 |
93 | from flask_restx import Api
94 |
95 | from .namespace1 import api as ns1
96 | from .namespace2 import api as ns2
97 | # ...
98 | from .namespaceX import api as nsX
99 |
100 | api = Api(
101 | title='My Title',
102 | version='1.0',
103 | description='A description',
104 | # All API metadatas
105 | )
106 |
107 | api.add_namespace(ns1)
108 | api.add_namespace(ns2)
109 | # ...
110 | api.add_namespace(nsX)
111 |
112 |
113 | You can define custom url-prefixes for namespaces during registering them in your API.
114 | You don't have to bind url-prefix while declaration of Namespace object.
115 |
116 | .. code-block:: Python
117 |
118 | from flask_restx import Api
119 |
120 | from .namespace1 import api as ns1
121 | from .namespace2 import api as ns2
122 | # ...
123 | from .namespaceX import api as nsX
124 |
125 | api = Api(
126 | title='My Title',
127 | version='1.0',
128 | description='A description',
129 | # All API metadatas
130 | )
131 |
132 | api.add_namespace(ns1, path='/prefix/of/ns1')
133 | api.add_namespace(ns2, path='/prefix/of/ns2')
134 | # ...
135 | api.add_namespace(nsX, path='/prefix/of/nsX')
136 |
137 |
138 | Using this pattern, you simply have to register your API in `app.py` like that:
139 |
140 | .. code-block:: Python
141 |
142 | from flask import Flask
143 | from apis import api
144 |
145 | app = Flask(__name__)
146 | api.init_app(app)
147 |
148 | app.run(debug=True)
149 |
150 |
151 | Use With Blueprints
152 | -------------------
153 |
154 | See :doc:`flask:blueprints` in the Flask documentation for what blueprints are and why you should use them.
155 | Here's an example of how to link an :class:`Api` up to a :class:`~flask.Blueprint`. Nested Blueprints are
156 | not supported.
157 |
158 | .. code-block:: python
159 |
160 | from flask import Blueprint
161 | from flask_restx import Api
162 |
163 | blueprint = Blueprint('api', __name__)
164 | api = Api(blueprint)
165 | # ...
166 |
167 | Using a `blueprint` will allow you to mount your API on any url prefix and/or subdomain
168 | in you application:
169 |
170 |
171 | .. code-block:: Python
172 |
173 | from flask import Flask
174 | from apis import blueprint as api
175 |
176 | app = Flask(__name__)
177 | app.register_blueprint(api, url_prefix='/api/1')
178 | app.run(debug=True)
179 |
180 | .. note ::
181 |
182 | Calling :meth:`Api.init_app` is not required here because registering the
183 | blueprint with the app takes care of setting up the routing for the application.
184 |
185 | .. note::
186 |
187 | When using blueprints, remember to use the blueprint name with :func:`~flask.url_for`:
188 |
189 | .. code-block:: python
190 |
191 | # without blueprint
192 | url_for('my_api_endpoint')
193 |
194 | # with blueprint
195 | url_for('api.my_api_endpoint')
196 |
197 |
198 | Multiple APIs with reusable namespaces
199 | --------------------------------------
200 |
201 | Sometimes you need to maintain multiple versions of an API.
202 | If you built your API using namespaces composition,
203 | it's quite simple to scale it to multiple APIs.
204 |
205 | Given the previous layout, we can migrate it to the following directory structure::
206 |
207 | project/
208 | ├── app.py
209 | ├── apiv1.py
210 | ├── apiv2.py
211 | └── apis
212 | ├── __init__.py
213 | ├── namespace1.py
214 | ├── namespace2.py
215 | ├── ...
216 | └── namespaceX.py
217 |
218 | Each `apis/namespaceX` module will have the following pattern:
219 |
220 | .. code-block:: python
221 |
222 | from flask_restx import Namespace, Resource
223 |
224 | api = Namespace('mynamespace', 'Namespace Description' )
225 |
226 | @api.route("/")
227 | class Myclass(Resource):
228 | def get(self):
229 | return {}
230 |
231 | Each `apivX` module will have the following pattern:
232 |
233 | .. code-block:: python
234 |
235 | from flask import Blueprint
236 | from flask_restx import Api
237 |
238 | api = Api(blueprint)
239 |
240 | from .apis.namespace1 import api as ns1
241 | from .apis.namespace2 import api as ns2
242 | # ...
243 | from .apis.namespaceX import api as nsX
244 |
245 | blueprint = Blueprint('api', __name__, url_prefix='/api/1')
246 | api = Api(blueprint,
247 | title='My Title',
248 | version='1.0',
249 | description='A description',
250 | # All API metadatas
251 | )
252 |
253 | api.add_namespace(ns1)
254 | api.add_namespace(ns2)
255 | # ...
256 | api.add_namespace(nsX)
257 |
258 | And the app will simply mount them:
259 |
260 | .. code-block:: Python
261 |
262 | from flask import Flask
263 | from api1 import blueprint as api1
264 | from apiX import blueprint as apiX
265 |
266 | app = Flask(__name__)
267 | app.register_blueprint(api1)
268 | app.register_blueprint(apiX)
269 | app.run(debug=True)
270 |
271 |
272 | These are only proposals and you can do whatever suits your needs.
273 | Look at the `github repository examples folder`_ for more complete examples.
274 |
275 | .. _github repository examples folder: https://github.com/python-restx/flask-restx/tree/master/examples
276 |
--------------------------------------------------------------------------------
/examples/resource_class_kwargs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | from flask_restx import Resource, Namespace
6 | from flask import Blueprint, Flask
7 | from flask_restx import Api, fields, Model
8 |
9 | ### models.py contain models for data validation ###
10 | # just a simple model
11 | MyTest = Model('MyTest', {
12 | 'data': fields.String(required=True,readonly=True),
13 | })
14 |
15 | ### namespaces.py contains definition of routes
16 | namesp = Namespace(name="tests", validate=True)
17 | # register model
18 | namesp.models[MyTest.name] = MyTest
19 | @namesp.route('/')
20 | class get_session(Resource):
21 |
22 | def __init__(self, api=None, *args, **kwargs):
23 | # sessions is a black box dependency
24 | self.answer_service = kwargs['answer_service']
25 | super().__init__(api,*args, **kwargs)
26 |
27 | @namesp.marshal_with(MyTest)
28 | def get(self, message):
29 | # ducktyping
30 | # any used answer_service must implement this method somehow
31 | return self.answer_service.answer(message)
32 |
33 |
34 | ### managers.py contain logic what should happen on request ###
35 |
36 | # loosly coupled and independent from communication
37 | # could be implemented with database, log file what so ever
38 | class AnswerService:
39 | def __init__(self,msg):
40 | self.msg=msg
41 | def answer(self, request:str):
42 | return {'data': request+self.msg}
43 |
44 | #### main.py ###
45 | blueprint = Blueprint("api", __name__, url_prefix="/api/v1")
46 |
47 | api = Api(
48 | blueprint,
49 | version="1.0",
50 | doc="/ui",
51 | validate=False,
52 | )
53 |
54 | # main glues communication and managers together
55 | ans= AnswerService('~nice to meet you')
56 | injected_objects={'answer_service': ans}
57 |
58 | ### could also be defined without namespace ###
59 | #api.models[MyTest.name] = MyTest
60 | #api.add_resource(get_session, '/answer',
61 | # resource_class_kwargs=injected_objects)
62 |
63 |
64 | # inject the objects containing logic here
65 | for res in namesp.resources:
66 | res.kwargs['resource_class_kwargs'] = injected_objects
67 | print(res)
68 | # finally add namespace to api
69 | api.add_namespace(namesp)
70 |
71 | app = Flask('test')
72 | from flask import redirect
73 | @app.route('/', methods=['POST', 'GET'])
74 | def home():
75 | return redirect('/api/v1/ui')
76 |
77 | app.register_blueprint(blueprint)
78 | app.run(debug=False, port=8002)
79 |
--------------------------------------------------------------------------------
/examples/todo.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask_restx import Api, Resource, fields
3 | from werkzeug.middleware.proxy_fix import ProxyFix
4 |
5 | app = Flask(__name__)
6 | app.wsgi_app = ProxyFix(app.wsgi_app)
7 | api = Api(
8 | app,
9 | version="1.0",
10 | title="Todo API",
11 | description="A simple TODO API",
12 | )
13 |
14 | ns = api.namespace("todos", description="TODO operations")
15 |
16 | TODOS = {
17 | "todo1": {"task": "build an API"},
18 | "todo2": {"task": "?????"},
19 | "todo3": {"task": "profit!"},
20 | }
21 |
22 | todo = api.model(
23 | "Todo", {"task": fields.String(required=True, description="The task details")}
24 | )
25 |
26 | listed_todo = api.model(
27 | "ListedTodo",
28 | {
29 | "id": fields.String(required=True, description="The todo ID"),
30 | "todo": fields.Nested(todo, description="The Todo"),
31 | },
32 | )
33 |
34 |
35 | def abort_if_todo_doesnt_exist(todo_id):
36 | if todo_id not in TODOS:
37 | api.abort(404, "Todo {} doesn't exist".format(todo_id))
38 |
39 |
40 | parser = api.parser()
41 | parser.add_argument(
42 | "task", type=str, required=True, help="The task details", location="form"
43 | )
44 |
45 |
46 | @ns.route("/")
47 | @api.doc(responses={404: "Todo not found"}, params={"todo_id": "The Todo ID"})
48 | class Todo(Resource):
49 | """Show a single todo item and lets you delete them"""
50 |
51 | @api.doc(description="todo_id should be in {0}".format(", ".join(TODOS.keys())))
52 | @api.marshal_with(todo)
53 | def get(self, todo_id):
54 | """Fetch a given resource"""
55 | abort_if_todo_doesnt_exist(todo_id)
56 | return TODOS[todo_id]
57 |
58 | @api.doc(responses={204: "Todo deleted"})
59 | def delete(self, todo_id):
60 | """Delete a given resource"""
61 | abort_if_todo_doesnt_exist(todo_id)
62 | del TODOS[todo_id]
63 | return "", 204
64 |
65 | @api.doc(parser=parser)
66 | @api.marshal_with(todo)
67 | def put(self, todo_id):
68 | """Update a given resource"""
69 | args = parser.parse_args()
70 | task = {"task": args["task"]}
71 | TODOS[todo_id] = task
72 | return task
73 |
74 |
75 | @ns.route("/")
76 | class TodoList(Resource):
77 | """Shows a list of all todos, and lets you POST to add new tasks"""
78 |
79 | @api.marshal_list_with(listed_todo)
80 | def get(self):
81 | """List all todos"""
82 | return [{"id": id, "todo": todo} for id, todo in TODOS.items()]
83 |
84 | @api.doc(parser=parser)
85 | @api.marshal_with(todo, code=201)
86 | def post(self):
87 | """Create a todo"""
88 | args = parser.parse_args()
89 | todo_id = "todo%d" % (len(TODOS) + 1)
90 | TODOS[todo_id] = {"task": args["task"]}
91 | return TODOS[todo_id], 201
92 |
93 |
94 | if __name__ == "__main__":
95 | app.run(debug=True)
96 |
--------------------------------------------------------------------------------
/examples/todo_blueprint.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, Blueprint
2 | from flask_restx import Api, Resource, fields
3 |
4 | api_v1 = Blueprint("api", __name__, url_prefix="/api/1")
5 |
6 | api = Api(
7 | api_v1,
8 | version="1.0",
9 | title="Todo API",
10 | description="A simple TODO API",
11 | )
12 |
13 | ns = api.namespace("todos", description="TODO operations")
14 |
15 | TODOS = {
16 | "todo1": {"task": "build an API"},
17 | "todo2": {"task": "?????"},
18 | "todo3": {"task": "profit!"},
19 | }
20 |
21 | todo = api.model(
22 | "Todo", {"task": fields.String(required=True, description="The task details")}
23 | )
24 |
25 | listed_todo = api.model(
26 | "ListedTodo",
27 | {
28 | "id": fields.String(required=True, description="The todo ID"),
29 | "todo": fields.Nested(todo, description="The Todo"),
30 | },
31 | )
32 |
33 |
34 | def abort_if_todo_doesnt_exist(todo_id):
35 | if todo_id not in TODOS:
36 | api.abort(404, "Todo {} doesn't exist".format(todo_id))
37 |
38 |
39 | parser = api.parser()
40 | parser.add_argument(
41 | "task", type=str, required=True, help="The task details", location="form"
42 | )
43 |
44 |
45 | @ns.route("/")
46 | @api.doc(responses={404: "Todo not found"}, params={"todo_id": "The Todo ID"})
47 | class Todo(Resource):
48 | """Show a single todo item and lets you delete them"""
49 |
50 | @api.doc(description="todo_id should be in {0}".format(", ".join(TODOS.keys())))
51 | @api.marshal_with(todo)
52 | def get(self, todo_id):
53 | """Fetch a given resource"""
54 | abort_if_todo_doesnt_exist(todo_id)
55 | return TODOS[todo_id]
56 |
57 | @api.doc(responses={204: "Todo deleted"})
58 | def delete(self, todo_id):
59 | """Delete a given resource"""
60 | abort_if_todo_doesnt_exist(todo_id)
61 | del TODOS[todo_id]
62 | return "", 204
63 |
64 | @api.doc(parser=parser)
65 | @api.marshal_with(todo)
66 | def put(self, todo_id):
67 | """Update a given resource"""
68 | args = parser.parse_args()
69 | task = {"task": args["task"]}
70 | TODOS[todo_id] = task
71 | return task
72 |
73 |
74 | @ns.route("/")
75 | class TodoList(Resource):
76 | """Shows a list of all todos, and lets you POST to add new tasks"""
77 |
78 | @api.marshal_list_with(listed_todo)
79 | def get(self):
80 | """List all todos"""
81 | return [{"id": id, "todo": todo} for id, todo in TODOS.items()]
82 |
83 | @api.doc(parser=parser)
84 | @api.marshal_with(todo, code=201)
85 | def post(self):
86 | """Create a todo"""
87 | args = parser.parse_args()
88 | todo_id = "todo%d" % (len(TODOS) + 1)
89 | TODOS[todo_id] = {"task": args["task"]}
90 | return TODOS[todo_id], 201
91 |
92 |
93 | if __name__ == "__main__":
94 | app = Flask(__name__)
95 | app.register_blueprint(api_v1)
96 | app.run(debug=True)
97 |
--------------------------------------------------------------------------------
/examples/todo_simple.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | from flask_restx import Resource, Api
3 |
4 | app = Flask(__name__)
5 | api = Api(app)
6 |
7 | todos = {}
8 |
9 |
10 | @api.route("/")
11 | class TodoSimple(Resource):
12 | """
13 | You can try this example as follow:
14 | $ curl http://localhost:5000/todo1 -d "data=Remember the milk" -X PUT
15 | $ curl http://localhost:5000/todo1
16 | {"todo1": "Remember the milk"}
17 | $ curl http://localhost:5000/todo2 -d "data=Change my breakpads" -X PUT
18 | $ curl http://localhost:5000/todo2
19 | {"todo2": "Change my breakpads"}
20 |
21 | Or from python if you have requests :
22 | >>> from requests import put, get
23 | >>> put('http://localhost:5000/todo1', data={'data': 'Remember the milk'}).json
24 | {u'todo1': u'Remember the milk'}
25 | >>> get('http://localhost:5000/todo1').json
26 | {u'todo1': u'Remember the milk'}
27 | >>> put('http://localhost:5000/todo2', data={'data': 'Change my breakpads'}).json
28 | {u'todo2': u'Change my breakpads'}
29 | >>> get('http://localhost:5000/todo2').json
30 | {u'todo2': u'Change my breakpads'}
31 |
32 | """
33 |
34 | def get(self, todo_id):
35 | return {todo_id: todos[todo_id]}
36 |
37 | def put(self, todo_id):
38 | todos[todo_id] = request.form["data"]
39 | return {todo_id: todos[todo_id]}
40 |
41 |
42 | if __name__ == "__main__":
43 | app.run(debug=False)
44 |
--------------------------------------------------------------------------------
/examples/todomvc.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask_restx import Api, Resource, fields
3 | from werkzeug.middleware.proxy_fix import ProxyFix
4 |
5 | app = Flask(__name__)
6 | app.wsgi_app = ProxyFix(app.wsgi_app)
7 | api = Api(
8 | app,
9 | version="1.0",
10 | title="TodoMVC API",
11 | description="A simple TodoMVC API",
12 | )
13 |
14 | ns = api.namespace("todos", description="TODO operations")
15 |
16 | todo = api.model(
17 | "Todo",
18 | {
19 | "id": fields.Integer(readonly=True, description="The task unique identifier"),
20 | "task": fields.String(required=True, description="The task details"),
21 | },
22 | )
23 |
24 |
25 | class TodoDAO(object):
26 | def __init__(self):
27 | self.counter = 0
28 | self.todos = []
29 |
30 | def get(self, id):
31 | for todo in self.todos:
32 | if todo["id"] == id:
33 | return todo
34 | api.abort(404, "Todo {} doesn't exist".format(id))
35 |
36 | def create(self, data):
37 | todo = data
38 | todo["id"] = self.counter = self.counter + 1
39 | self.todos.append(todo)
40 | return todo
41 |
42 | def update(self, id, data):
43 | todo = self.get(id)
44 | todo.update(data)
45 | return todo
46 |
47 | def delete(self, id):
48 | todo = self.get(id)
49 | self.todos.remove(todo)
50 |
51 |
52 | todo_dao = TodoDAO()
53 | todo_dao.create({"task": "Build an API"})
54 | todo_dao.create({"task": "?????"})
55 | todo_dao.create({"task": "profit!"})
56 |
57 |
58 | @ns.route("/")
59 | class TodoList(Resource):
60 | """Shows a list of all todos, and lets you POST to add new tasks"""
61 |
62 | @ns.doc("list_todos")
63 | @ns.marshal_list_with(todo)
64 | def get(self):
65 | """List all tasks"""
66 | return todo_dao.todos
67 |
68 | @ns.doc("create_todo")
69 | @ns.expect(todo)
70 | @ns.marshal_with(todo, code=201)
71 | def post(self):
72 | """Create a new task"""
73 | return todo_dao.create(api.payload), 201
74 |
75 |
76 | @ns.route("/")
77 | @ns.response(404, "Todo not found")
78 | @ns.param("id", "The task identifier")
79 | class Todo(Resource):
80 | """Show a single todo item and lets you delete them"""
81 |
82 | @ns.doc("get_todo")
83 | @ns.marshal_with(todo)
84 | def get(self, id):
85 | """Fetch a given resource"""
86 | return todo_dao.get(id)
87 |
88 | @ns.doc("delete_todo")
89 | @ns.response(204, "Todo deleted")
90 | def delete(self, id):
91 | """Delete a task given its identifier"""
92 | todo_dao.delete(id)
93 | return "", 204
94 |
95 | @ns.expect(todo)
96 | @ns.marshal_with(todo)
97 | def put(self, id):
98 | """Update a task given its identifier"""
99 | return todo_dao.update(id, api.payload)
100 |
101 |
102 | if __name__ == "__main__":
103 | app.run(debug=True)
104 |
--------------------------------------------------------------------------------
/examples/xml_representation.py:
--------------------------------------------------------------------------------
1 | # needs: pip install python-simplexml
2 | from simplexml import dumps
3 | from flask import make_response, Flask
4 | from flask_restx import Api, Resource, fields
5 |
6 |
7 | def output_xml(data, code, headers=None):
8 | """Makes a Flask response with a XML encoded body"""
9 | resp = make_response(dumps({"response": data}), code)
10 | resp.headers.extend(headers or {})
11 | return resp
12 |
13 |
14 | app = Flask(__name__)
15 | api = Api(app, default_mediatype="application/xml")
16 | api.representations["application/xml"] = output_xml
17 |
18 | hello_fields = api.model("Hello", {"entry": fields.String})
19 |
20 |
21 | @api.route("/")
22 | class Hello(Resource):
23 | """
24 | # you need requests
25 | >>> from requests import get
26 | >>> get('http://localhost:5000/me').content # default_mediatype
27 | 'me'
28 | >>> get('http://localhost:5000/me', headers={"accept":"application/json"}).content
29 | '{"hello": "me"}'
30 | >>> get('http://localhost:5000/me', headers={"accept":"application/xml"}).content
31 | 'me'
32 | """
33 |
34 | @api.doc(model=hello_fields, params={"entry": "The entry to wrap"})
35 | def get(self, entry):
36 | """Get a wrapped entry"""
37 | return {"hello": entry}
38 |
39 |
40 | if __name__ == "__main__":
41 | app.run(debug=True)
42 |
--------------------------------------------------------------------------------
/examples/zoo_app/complex.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from werkzeug.middleware.proxy_fix import ProxyFix
3 |
4 | from zoo import api
5 |
6 | app = Flask(__name__)
7 | app.wsgi_app = ProxyFix(app.wsgi_app)
8 |
9 | api.init_app(app)
10 |
11 | app.run(debug=True)
12 |
--------------------------------------------------------------------------------
/examples/zoo_app/requirements.txt:
--------------------------------------------------------------------------------
1 | aniso8601==9.0.1
2 | attrs==21.2.0
3 | click==7.1.2
4 | Flask==1.1.4
5 | flask-restx==0.5.1
6 | itsdangerous==1.1.0
7 | Jinja2==2.11.3
8 | jsonschema==3.2.0
9 | MarkupSafe==2.0.1
10 | pyrsistent==0.17.3
11 | six==1.16.0
12 | Werkzeug==2.2.3
13 |
14 |
--------------------------------------------------------------------------------
/examples/zoo_app/zoo/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Api
2 |
3 | from .cat import api as cat_api
4 | from .dog import api as dog_api
5 |
6 | api = Api(
7 | title="Zoo API",
8 | version="1.0",
9 | description="A simple demo API",
10 | )
11 |
12 | api.add_namespace(cat_api)
13 | api.add_namespace(dog_api)
14 |
--------------------------------------------------------------------------------
/examples/zoo_app/zoo/cat.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Namespace, Resource, fields
2 |
3 | api = Namespace("cats", description="Cats related operations")
4 |
5 | cat = api.model(
6 | "Cat",
7 | {
8 | "id": fields.String(required=True, description="The cat identifier"),
9 | "name": fields.String(required=True, description="The cat name"),
10 | },
11 | )
12 |
13 | CATS = [
14 | {"id": "felix", "name": "Felix"},
15 | ]
16 |
17 |
18 | @api.route("/")
19 | class CatList(Resource):
20 | @api.doc("list_cats")
21 | @api.marshal_list_with(cat)
22 | def get(self):
23 | """List all cats"""
24 | return CATS
25 |
26 |
27 | @api.route("/")
28 | @api.param("id", "The cat identifier")
29 | @api.response(404, "Cat not found")
30 | class Cat(Resource):
31 | @api.doc("get_cat")
32 | @api.marshal_with(cat)
33 | def get(self, id):
34 | """Fetch a cat given its identifier"""
35 | for cat in CATS:
36 | if cat["id"] == id:
37 | return cat
38 | api.abort(404)
39 |
--------------------------------------------------------------------------------
/examples/zoo_app/zoo/dog.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Namespace, Resource, fields
2 |
3 | api = Namespace("dogs", description="Dogs related operations")
4 |
5 | dog = api.model(
6 | "Dog",
7 | {
8 | "id": fields.String(required=True, description="The dog identifier"),
9 | "name": fields.String(required=True, description="The dog name"),
10 | },
11 | )
12 |
13 | DOGS = [
14 | {"id": "medor", "name": "Medor"},
15 | ]
16 |
17 |
18 | @api.route("/")
19 | class DogList(Resource):
20 | @api.doc("list_dogs")
21 | @api.marshal_list_with(dog)
22 | def get(self):
23 | """List all dogs"""
24 | return DOGS
25 |
26 |
27 | @api.route("/")
28 | @api.param("id", "The dog identifier")
29 | @api.response(404, "Dog not found")
30 | class Dog(Resource):
31 | @api.doc("get_dog")
32 | @api.marshal_with(dog)
33 | def get(self, id):
34 | """Fetch a dog given its identifier"""
35 | for dog in DOGS:
36 | if dog["id"] == id:
37 | return dog
38 | api.abort(404)
39 |
--------------------------------------------------------------------------------
/flask_restx/__about__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __version__ = "1.3.1.dev"
3 | __description__ = (
4 | "Fully featured framework for fast, easy and documented API development with Flask"
5 | )
6 |
--------------------------------------------------------------------------------
/flask_restx/__init__.py:
--------------------------------------------------------------------------------
1 | from . import fields, reqparse, apidoc, inputs, cors
2 | from .api import Api # noqa
3 | from .marshalling import marshal, marshal_with, marshal_with_field # noqa
4 | from .mask import Mask
5 | from .model import Model, OrderedModel, SchemaModel # noqa
6 | from .namespace import Namespace # noqa
7 | from .resource import Resource # noqa
8 | from .errors import abort, RestError, SpecsError, ValidationError
9 | from .swagger import Swagger
10 | from .__about__ import __version__, __description__
11 |
12 | __all__ = (
13 | "__version__",
14 | "__description__",
15 | "Api",
16 | "Resource",
17 | "apidoc",
18 | "marshal",
19 | "marshal_with",
20 | "marshal_with_field",
21 | "Mask",
22 | "Model",
23 | "Namespace",
24 | "OrderedModel",
25 | "SchemaModel",
26 | "abort",
27 | "cors",
28 | "fields",
29 | "inputs",
30 | "reqparse",
31 | "RestError",
32 | "SpecsError",
33 | "Swagger",
34 | "ValidationError",
35 | )
36 |
--------------------------------------------------------------------------------
/flask_restx/_http.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | """
3 | This file is backported from Python 3.5 http built-in module.
4 | """
5 |
6 | from enum import IntEnum
7 |
8 |
9 | class HTTPStatus(IntEnum):
10 | """HTTP status codes and reason phrases
11 |
12 | Status codes from the following RFCs are all observed:
13 |
14 | * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
15 | * RFC 6585: Additional HTTP Status Codes
16 | * RFC 3229: Delta encoding in HTTP
17 | * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
18 | * RFC 5842: Binding Extensions to WebDAV
19 | * RFC 7238: Permanent Redirect
20 | * RFC 2295: Transparent Content Negotiation in HTTP
21 | * RFC 2774: An HTTP Extension Framework
22 | """
23 |
24 | def __new__(cls, value, phrase, description=""):
25 | obj = int.__new__(cls, value)
26 | obj._value_ = value
27 |
28 | obj.phrase = phrase
29 | obj.description = description
30 | return obj
31 |
32 | def __str__(self):
33 | return str(self.value)
34 |
35 | # informational
36 | CONTINUE = 100, "Continue", "Request received, please continue"
37 | SWITCHING_PROTOCOLS = (
38 | 101,
39 | "Switching Protocols",
40 | "Switching to new protocol; obey Upgrade header",
41 | )
42 | PROCESSING = 102, "Processing"
43 |
44 | # success
45 | OK = 200, "OK", "Request fulfilled, document follows"
46 | CREATED = 201, "Created", "Document created, URL follows"
47 | ACCEPTED = (202, "Accepted", "Request accepted, processing continues off-line")
48 | NON_AUTHORITATIVE_INFORMATION = (
49 | 203,
50 | "Non-Authoritative Information",
51 | "Request fulfilled from cache",
52 | )
53 | NO_CONTENT = 204, "No Content", "Request fulfilled, nothing follows"
54 | RESET_CONTENT = 205, "Reset Content", "Clear input form for further input"
55 | PARTIAL_CONTENT = 206, "Partial Content", "Partial content follows"
56 | MULTI_STATUS = 207, "Multi-Status"
57 | ALREADY_REPORTED = 208, "Already Reported"
58 | IM_USED = 226, "IM Used"
59 |
60 | # redirection
61 | MULTIPLE_CHOICES = (
62 | 300,
63 | "Multiple Choices",
64 | "Object has several resources -- see URI list",
65 | )
66 | MOVED_PERMANENTLY = (
67 | 301,
68 | "Moved Permanently",
69 | "Object moved permanently -- see URI list",
70 | )
71 | FOUND = 302, "Found", "Object moved temporarily -- see URI list"
72 | SEE_OTHER = 303, "See Other", "Object moved -- see Method and URL list"
73 | NOT_MODIFIED = (304, "Not Modified", "Document has not changed since given time")
74 | USE_PROXY = (
75 | 305,
76 | "Use Proxy",
77 | "You must use proxy specified in Location to access this resource",
78 | )
79 | TEMPORARY_REDIRECT = (
80 | 307,
81 | "Temporary Redirect",
82 | "Object moved temporarily -- see URI list",
83 | )
84 | PERMANENT_REDIRECT = (
85 | 308,
86 | "Permanent Redirect",
87 | "Object moved temporarily -- see URI list",
88 | )
89 |
90 | # client error
91 | BAD_REQUEST = (400, "Bad Request", "Bad request syntax or unsupported method")
92 | UNAUTHORIZED = (401, "Unauthorized", "No permission -- see authorization schemes")
93 | PAYMENT_REQUIRED = (402, "Payment Required", "No payment -- see charging schemes")
94 | FORBIDDEN = (403, "Forbidden", "Request forbidden -- authorization will not help")
95 | NOT_FOUND = (404, "Not Found", "Nothing matches the given URI")
96 | METHOD_NOT_ALLOWED = (
97 | 405,
98 | "Method Not Allowed",
99 | "Specified method is invalid for this resource",
100 | )
101 | NOT_ACCEPTABLE = (406, "Not Acceptable", "URI not available in preferred format")
102 | PROXY_AUTHENTICATION_REQUIRED = (
103 | 407,
104 | "Proxy Authentication Required",
105 | "You must authenticate with this proxy before proceeding",
106 | )
107 | REQUEST_TIMEOUT = (408, "Request Timeout", "Request timed out; try again later")
108 | CONFLICT = 409, "Conflict", "Request conflict"
109 | GONE = (410, "Gone", "URI no longer exists and has been permanently removed")
110 | LENGTH_REQUIRED = (411, "Length Required", "Client must specify Content-Length")
111 | PRECONDITION_FAILED = (
112 | 412,
113 | "Precondition Failed",
114 | "Precondition in headers is false",
115 | )
116 | REQUEST_ENTITY_TOO_LARGE = (413, "Request Entity Too Large", "Entity is too large")
117 | REQUEST_URI_TOO_LONG = (414, "Request-URI Too Long", "URI is too long")
118 | UNSUPPORTED_MEDIA_TYPE = (
119 | 415,
120 | "Unsupported Media Type",
121 | "Entity body in unsupported format",
122 | )
123 | REQUESTED_RANGE_NOT_SATISFIABLE = (
124 | 416,
125 | "Requested Range Not Satisfiable",
126 | "Cannot satisfy request range",
127 | )
128 | EXPECTATION_FAILED = (
129 | 417,
130 | "Expectation Failed",
131 | "Expect condition could not be satisfied",
132 | )
133 | UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity"
134 | LOCKED = 423, "Locked"
135 | FAILED_DEPENDENCY = 424, "Failed Dependency"
136 | UPGRADE_REQUIRED = 426, "Upgrade Required"
137 | PRECONDITION_REQUIRED = (
138 | 428,
139 | "Precondition Required",
140 | "The origin server requires the request to be conditional",
141 | )
142 | TOO_MANY_REQUESTS = (
143 | 429,
144 | "Too Many Requests",
145 | "The user has sent too many requests in "
146 | 'a given amount of time ("rate limiting")',
147 | )
148 | REQUEST_HEADER_FIELDS_TOO_LARGE = (
149 | 431,
150 | "Request Header Fields Too Large",
151 | "The server is unwilling to process the request because its header "
152 | "fields are too large",
153 | )
154 |
155 | # server errors
156 | INTERNAL_SERVER_ERROR = (
157 | 500,
158 | "Internal Server Error",
159 | "Server got itself in trouble",
160 | )
161 | NOT_IMPLEMENTED = (501, "Not Implemented", "Server does not support this operation")
162 | BAD_GATEWAY = (502, "Bad Gateway", "Invalid responses from another server/proxy")
163 | SERVICE_UNAVAILABLE = (
164 | 503,
165 | "Service Unavailable",
166 | "The server cannot process the request due to a high load",
167 | )
168 | GATEWAY_TIMEOUT = (
169 | 504,
170 | "Gateway Timeout",
171 | "The gateway server did not receive a timely response",
172 | )
173 | HTTP_VERSION_NOT_SUPPORTED = (
174 | 505,
175 | "HTTP Version Not Supported",
176 | "Cannot fulfill request",
177 | )
178 | VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates"
179 | INSUFFICIENT_STORAGE = 507, "Insufficient Storage"
180 | LOOP_DETECTED = 508, "Loop Detected"
181 | NOT_EXTENDED = 510, "Not Extended"
182 | NETWORK_AUTHENTICATION_REQUIRED = (
183 | 511,
184 | "Network Authentication Required",
185 | "The client needs to authenticate to gain network access",
186 | )
187 |
--------------------------------------------------------------------------------
/flask_restx/apidoc.py:
--------------------------------------------------------------------------------
1 | from flask import url_for, Blueprint, render_template
2 |
3 |
4 | class Apidoc(Blueprint):
5 | """
6 | Allow to know if the blueprint has already been registered
7 | until https://github.com/mitsuhiko/flask/pull/1301 is merged
8 | """
9 |
10 | def __init__(self, *args, **kwargs):
11 | self.registered = False
12 | super(Apidoc, self).__init__(*args, **kwargs)
13 |
14 | def register(self, *args, **kwargs):
15 | super(Apidoc, self).register(*args, **kwargs)
16 | self.registered = True
17 |
18 |
19 | apidoc = Apidoc(
20 | "restx_doc",
21 | __name__,
22 | template_folder="templates",
23 | static_folder="static",
24 | static_url_path="/swaggerui",
25 | )
26 |
27 |
28 | @apidoc.add_app_template_global
29 | def swagger_static(filename):
30 | return url_for("restx_doc.static", filename=filename)
31 |
32 |
33 | def ui_for(api):
34 | """Render a SwaggerUI for a given API"""
35 | return render_template("swagger-ui.html", title=api.title, specs_url=api.specs_url)
36 |
--------------------------------------------------------------------------------
/flask_restx/cors.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from flask import make_response, request, current_app
3 | from functools import update_wrapper
4 |
5 |
6 | def crossdomain(
7 | origin=None,
8 | methods=None,
9 | headers=None,
10 | expose_headers=None,
11 | max_age=21600,
12 | attach_to_all=True,
13 | automatic_options=True,
14 | credentials=False,
15 | ):
16 | """
17 | https://web.archive.org/web/20190128010149/http://flask.pocoo.org/snippets/56/
18 | """
19 | if methods is not None:
20 | methods = ", ".join(sorted(x.upper() for x in methods))
21 | if headers is not None and not isinstance(headers, str):
22 | headers = ", ".join(x.upper() for x in headers)
23 | if expose_headers is not None and not isinstance(expose_headers, str):
24 | expose_headers = ", ".join(x.upper() for x in expose_headers)
25 | if not isinstance(origin, str):
26 | origin = ", ".join(origin)
27 | if isinstance(max_age, timedelta):
28 | max_age = max_age.total_seconds()
29 |
30 | def get_methods():
31 | if methods is not None:
32 | return methods
33 |
34 | options_resp = current_app.make_default_options_response()
35 | return options_resp.headers["allow"]
36 |
37 | def decorator(f):
38 | def wrapped_function(*args, **kwargs):
39 | if automatic_options and request.method == "OPTIONS":
40 | resp = current_app.make_default_options_response()
41 | else:
42 | resp = make_response(f(*args, **kwargs))
43 | if not attach_to_all and request.method != "OPTIONS":
44 | return resp
45 |
46 | h = resp.headers
47 |
48 | h["Access-Control-Allow-Origin"] = origin
49 | h["Access-Control-Allow-Methods"] = get_methods()
50 | h["Access-Control-Max-Age"] = str(max_age)
51 | if credentials:
52 | h["Access-Control-Allow-Credentials"] = "true"
53 | if headers is not None:
54 | h["Access-Control-Allow-Headers"] = headers
55 | if expose_headers is not None:
56 | h["Access-Control-Expose-Headers"] = expose_headers
57 | return resp
58 |
59 | f.provide_automatic_options = False
60 | return update_wrapper(wrapped_function, f)
61 |
62 | return decorator
63 |
--------------------------------------------------------------------------------
/flask_restx/errors.py:
--------------------------------------------------------------------------------
1 | import flask
2 |
3 | from werkzeug.exceptions import HTTPException
4 |
5 | from ._http import HTTPStatus
6 |
7 | __all__ = (
8 | "abort",
9 | "RestError",
10 | "ValidationError",
11 | "SpecsError",
12 | )
13 |
14 |
15 | def abort(code=HTTPStatus.INTERNAL_SERVER_ERROR, message=None, **kwargs):
16 | """
17 | Properly abort the current request.
18 |
19 | Raise a `HTTPException` for the given status `code`.
20 | Attach any keyword arguments to the exception for later processing.
21 |
22 | :param int code: The associated HTTP status code
23 | :param str message: An optional details message
24 | :param kwargs: Any additional data to pass to the error payload
25 | :raise HTTPException:
26 | """
27 | try:
28 | flask.abort(code)
29 | except HTTPException as e:
30 | if message:
31 | kwargs["message"] = str(message)
32 | if kwargs:
33 | e.data = kwargs
34 | raise
35 |
36 |
37 | class RestError(Exception):
38 | """Base class for all Flask-RESTX Errors"""
39 |
40 | def __init__(self, msg):
41 | self.msg = msg
42 |
43 | def __str__(self):
44 | return self.msg
45 |
46 |
47 | class ValidationError(RestError):
48 | """A helper class for validation errors."""
49 |
50 | pass
51 |
52 |
53 | class SpecsError(RestError):
54 | """A helper class for incoherent specifications."""
55 |
56 | pass
57 |
--------------------------------------------------------------------------------
/flask_restx/mask.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import re
3 |
4 | from collections import OrderedDict
5 | from inspect import isclass
6 |
7 | from .errors import RestError
8 |
9 | log = logging.getLogger(__name__)
10 |
11 | LEXER = re.compile(r"\{|\}|\,|[\w_:\-\*]+")
12 |
13 |
14 | class MaskError(RestError):
15 | """Raised when an error occurs on mask"""
16 |
17 | pass
18 |
19 |
20 | class ParseError(MaskError):
21 | """Raised when the mask parsing failed"""
22 |
23 | pass
24 |
25 |
26 | class Mask(OrderedDict):
27 | """
28 | Hold a parsed mask.
29 |
30 | :param str|dict|Mask mask: A mask, parsed or not
31 | :param bool skip: If ``True``, missing fields won't appear in result
32 | """
33 |
34 | def __init__(self, mask=None, skip=False, **kwargs):
35 | self.skip = skip
36 | if isinstance(mask, str):
37 | super(Mask, self).__init__()
38 | self.parse(mask)
39 | elif isinstance(mask, (dict, OrderedDict)):
40 | super(Mask, self).__init__(mask, **kwargs)
41 | else:
42 | self.skip = skip
43 | super(Mask, self).__init__(**kwargs)
44 |
45 | def parse(self, mask):
46 | """
47 | Parse a fields mask.
48 | Expect something in the form::
49 |
50 | {field,nested{nested_field,another},last}
51 |
52 | External brackets are optionals so it can also be written::
53 |
54 | field,nested{nested_field,another},last
55 |
56 | All extras characters will be ignored.
57 |
58 | :param str mask: the mask string to parse
59 | :raises ParseError: when a mask is unparseable/invalid
60 |
61 | """
62 | if not mask:
63 | return
64 |
65 | mask = self.clean(mask)
66 | fields = self
67 | previous = None
68 | stack = []
69 |
70 | for token in LEXER.findall(mask):
71 | if token == "{":
72 | if previous not in fields:
73 | raise ParseError("Unexpected opening bracket")
74 | fields[previous] = Mask(skip=self.skip)
75 | stack.append(fields)
76 | fields = fields[previous]
77 | elif token == "}":
78 | if not stack:
79 | raise ParseError("Unexpected closing bracket")
80 | fields = stack.pop()
81 | elif token == ",":
82 | if previous in (",", "{", None):
83 | raise ParseError("Unexpected comma")
84 | else:
85 | fields[token] = True
86 |
87 | previous = token
88 |
89 | if stack:
90 | raise ParseError("Missing closing bracket")
91 |
92 | def clean(self, mask):
93 | """Remove unnecessary characters"""
94 | mask = mask.replace("\n", "").strip()
95 | # External brackets are optional
96 | if mask[0] == "{":
97 | if mask[-1] != "}":
98 | raise ParseError("Missing closing bracket")
99 | mask = mask[1:-1]
100 | return mask
101 |
102 | def apply(self, data):
103 | """
104 | Apply a fields mask to the data.
105 |
106 | :param data: The data or model to apply mask on
107 | :raises MaskError: when unable to apply the mask
108 |
109 | """
110 | from . import fields
111 |
112 | # Should handle lists
113 | if isinstance(data, (list, tuple, set)):
114 | return [self.apply(d) for d in data]
115 | elif isinstance(data, (fields.Nested, fields.List, fields.Polymorph)):
116 | return data.clone(self)
117 | elif type(data) == fields.Raw:
118 | return fields.Raw(default=data.default, attribute=data.attribute, mask=self)
119 | elif data == fields.Raw:
120 | return fields.Raw(mask=self)
121 | elif (
122 | isinstance(data, fields.Raw)
123 | or isclass(data)
124 | and issubclass(data, fields.Raw)
125 | ):
126 | # Not possible to apply a mask on these remaining fields types
127 | raise MaskError("Mask is inconsistent with model")
128 | # Should handle objects
129 | elif not isinstance(data, (dict, OrderedDict)) and hasattr(data, "__dict__"):
130 | data = data.__dict__
131 |
132 | return self.filter_data(data)
133 |
134 | def filter_data(self, data):
135 | """
136 | Handle the data filtering given a parsed mask
137 |
138 | :param dict data: the raw data to filter
139 | :param list mask: a parsed mask to filter against
140 | :param bool skip: whether or not to skip missing fields
141 |
142 | """
143 | out = {}
144 | for field, content in self.items():
145 | if field == "*":
146 | continue
147 | elif isinstance(content, Mask):
148 | nested = data.get(field, None)
149 | if self.skip and nested is None:
150 | continue
151 | elif nested is None:
152 | out[field] = None
153 | else:
154 | out[field] = content.apply(nested)
155 | elif self.skip and field not in data:
156 | continue
157 | else:
158 | out[field] = data.get(field, None)
159 |
160 | if "*" in self.keys():
161 | for key, value in data.items():
162 | if key not in out:
163 | out[key] = value
164 | return out
165 |
166 | def __str__(self):
167 | return "{{{0}}}".format(
168 | ",".join(
169 | [
170 | "".join((k, str(v))) if isinstance(v, Mask) else k
171 | for k, v in self.items()
172 | ]
173 | )
174 | )
175 |
176 |
177 | def apply(data, mask, skip=False):
178 | """
179 | Apply a fields mask to the data.
180 |
181 | :param data: The data or model to apply mask on
182 | :param str|Mask mask: the mask (parsed or not) to apply on data
183 | :param bool skip: If rue, missing field won't appear in result
184 | :raises MaskError: when unable to apply the mask
185 |
186 | """
187 | return Mask(mask, skip).apply(data)
188 |
--------------------------------------------------------------------------------
/flask_restx/model.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import re
3 | import warnings
4 |
5 | from collections import OrderedDict
6 |
7 | from collections.abc import MutableMapping
8 | from werkzeug.utils import cached_property
9 |
10 | from .mask import Mask
11 | from .errors import abort
12 |
13 | from jsonschema import Draft4Validator
14 | from jsonschema.exceptions import ValidationError
15 |
16 | from .utils import not_none
17 | from ._http import HTTPStatus
18 |
19 |
20 | RE_REQUIRED = re.compile(r"u?\'(?P.*)\' is a required property", re.I | re.U)
21 |
22 |
23 | def instance(cls):
24 | if isinstance(cls, type):
25 | return cls()
26 | return cls
27 |
28 |
29 | class ModelBase(object):
30 | """
31 | Handles validation and swagger style inheritance for both subclasses.
32 | Subclass must define `schema` attribute.
33 |
34 | :param str name: The model public name
35 | """
36 |
37 | def __init__(self, name, *args, **kwargs):
38 | super(ModelBase, self).__init__(*args, **kwargs)
39 | self.__apidoc__ = {"name": name}
40 | self.name = name
41 | self.__parents__ = []
42 |
43 | def instance_inherit(name, *parents):
44 | return self.__class__.inherit(name, self, *parents)
45 |
46 | self.inherit = instance_inherit
47 |
48 | @property
49 | def ancestors(self):
50 | """
51 | Return the ancestors tree
52 | """
53 | ancestors = [p.ancestors for p in self.__parents__]
54 | return set.union(set([self.name]), *ancestors)
55 |
56 | def get_parent(self, name):
57 | if self.name == name:
58 | return self
59 | else:
60 | for parent in self.__parents__:
61 | found = parent.get_parent(name)
62 | if found:
63 | return found
64 | raise ValueError("Parent " + name + " not found")
65 |
66 | @property
67 | def __schema__(self):
68 | schema = self._schema
69 |
70 | if self.__parents__:
71 | refs = [
72 | {"$ref": "#/definitions/{0}".format(parent.name)}
73 | for parent in self.__parents__
74 | ]
75 |
76 | return {"allOf": refs + [schema]}
77 | else:
78 | return schema
79 |
80 | @classmethod
81 | def inherit(cls, name, *parents):
82 | """
83 | Inherit this model (use the Swagger composition pattern aka. allOf)
84 | :param str name: The new model name
85 | :param dict fields: The new model extra fields
86 | """
87 | model = cls(name, parents[-1])
88 | model.__parents__ = parents[:-1]
89 | return model
90 |
91 | def validate(self, data, resolver=None, format_checker=None):
92 | validator = Draft4Validator(
93 | self.__schema__, resolver=resolver, format_checker=format_checker
94 | )
95 | try:
96 | validator.validate(data)
97 | except ValidationError:
98 | abort(
99 | HTTPStatus.BAD_REQUEST,
100 | message="Input payload validation failed",
101 | errors=dict(self.format_error(e) for e in validator.iter_errors(data)),
102 | )
103 |
104 | def format_error(self, error):
105 | path = list(error.path)
106 | if error.validator == "required":
107 | name = RE_REQUIRED.match(error.message).group("name")
108 | path.append(name)
109 | key = ".".join(str(p) for p in path)
110 | return key, error.message
111 |
112 | def __unicode__(self):
113 | return "Model({name},{{{fields}}})".format(
114 | name=self.name, fields=",".join(self.keys())
115 | )
116 |
117 | __str__ = __unicode__
118 |
119 |
120 | class RawModel(ModelBase):
121 | """
122 | A thin wrapper on ordered fields dict to store API doc metadata.
123 | Can also be used for response marshalling.
124 |
125 | :param str name: The model public name
126 | :param str mask: an optional default model mask
127 | :param bool strict: validation should raise error when there is param not provided in schema
128 | """
129 |
130 | wrapper = dict
131 |
132 | def __init__(self, name, *args, **kwargs):
133 | self.__mask__ = kwargs.pop("mask", None)
134 | self.__strict__ = kwargs.pop("strict", False)
135 | if self.__mask__ and not isinstance(self.__mask__, Mask):
136 | self.__mask__ = Mask(self.__mask__)
137 | super(RawModel, self).__init__(name, *args, **kwargs)
138 |
139 | def instance_clone(name, *parents):
140 | return self.__class__.clone(name, self, *parents)
141 |
142 | self.clone = instance_clone
143 |
144 | @property
145 | def _schema(self):
146 | properties = self.wrapper()
147 | required = set()
148 | discriminator = None
149 | for name, field in self.items():
150 | field = instance(field)
151 | properties[name] = field.__schema__
152 | if field.required:
153 | required.add(name)
154 | if getattr(field, "discriminator", False):
155 | discriminator = name
156 |
157 | definition = {
158 | "required": sorted(list(required)) or None,
159 | "properties": properties,
160 | "discriminator": discriminator,
161 | "x-mask": str(self.__mask__) if self.__mask__ else None,
162 | "type": "object",
163 | }
164 |
165 | if self.__strict__:
166 | definition["additionalProperties"] = False
167 |
168 | return not_none(definition)
169 |
170 | @cached_property
171 | def resolved(self):
172 | """
173 | Resolve real fields before submitting them to marshal
174 | """
175 | # Duplicate fields
176 | resolved = copy.deepcopy(self)
177 |
178 | # Recursively copy parent fields if necessary
179 | for parent in self.__parents__:
180 | resolved.update(parent.resolved)
181 |
182 | # Handle discriminator
183 | candidates = [f for f in resolved.values() if getattr(f, "discriminator", None)]
184 | # Ensure the is only one discriminator
185 | if len(candidates) > 1:
186 | raise ValueError("There can only be one discriminator by schema")
187 | # Ensure discriminator always output the model name
188 | elif len(candidates) == 1:
189 | candidates[0].default = self.name
190 |
191 | return resolved
192 |
193 | def extend(self, name, fields):
194 | """
195 | Extend this model (Duplicate all fields)
196 |
197 | :param str name: The new model name
198 | :param dict fields: The new model extra fields
199 |
200 | :deprecated: since 0.9. Use :meth:`clone` instead.
201 | """
202 | warnings.warn(
203 | "extend is is deprecated, use clone instead",
204 | DeprecationWarning,
205 | stacklevel=2,
206 | )
207 | if isinstance(fields, (list, tuple)):
208 | return self.clone(name, *fields)
209 | else:
210 | return self.clone(name, fields)
211 |
212 | @classmethod
213 | def clone(cls, name, *parents):
214 | """
215 | Clone these models (Duplicate all fields)
216 |
217 | It can be used from the class
218 |
219 | >>> model = Model.clone(fields_1, fields_2)
220 |
221 | or from an Instanciated model
222 |
223 | >>> new_model = model.clone(fields_1, fields_2)
224 |
225 | :param str name: The new model name
226 | :param dict parents: The new model extra fields
227 | """
228 | fields = cls.wrapper()
229 | for parent in parents:
230 | fields.update(copy.deepcopy(parent))
231 | return cls(name, fields)
232 |
233 | def __deepcopy__(self, memo):
234 | obj = self.__class__(
235 | self.name,
236 | [(key, copy.deepcopy(value, memo)) for key, value in self.items()],
237 | mask=self.__mask__,
238 | strict=self.__strict__,
239 | )
240 | obj.__parents__ = self.__parents__
241 | return obj
242 |
243 |
244 | class Model(RawModel, dict, MutableMapping):
245 | """
246 | A thin wrapper on fields dict to store API doc metadata.
247 | Can also be used for response marshalling.
248 |
249 | :param str name: The model public name
250 | :param str mask: an optional default model mask
251 | """
252 |
253 | pass
254 |
255 |
256 | class OrderedModel(RawModel, OrderedDict, MutableMapping):
257 | """
258 | A thin wrapper on ordered fields dict to store API doc metadata.
259 | Can also be used for response marshalling.
260 |
261 | :param str name: The model public name
262 | :param str mask: an optional default model mask
263 | """
264 |
265 | wrapper = OrderedDict
266 |
267 |
268 | class SchemaModel(ModelBase):
269 | """
270 | Stores API doc metadata based on a json schema.
271 |
272 | :param str name: The model public name
273 | :param dict schema: The json schema we are documenting
274 | """
275 |
276 | def __init__(self, name, schema=None):
277 | super(SchemaModel, self).__init__(name)
278 | self._schema = schema or {}
279 |
280 | def __unicode__(self):
281 | return "SchemaModel({name},{schema})".format(
282 | name=self.name, schema=self._schema
283 | )
284 |
285 | __str__ = __unicode__
286 |
--------------------------------------------------------------------------------
/flask_restx/postman.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from time import time
3 | from uuid import uuid5, NAMESPACE_URL
4 |
5 | from urllib.parse import urlencode
6 |
7 |
8 | def clean(data):
9 | """Remove all keys where value is None"""
10 | return dict((k, v) for k, v in data.items() if v is not None)
11 |
12 |
13 | DEFAULT_VARS = {
14 | "string": "",
15 | "integer": 0,
16 | "number": 0,
17 | }
18 |
19 |
20 | class Request(object):
21 | """Wraps a Swagger operation into a Postman Request"""
22 |
23 | def __init__(self, collection, path, params, method, operation):
24 | self.collection = collection
25 | self.path = path
26 | self.params = params
27 | self.method = method.upper()
28 | self.operation = operation
29 |
30 | @property
31 | def id(self):
32 | seed = str(" ".join((self.method, self.url)))
33 | return str(uuid5(self.collection.uuid, seed))
34 |
35 | @property
36 | def url(self):
37 | return self.collection.api.base_url.rstrip("/") + self.path
38 |
39 | @property
40 | def headers(self):
41 | headers = {}
42 | # Handle content-type
43 | if self.method != "GET":
44 | consumes = self.collection.api.__schema__.get("consumes", [])
45 | consumes = self.operation.get("consumes", consumes)
46 | if len(consumes):
47 | headers["Content-Type"] = consumes[-1]
48 |
49 | # Add all parameters headers
50 | for param in self.operation.get("parameters", []):
51 | if param["in"] == "header":
52 | headers[param["name"]] = param.get("default", "")
53 |
54 | # Add security headers if needed (global then local)
55 | for security in self.collection.api.__schema__.get("security", []):
56 | for key, header in self.collection.apikeys.items():
57 | if key in security:
58 | headers[header] = ""
59 | for security in self.operation.get("security", []):
60 | for key, header in self.collection.apikeys.items():
61 | if key in security:
62 | headers[header] = ""
63 |
64 | lines = [":".join(line) for line in headers.items()]
65 | return "\n".join(lines)
66 |
67 | @property
68 | def folder(self):
69 | if "tags" not in self.operation or len(self.operation["tags"]) == 0:
70 | return
71 | tag = self.operation["tags"][0]
72 | for folder in self.collection.folders:
73 | if folder.tag == tag:
74 | return folder.id
75 |
76 | def as_dict(self, urlvars=False):
77 | url, variables = self.process_url(urlvars)
78 | return clean(
79 | {
80 | "id": self.id,
81 | "method": self.method,
82 | "name": self.operation["operationId"],
83 | "description": self.operation.get("summary"),
84 | "url": url,
85 | "headers": self.headers,
86 | "collectionId": self.collection.id,
87 | "folder": self.folder,
88 | "pathVariables": variables,
89 | "time": int(time()),
90 | }
91 | )
92 |
93 | def process_url(self, urlvars=False):
94 | url = self.url
95 | path_vars = {}
96 | url_vars = {}
97 | params = dict((p["name"], p) for p in self.params)
98 | params.update(
99 | dict((p["name"], p) for p in self.operation.get("parameters", []))
100 | )
101 | if not params:
102 | return url, None
103 | for name, param in params.items():
104 | if param["in"] == "path":
105 | url = url.replace("{%s}" % name, ":%s" % name)
106 | path_vars[name] = DEFAULT_VARS.get(param["type"], "")
107 | elif param["in"] == "query" and urlvars:
108 | default = DEFAULT_VARS.get(param["type"], "")
109 | url_vars[name] = param.get("default", default)
110 | if url_vars:
111 | url = "?".join((url, urlencode(url_vars)))
112 | return url, path_vars
113 |
114 |
115 | class Folder(object):
116 | def __init__(self, collection, tag):
117 | self.collection = collection
118 | self.tag = tag["name"]
119 | self.description = tag["description"]
120 |
121 | @property
122 | def id(self):
123 | return str(uuid5(self.collection.uuid, str(self.tag)))
124 |
125 | @property
126 | def order(self):
127 | return [r.id for r in self.collection.requests if r.folder == self.id]
128 |
129 | def as_dict(self):
130 | return clean(
131 | {
132 | "id": self.id,
133 | "name": self.tag,
134 | "description": self.description,
135 | "order": self.order,
136 | "collectionId": self.collection.id,
137 | }
138 | )
139 |
140 |
141 | class PostmanCollectionV1(object):
142 | """Postman Collection (V1 format) serializer"""
143 |
144 | def __init__(self, api, swagger=False):
145 | self.api = api
146 | self.swagger = swagger
147 |
148 | @property
149 | def uuid(self):
150 | return uuid5(NAMESPACE_URL, self.api.base_url)
151 |
152 | @property
153 | def id(self):
154 | return str(self.uuid)
155 |
156 | @property
157 | def requests(self):
158 | if self.swagger:
159 | # First request is Swagger specifications
160 | yield Request(
161 | self,
162 | "/swagger.json",
163 | {},
164 | "get",
165 | {
166 | "operationId": "Swagger specifications",
167 | "summary": "The API Swagger specifications as JSON",
168 | },
169 | )
170 | # Then iter over API paths and methods
171 | for path, operations in self.api.__schema__["paths"].items():
172 | path_params = operations.get("parameters", [])
173 |
174 | for method, operation in operations.items():
175 | if method != "parameters":
176 | yield Request(self, path, path_params, method, operation)
177 |
178 | @property
179 | def folders(self):
180 | for tag in self.api.__schema__["tags"]:
181 | yield Folder(self, tag)
182 |
183 | @property
184 | def apikeys(self):
185 | return dict(
186 | (name, secdef["name"])
187 | for name, secdef in self.api.__schema__.get("securityDefinitions").items()
188 | if secdef.get("in") == "header" and secdef.get("type") == "apiKey"
189 | )
190 |
191 | def as_dict(self, urlvars=False):
192 | return clean(
193 | {
194 | "id": self.id,
195 | "name": " ".join((self.api.title, self.api.version)),
196 | "description": self.api.description,
197 | "order": [r.id for r in self.requests if not r.folder],
198 | "requests": [r.as_dict(urlvars=urlvars) for r in self.requests],
199 | "folders": [f.as_dict() for f in self.folders],
200 | "timestamp": int(time()),
201 | }
202 | )
203 |
--------------------------------------------------------------------------------
/flask_restx/representations.py:
--------------------------------------------------------------------------------
1 | try:
2 | from ujson import dumps
3 | except ImportError:
4 | from json import dumps
5 |
6 | from flask import make_response, current_app
7 |
8 |
9 | def output_json(data, code, headers=None):
10 | """Makes a Flask response with a JSON encoded body"""
11 |
12 | settings = current_app.config.get("RESTX_JSON", {})
13 |
14 | # If we're in debug mode, and the indent is not set, we set it to a
15 | # reasonable value here. Note that this won't override any existing value
16 | # that was set.
17 | if current_app.debug:
18 | settings.setdefault("indent", 4)
19 |
20 | # always end the json dumps with a new line
21 | # see https://github.com/mitsuhiko/flask/pull/1262
22 | dumped = dumps(data, **settings) + "\n"
23 |
24 | resp = make_response(dumped, code)
25 | resp.headers.extend(headers or {})
26 | return resp
27 |
--------------------------------------------------------------------------------
/flask_restx/resource.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 | from flask.views import MethodView
3 |
4 |
5 | from .model import ModelBase
6 |
7 | from .utils import unpack, BaseResponse
8 |
9 |
10 | class Resource(MethodView):
11 | """
12 | Represents an abstract RESTX resource.
13 |
14 | Concrete resources should extend from this class
15 | and expose methods for each supported HTTP method.
16 | If a resource is invoked with an unsupported HTTP method,
17 | the API will return a response with status 405 Method Not Allowed.
18 | Otherwise the appropriate method is called and passed all arguments
19 | from the url rule used when adding the resource to an Api instance.
20 | See :meth:`~flask_restx.Api.add_resource` for details.
21 | """
22 |
23 | representations = None
24 | method_decorators = []
25 |
26 | def __init__(self, api=None, *args, **kwargs):
27 | self.api = api
28 |
29 | def dispatch_request(self, *args, **kwargs):
30 | # Taken from flask
31 | meth = getattr(self, request.method.lower(), None)
32 | if meth is None and request.method == "HEAD":
33 | meth = getattr(self, "get", None)
34 | assert meth is not None, "Unimplemented method %r" % request.method
35 |
36 | for decorator in self.method_decorators:
37 | meth = decorator(meth)
38 |
39 | self.validate_payload(meth)
40 |
41 | resp = meth(*args, **kwargs)
42 |
43 | if isinstance(resp, BaseResponse):
44 | return resp
45 |
46 | representations = self.representations or {}
47 |
48 | mediatype = request.accept_mimetypes.best_match(representations, default=None)
49 | if mediatype in representations:
50 | data, code, headers = unpack(resp)
51 | resp = representations[mediatype](data, code, headers)
52 | resp.headers["Content-Type"] = mediatype
53 | return resp
54 |
55 | return resp
56 |
57 | def __validate_payload(self, expect, collection=False):
58 | """
59 | :param ModelBase expect: the expected model for the input payload
60 | :param bool collection: False if a single object of a resource is
61 | expected, True if a collection of objects of a resource is expected.
62 | """
63 | # TODO: proper content negotiation
64 | data = request.get_json()
65 | if collection:
66 | data = data if isinstance(data, list) else [data]
67 | for obj in data:
68 | expect.validate(obj, self.api.refresolver, self.api.format_checker)
69 | else:
70 | expect.validate(data, self.api.refresolver, self.api.format_checker)
71 |
72 | def validate_payload(self, func):
73 | """Perform a payload validation on expected model if necessary"""
74 | if getattr(func, "__apidoc__", False) is not False:
75 | doc = func.__apidoc__
76 | validate = doc.get("validate", None)
77 | validate = validate if validate is not None else self.api._validate
78 | if validate:
79 | for expect in doc.get("expect", []):
80 | # TODO: handle third party handlers
81 | if isinstance(expect, list) and len(expect) == 1:
82 | if isinstance(expect[0], ModelBase):
83 | self.__validate_payload(expect[0], collection=True)
84 | if isinstance(expect, ModelBase):
85 | self.__validate_payload(expect, collection=False)
86 |
--------------------------------------------------------------------------------
/flask_restx/schemas/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This module give access to OpenAPI specifications schemas
3 | and allows to validate specs against them.
4 |
5 | .. versionadded:: 0.12.1
6 | """
7 |
8 | import io
9 | import json
10 |
11 | import importlib_resources
12 |
13 | from collections.abc import Mapping
14 | from jsonschema import Draft4Validator
15 |
16 | from flask_restx import errors
17 |
18 |
19 | class SchemaValidationError(errors.ValidationError):
20 | """
21 | Raised when specification is not valid
22 |
23 | .. versionadded:: 0.12.1
24 | """
25 |
26 | def __init__(self, msg, errors=None):
27 | super(SchemaValidationError, self).__init__(msg)
28 | self.errors = errors
29 |
30 | def __str__(self):
31 | msg = [self.msg]
32 | for error in sorted(self.errors, key=lambda e: e.path):
33 | path = ".".join(error.path)
34 | msg.append("- {}: {}".format(path, error.message))
35 | for suberror in sorted(error.context, key=lambda e: e.schema_path):
36 | path = ".".join(suberror.schema_path)
37 | msg.append(" - {}: {}".format(path, suberror.message))
38 | return "\n".join(msg)
39 |
40 | __unicode__ = __str__
41 |
42 |
43 | class LazySchema(Mapping):
44 | """
45 | A thin wrapper around schema file lazy loading the data on first access
46 |
47 | :param filename str: The package relative json schema filename
48 | :param validator: The jsonschema validator class version
49 |
50 | .. versionadded:: 0.12.1
51 | """
52 |
53 | def __init__(self, filename, validator=Draft4Validator):
54 | super(LazySchema, self).__init__()
55 | self.filename = filename
56 | self._schema = None
57 | self._validator = validator
58 |
59 | def _load(self):
60 | if not self._schema:
61 | ref = importlib_resources.files(__name__) / self.filename
62 |
63 | with io.open(ref) as infile:
64 | self._schema = json.load(infile)
65 |
66 | def __getitem__(self, key):
67 | self._load()
68 | return self._schema.__getitem__(key)
69 |
70 | def __iter__(self):
71 | self._load()
72 | return self._schema.__iter__()
73 |
74 | def __len__(self):
75 | self._load()
76 | return self._schema.__len__()
77 |
78 | @property
79 | def validator(self):
80 | """The jsonschema validator to validate against"""
81 | return self._validator(self)
82 |
83 |
84 | #: OpenAPI 2.0 specification schema
85 | OAS_20 = LazySchema("oas-2.0.json")
86 |
87 | #: Map supported OpenAPI versions to their JSON schema
88 | VERSIONS = {
89 | "2.0": OAS_20,
90 | }
91 |
92 |
93 | def validate(data):
94 | """
95 | Validate an OpenAPI specification.
96 |
97 | Supported OpenAPI versions: 2.0
98 |
99 | :param data dict: The specification to validate
100 | :returns boolean: True if the specification is valid
101 | :raises SchemaValidationError: when the specification is invalid
102 | :raises flask_restx.errors.SpecsError: when it's not possible to determinate
103 | the schema to validate against
104 |
105 | .. versionadded:: 0.12.1
106 | """
107 | if "swagger" not in data:
108 | raise errors.SpecsError("Unable to determinate OpenAPI schema version")
109 |
110 | version = data["swagger"]
111 | if version not in VERSIONS:
112 | raise errors.SpecsError('Unknown OpenAPI schema version "{}"'.format(version))
113 |
114 | validator = VERSIONS[version].validator
115 |
116 | validation_errors = list(validator.iter_errors(data))
117 | if validation_errors:
118 | raise SchemaValidationError(
119 | "OpenAPI {} validation failed".format(version), errors=validation_errors
120 | )
121 | return True
122 |
--------------------------------------------------------------------------------
/flask_restx/templates/swagger-ui-css.html:
--------------------------------------------------------------------------------
1 | {#
3 |
5 |
7 |
9 |
11 | #}
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/flask_restx/templates/swagger-ui-libs.html:
--------------------------------------------------------------------------------
1 | {#
2 | {% if config.SWAGGER_UI_LANGUAGES %}
3 |
4 | {% for lang in config.SWAGGER_UI_LANGUAGES %}
5 | {% set filename = 'lang/{0}.js'.format(lang) %}
6 |
7 | {% endfor%}
8 | {% endif %}
9 | #}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/flask_restx/templates/swagger-ui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 | {% include 'swagger-ui-css.html' %}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
46 |
47 |
48 |
49 | {% include 'swagger-ui-libs.html' %}
50 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/flask_restx/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | import warnings
3 | import typing
4 |
5 | from collections import OrderedDict
6 | from copy import deepcopy
7 |
8 | from ._http import HTTPStatus
9 |
10 |
11 | FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)")
12 | ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])")
13 |
14 |
15 | __all__ = (
16 | "merge",
17 | "camel_to_dash",
18 | "default_id",
19 | "not_none",
20 | "not_none_sorted",
21 | "unpack",
22 | "BaseResponse",
23 | "import_check_view_func",
24 | )
25 |
26 |
27 | def import_werkzeug_response():
28 | """Resolve `werkzeug` `Response` class import because
29 | `BaseResponse` was renamed in version 2.* to `Response`"""
30 | import importlib.metadata
31 |
32 | werkzeug_major = int(importlib.metadata.version("werkzeug").split(".")[0])
33 | if werkzeug_major < 2:
34 | from werkzeug.wrappers import BaseResponse
35 |
36 | return BaseResponse
37 |
38 | from werkzeug.wrappers import Response
39 |
40 | return Response
41 |
42 |
43 | BaseResponse = import_werkzeug_response()
44 |
45 |
46 | class FlaskCompatibilityWarning(DeprecationWarning):
47 | pass
48 |
49 |
50 | def merge(first, second):
51 | """
52 | Recursively merges two dictionaries.
53 |
54 | Second dictionary values will take precedence over those from the first one.
55 | Nested dictionaries are merged too.
56 |
57 | :param dict first: The first dictionary
58 | :param dict second: The second dictionary
59 | :return: the resulting merged dictionary
60 | :rtype: dict
61 | """
62 | if not isinstance(second, dict):
63 | return second
64 | result = deepcopy(first)
65 | for key, value in second.items():
66 | if key in result and isinstance(result[key], dict):
67 | result[key] = merge(result[key], value)
68 | else:
69 | result[key] = deepcopy(value)
70 | return result
71 |
72 |
73 | def camel_to_dash(value):
74 | """
75 | Transform a CamelCase string into a low_dashed one
76 |
77 | :param str value: a CamelCase string to transform
78 | :return: the low_dashed string
79 | :rtype: str
80 | """
81 | first_cap = FIRST_CAP_RE.sub(r"\1_\2", value)
82 | return ALL_CAP_RE.sub(r"\1_\2", first_cap).lower()
83 |
84 |
85 | def default_id(resource, method):
86 | """Default operation ID generator"""
87 | return "{0}_{1}".format(method, camel_to_dash(resource))
88 |
89 |
90 | def not_none(data):
91 | """
92 | Remove all keys where value is None
93 |
94 | :param dict data: A dictionary with potentially some values set to None
95 | :return: The same dictionary without the keys with values to ``None``
96 | :rtype: dict
97 | """
98 | return dict((k, v) for k, v in data.items() if v is not None)
99 |
100 |
101 | def not_none_sorted(data):
102 | """
103 | Remove all keys where value is None
104 |
105 | :param OrderedDict data: A dictionary with potentially some values set to None
106 | :return: The same dictionary without the keys with values to ``None``
107 | :rtype: OrderedDict
108 | """
109 | return OrderedDict((k, v) for k, v in sorted(data.items()) if v is not None)
110 |
111 |
112 | def unpack(response, default_code=HTTPStatus.OK):
113 | """
114 | Unpack a Flask standard response.
115 |
116 | Flask response can be:
117 | - a single value
118 | - a 2-tuple ``(value, code)``
119 | - a 3-tuple ``(value, code, headers)``
120 |
121 | .. warning::
122 |
123 | When using this function, you must ensure that the tuple is not the response data.
124 | To do so, prefer returning list instead of tuple for listings.
125 |
126 | :param response: A Flask style response
127 | :param int default_code: The HTTP code to use as default if none is provided
128 | :return: a 3-tuple ``(data, code, headers)``
129 | :rtype: tuple
130 | :raise ValueError: if the response does not have one of the expected format
131 | """
132 | if not isinstance(response, tuple):
133 | # data only
134 | return response, default_code, {}
135 | elif len(response) == 1:
136 | # data only as tuple
137 | return response[0], default_code, {}
138 | elif len(response) == 2:
139 | # data and code
140 | data, code = response
141 | return data, code, {}
142 | elif len(response) == 3:
143 | # data, code and headers
144 | data, code, headers = response
145 | return data, code or default_code, headers
146 | else:
147 | raise ValueError("Too many response values")
148 |
149 |
150 | def to_view_name(view_func: typing.Callable) -> str:
151 | """Helper that returns the default endpoint for a given
152 | function. This always is the function name.
153 |
154 | Note: copy of simple flask internal helper
155 | """
156 | assert view_func is not None, "expected view func if endpoint is not provided."
157 | return view_func.__name__
158 |
159 |
160 | def import_check_view_func():
161 | """
162 | Resolve import flask _endpoint_from_view_func.
163 |
164 | Show warning if function cannot be found and provide copy of last known implementation.
165 |
166 | Note: This helper method exists because reoccurring problem with flask function, but
167 | actual method body remaining the same in each flask version.
168 | """
169 | import importlib.metadata
170 |
171 | flask_version = importlib.metadata.version("flask").split(".")
172 | try:
173 | if flask_version[0] == "1":
174 | from flask.helpers import _endpoint_from_view_func
175 | elif flask_version[0] == "2":
176 | from flask.scaffold import _endpoint_from_view_func
177 | elif flask_version[0] == "3":
178 | from flask.sansio.scaffold import _endpoint_from_view_func
179 | else:
180 | warnings.simplefilter("once", FlaskCompatibilityWarning)
181 | _endpoint_from_view_func = None
182 | except ImportError:
183 | warnings.simplefilter("once", FlaskCompatibilityWarning)
184 | _endpoint_from_view_func = None
185 | if _endpoint_from_view_func is None:
186 | _endpoint_from_view_func = to_view_name
187 | return _endpoint_from_view_func
188 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flask-restx",
3 | "version": "1.3.0",
4 | "description": "Fully featured framework for fast, easy and documented API development with Flask",
5 | "repository": "python-restx/flask-restx",
6 | "keywords": [
7 | "swagger",
8 | "flask"
9 | ],
10 | "author": "python-restx authors",
11 | "license": "BSD-3-Clause",
12 | "dependencies": {
13 | "swagger-ui-dist": "^4.15.0",
14 | "typeface-droid-sans": "0.0.40"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/readthedocs.pip:
--------------------------------------------------------------------------------
1 | sphinx
2 | alabaster
3 | sphinx_issues
4 |
--------------------------------------------------------------------------------
/requirements/develop.pip:
--------------------------------------------------------------------------------
1 | tox
2 | black
3 |
--------------------------------------------------------------------------------
/requirements/doc.pip:
--------------------------------------------------------------------------------
1 | alabaster==0.7.12
2 | Sphinx==5.3.0
3 | sphinx-issues==3.0.1
4 |
--------------------------------------------------------------------------------
/requirements/install.pip:
--------------------------------------------------------------------------------
1 | aniso8601>=0.82
2 | jsonschema
3 | Flask>=0.8, !=2.0.0
4 | werkzeug!=2.0.0
5 | importlib_resources
6 |
--------------------------------------------------------------------------------
/requirements/test.pip:
--------------------------------------------------------------------------------
1 | blinker
2 | Faker==2.0.0
3 | mock==3.0.5
4 | pytest==7.0.1
5 | pytest-benchmark==3.4.1
6 | pytest-cov==4.0.0
7 | pytest-flask==1.3.0
8 | pytest-mock==3.6.1
9 | pytest-profiling==1.7.0
10 | invoke==2.2.0
11 | twine==3.8.0
12 | setuptools
13 | backports.zoneinfo;python_version<"3.9"
14 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [tool:pytest]
5 | testpaths = tests
6 | python_files = test_*.py bench_*.py
7 | python_functions = test_* bench_*
8 | python_classes = *Test *Benchmark
9 | markers =
10 | api: test requiring an initialized API
11 | request_context: switch the request
12 |
13 | [ossaudit]
14 |
15 | # The issue is fixed since the v40.8.0 of setuptools, but
16 | # the python3.5 and python3.6 use the old versions.
17 | # https://ossindex.sonatype.org/vuln/06e60262-8241-42ef-8f64-e3d72091de19
18 | # Ignore it until we suppor python < 3.7
19 | ignore-ids = 06e60262-8241-42ef-8f64-e3d72091de19
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # flake8: noqa
4 |
5 | import io
6 | import os
7 | import re
8 | import sys
9 |
10 | from setuptools import setup, find_packages
11 |
12 | RE_REQUIREMENT = re.compile(r"^\s*-r\s*(?P.*)$")
13 |
14 | PYPI_RST_FILTERS = (
15 | # Replace Python crossreferences by simple monospace
16 | (r":(?:class|func|meth|mod|attr|obj|exc|data|const):`~(?:\w+\.)*(\w+)`", r"``\1``"),
17 | (r":(?:class|func|meth|mod|attr|obj|exc|data|const):`([^`]+)`", r"``\1``"),
18 | # replace doc references
19 | (
20 | r":doc:`(.+) <(.*)>`",
21 | r"`\1 `_",
22 | ),
23 | # replace issues references
24 | (
25 | r":issue:`(.+?)`",
26 | r"`#\1 `_",
27 | ),
28 | # replace pr references
29 | (r":pr:`(.+?)`", r"`#\1 `_"),
30 | # replace commit references
31 | (
32 | r":commit:`(.+?)`",
33 | r"`#\1 `_",
34 | ),
35 | # Drop unrecognized currentmodule
36 | (r"\.\. currentmodule:: .*", ""),
37 | )
38 |
39 |
40 | def rst(filename):
41 | """
42 | Load rst file and sanitize it for PyPI.
43 | Remove unsupported github tags:
44 | - code-block directive
45 | - all badges
46 | """
47 | content = io.open(filename).read()
48 | for regex, replacement in PYPI_RST_FILTERS:
49 | content = re.sub(regex, replacement, content)
50 | return content
51 |
52 |
53 | def pip(filename):
54 | """Parse pip reqs file and transform it to setuptools requirements."""
55 | requirements = []
56 | for line in io.open(os.path.join("requirements", "{0}.pip".format(filename))):
57 | line = line.strip()
58 | if not line or "://" in line or line.startswith("#"):
59 | continue
60 | requirements.append(line)
61 | return requirements
62 |
63 |
64 | long_description = "\n".join((rst("README.rst"), ""))
65 |
66 |
67 | exec(
68 | compile(open("flask_restx/__about__.py").read(), "flask_restx/__about__.py", "exec")
69 | )
70 |
71 | install_requires = pip("install")
72 | doc_require = pip("doc")
73 | tests_require = pip("test")
74 | dev_require = tests_require + pip("develop")
75 |
76 | setup(
77 | name="flask-restx",
78 | version=__version__,
79 | description=__description__,
80 | long_description=long_description,
81 | url="https://github.com/python-restx/flask-restx",
82 | author="python-restx Authors",
83 | packages=find_packages(exclude=["tests", "tests.*"]),
84 | include_package_data=True,
85 | install_requires=install_requires,
86 | tests_require=tests_require,
87 | dev_require=dev_require,
88 | extras_require={
89 | "test": tests_require,
90 | "doc": doc_require,
91 | "dev": dev_require,
92 | },
93 | license="BSD-3-Clause",
94 | zip_safe=False,
95 | keywords="flask restx rest api swagger openapi",
96 | classifiers=[
97 | "Development Status :: 5 - Production/Stable",
98 | "Programming Language :: Python",
99 | "Environment :: Web Environment",
100 | "Operating System :: OS Independent",
101 | "Intended Audience :: Developers",
102 | "Topic :: System :: Software Distribution",
103 | "Programming Language :: Python",
104 | "Programming Language :: Python :: 3",
105 | "Programming Language :: Python :: 3.9",
106 | "Programming Language :: Python :: 3.10",
107 | "Programming Language :: Python :: 3.11",
108 | "Programming Language :: Python :: 3.12",
109 | "Programming Language :: Python :: Implementation :: PyPy",
110 | "Topic :: Software Development :: Libraries :: Python Modules",
111 | "License :: OSI Approved :: BSD License",
112 | ],
113 | python_requires=">=3.9",
114 | )
115 |
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from datetime import datetime
5 |
6 | from invoke import task
7 |
8 | ROOT = os.path.dirname(__file__)
9 |
10 | CLEAN_PATTERNS = [
11 | "build",
12 | "dist",
13 | "cover",
14 | "docs/_build",
15 | "**/*.pyc",
16 | ".tox",
17 | "**/__pycache__",
18 | "reports",
19 | "*.egg-info",
20 | ]
21 |
22 |
23 | def color(code):
24 | """A simple ANSI color wrapper factory"""
25 | return lambda t: "\033[{0}{1}\033[0;m".format(code, t)
26 |
27 |
28 | green = color("1;32m")
29 | red = color("1;31m")
30 | blue = color("1;30m")
31 | cyan = color("1;36m")
32 | purple = color("1;35m")
33 | white = color("1;39m")
34 |
35 |
36 | def header(text):
37 | """Display an header"""
38 | print(" ".join((blue(">>"), cyan(text))))
39 | sys.stdout.flush()
40 |
41 |
42 | def info(text, *args, **kwargs):
43 | """Display informations"""
44 | text = text.format(*args, **kwargs)
45 | print(" ".join((purple(">>>"), text)))
46 | sys.stdout.flush()
47 |
48 |
49 | def success(text):
50 | """Display a success message"""
51 | print(" ".join((green(">>"), white(text))))
52 | sys.stdout.flush()
53 |
54 |
55 | def error(text):
56 | """Display an error message"""
57 | print(red("✘ {0}".format(text)))
58 | sys.stdout.flush()
59 |
60 |
61 | def exit(text=None, code=-1):
62 | if text:
63 | error(text)
64 | sys.exit(-1)
65 |
66 |
67 | def build_args(*args):
68 | return " ".join(a for a in args if a)
69 |
70 |
71 | @task
72 | def clean(ctx):
73 | """Cleanup all build artifacts"""
74 | header(clean.__doc__)
75 | with ctx.cd(ROOT):
76 | for pattern in CLEAN_PATTERNS:
77 | info("Removing {0}", pattern)
78 | ctx.run("rm -rf {0}".format(pattern))
79 |
80 |
81 | @task
82 | def deps(ctx):
83 | """Install or update development dependencies"""
84 | header(deps.__doc__)
85 | with ctx.cd(ROOT):
86 | ctx.run(
87 | "pip install -r requirements/develop.pip -r requirements/doc.pip", pty=True
88 | )
89 |
90 |
91 | @task
92 | def demo(ctx):
93 | """Run the demo"""
94 | header(demo.__doc__)
95 | with ctx.cd(ROOT):
96 | ctx.run("python examples/todo.py")
97 |
98 |
99 | @task
100 | def test(ctx, profile=False):
101 | """Run tests suite"""
102 | header(test.__doc__)
103 | kwargs = build_args(
104 | "--benchmark-skip",
105 | "--profile" if profile else None,
106 | )
107 | with ctx.cd(ROOT):
108 | ctx.run("pytest {0}".format(kwargs), pty=True)
109 |
110 |
111 | @task
112 | def benchmark(
113 | ctx,
114 | max_time=2,
115 | save=False,
116 | compare=False,
117 | histogram=False,
118 | profile=False,
119 | tox=False,
120 | ):
121 | """Run benchmarks"""
122 | header(benchmark.__doc__)
123 | ts = datetime.now()
124 | kwargs = build_args(
125 | "--benchmark-max-time={0}".format(max_time),
126 | "--benchmark-autosave" if save else None,
127 | "--benchmark-compare" if compare else None,
128 | (
129 | "--benchmark-histogram=histograms/{0:%Y%m%d-%H%M%S}".format(ts)
130 | if histogram
131 | else None
132 | ),
133 | "--benchmark-cprofile=tottime" if profile else None,
134 | )
135 | cmd = "pytest tests/benchmarks {0}".format(kwargs)
136 | if tox:
137 | envs = ctx.run("tox -l", hide=True).stdout.splitlines()
138 | envs = ",".join(e for e in envs if e != "doc")
139 | cmd = "tox -e {envs} -- {cmd}".format(envs=envs, cmd=cmd)
140 | ctx.run(cmd, pty=True)
141 |
142 |
143 | @task
144 | def cover(ctx, html=False):
145 | """Run tests suite with coverage"""
146 | header(cover.__doc__)
147 | extra = "--cov-report html" if html else ""
148 | with ctx.cd(ROOT):
149 | ctx.run(
150 | "pytest --benchmark-skip --cov flask_restx --cov-report term --cov-report xml {0}".format(
151 | extra
152 | ),
153 | pty=True,
154 | )
155 |
156 |
157 | @task
158 | def tox(ctx):
159 | """Run tests against Python versions"""
160 | header(tox.__doc__)
161 | ctx.run("tox", pty=True)
162 |
163 |
164 | @task
165 | def qa(ctx):
166 | """Run a quality report"""
167 | header(qa.__doc__)
168 | with ctx.cd(ROOT):
169 | info("Ensure PyPI can render README and CHANGELOG")
170 | info("Building dist package")
171 | dist = ctx.run("python setup.py sdist", pty=True, warn=False, hide=True)
172 | if dist.failed:
173 | error("Unable to build sdist package")
174 | exit("Quality check failed", dist.return_code)
175 | readme_results = ctx.run("twine check dist/*", pty=True, warn=True, hide=True)
176 | if readme_results.failed:
177 | print(readme_results.stdout)
178 | error("README and/or CHANGELOG is not renderable by PyPI")
179 | else:
180 | success("README and CHANGELOG are renderable by PyPI")
181 | if readme_results.failed:
182 | exit("Quality check failed", readme_results.return_code)
183 | success("Quality check OK")
184 |
185 |
186 | @task
187 | def doc(ctx):
188 | """Build the documentation"""
189 | header(doc.__doc__)
190 | with ctx.cd(os.path.join(ROOT, "doc")):
191 | ctx.run("make html", pty=True)
192 |
193 |
194 | @task
195 | def assets(ctx):
196 | """Fetch web assets"""
197 | header(assets.__doc__)
198 | with ctx.cd(ROOT):
199 | ctx.run("npm install")
200 | ctx.run("mkdir -p flask_restx/static")
201 | ctx.run(
202 | "cp node_modules/swagger-ui-dist/{swagger-ui*.{css,js}{,.map},favicon*.png,oauth2-redirect.html} flask_restx/static"
203 | )
204 | # Until next release we need to install droid sans separately
205 | ctx.run(
206 | "cp node_modules/typeface-droid-sans/index.css flask_restx/static/droid-sans.css"
207 | )
208 | ctx.run("cp -R node_modules/typeface-droid-sans/files flask_restx/static/")
209 |
210 |
211 | @task
212 | def dist(ctx):
213 | """Package for distribution"""
214 | header(dist.__doc__)
215 | with ctx.cd(ROOT):
216 | ctx.run("python setup.py bdist_wheel", pty=True)
217 |
218 |
219 | @task(clean, deps, test, doc, qa, assets, dist, default=True)
220 | def all(ctx):
221 | """Run tests, reports and packaging"""
222 | pass
223 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-restx/flask-restx/ffb079c696e7901b0b27526810ff32c52beb8aa0/tests/__init__.py
--------------------------------------------------------------------------------
/tests/benchmarks/bench_marshalling.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from faker import Faker
4 |
5 | from flask_restx import marshal, fields
6 |
7 | fake = Faker()
8 |
9 | person_fields = {"name": fields.String, "age": fields.Integer}
10 |
11 | family_fields = {
12 | "father": fields.Nested(person_fields),
13 | "mother": fields.Nested(person_fields),
14 | "children": fields.List(fields.Nested(person_fields)),
15 | }
16 |
17 |
18 | def person():
19 | return {"name": fake.name(), "age": fake.pyint()}
20 |
21 |
22 | def family():
23 | return {"father": person(), "mother": person(), "children": [person(), person()]}
24 |
25 |
26 | def marshal_simple():
27 | return marshal(person(), person_fields)
28 |
29 |
30 | def marshal_nested():
31 | return marshal(family(), family_fields)
32 |
33 |
34 | def marshal_simple_with_mask(app):
35 | with app.test_request_context("/", headers={"X-Fields": "name"}):
36 | return marshal(person(), person_fields)
37 |
38 |
39 | def marshal_nested_with_mask(app):
40 | with app.test_request_context("/", headers={"X-Fields": "father,children{name}"}):
41 | return marshal(family(), family_fields)
42 |
43 |
44 | @pytest.mark.benchmark(group="marshalling")
45 | class MarshallingBenchmark(object):
46 | def bench_marshal_simple(self, benchmark):
47 | benchmark(marshal_simple)
48 |
49 | def bench_marshal_nested(self, benchmark):
50 | benchmark(marshal_nested)
51 |
52 | def bench_marshal_simple_with_mask(self, app, benchmark):
53 | benchmark(marshal_simple_with_mask, app)
54 |
55 | def bench_marshal_nested_with_mask(self, app, benchmark):
56 | benchmark(marshal_nested_with_mask, app)
57 |
--------------------------------------------------------------------------------
/tests/benchmarks/bench_swagger.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_restx import fields, Api, Resource
4 | from flask_restx.swagger import Swagger
5 |
6 | api = Api()
7 |
8 | person = api.model("Person", {"name": fields.String, "age": fields.Integer})
9 |
10 | family = api.model(
11 | "Family",
12 | {
13 | "name": fields.String,
14 | "father": fields.Nested(person),
15 | "mother": fields.Nested(person),
16 | "children": fields.List(fields.Nested(person)),
17 | },
18 | )
19 |
20 |
21 | @api.route("/families", endpoint="families")
22 | class Families(Resource):
23 | @api.marshal_with(family)
24 | def get(self):
25 | """List all families"""
26 | pass
27 |
28 | @api.marshal_with(family)
29 | @api.response(201, "Family created")
30 | def post(self):
31 | """Create a new family"""
32 | pass
33 |
34 |
35 | @api.route("/families//", endpoint="family")
36 | @api.response(404, "Family not found")
37 | class Family(Resource):
38 | @api.marshal_with(family)
39 | def get(self):
40 | """Get a family given its name"""
41 | pass
42 |
43 | @api.marshal_with(family)
44 | def put(self):
45 | """Update a family given its name"""
46 | pass
47 |
48 |
49 | @api.route("/persons", endpoint="persons")
50 | class Persons(Resource):
51 | @api.marshal_with(person)
52 | def get(self):
53 | """List all persons"""
54 | pass
55 |
56 | @api.marshal_with(person)
57 | @api.response(201, "Person created")
58 | def post(self):
59 | """Create a new person"""
60 | pass
61 |
62 |
63 | @api.route("/persons//", endpoint="person")
64 | @api.response(404, "Person not found")
65 | class Person(Resource):
66 | @api.marshal_with(person)
67 | def get(self):
68 | """Get a person given its name"""
69 | pass
70 |
71 | @api.marshal_with(person)
72 | def put(self):
73 | """Update a person given its name"""
74 | pass
75 |
76 |
77 | def swagger_specs(app):
78 | with app.test_request_context("/"):
79 | return Swagger(api).as_dict()
80 |
81 |
82 | def swagger_specs_cached(app):
83 | with app.test_request_context("/"):
84 | return api.__schema__
85 |
86 |
87 | @pytest.mark.benchmark(group="swagger")
88 | class SwaggerBenchmark(object):
89 | @pytest.fixture(autouse=True)
90 | def register(self, app):
91 | api.init_app(app)
92 |
93 | def bench_swagger_specs(self, app, benchmark):
94 | benchmark(swagger_specs, app)
95 |
96 | def bench_swagger_specs_cached(self, app, benchmark):
97 | benchmark(swagger_specs_cached, app)
98 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 |
4 | from flask import Flask, Blueprint
5 | from flask.testing import FlaskClient
6 |
7 | import flask_restx as restx
8 |
9 |
10 | class TestClient(FlaskClient):
11 | # Borrowed from https://pythonadventures.wordpress.com/2016/03/06/detect-duplicate-keys-in-a-json-file/
12 | # Thank you to Wordpress author @ubuntuincident, aka Jabba Laci.
13 | def dict_raise_on_duplicates(self, ordered_pairs):
14 | """Reject duplicate keys."""
15 | d = {}
16 | for k, v in ordered_pairs:
17 | if k in d:
18 | raise ValueError("duplicate key: %r" % (k,))
19 | else:
20 | d[k] = v
21 | return d
22 |
23 | def get_json(self, url, status=200, **kwargs):
24 | response = self.get(url, **kwargs)
25 | assert response.status_code == status
26 | assert response.content_type == "application/json"
27 | return json.loads(
28 | response.data.decode("utf8"),
29 | object_pairs_hook=self.dict_raise_on_duplicates,
30 | )
31 |
32 | def post_json(self, url, data, status=200, **kwargs):
33 | response = self.post(
34 | url, data=json.dumps(data), headers={"content-type": "application/json"}
35 | )
36 | assert response.status_code == status
37 | assert response.content_type == "application/json"
38 | return json.loads(response.data.decode("utf8"))
39 |
40 | def get_specs(self, prefix="", status=200, **kwargs):
41 | """Get a Swagger specification for a RESTX API"""
42 | return self.get_json("{0}/swagger.json".format(prefix), status=status, **kwargs)
43 |
44 |
45 | @pytest.fixture
46 | def app():
47 | app = Flask(__name__, subdomain_matching=True)
48 | app.test_client_class = TestClient
49 | yield app
50 |
51 |
52 | @pytest.fixture
53 | def api(request, app):
54 | marker = request.node.get_closest_marker("api")
55 | bpkwargs = {}
56 | kwargs = {}
57 | if marker:
58 | if "prefix" in marker.kwargs:
59 | bpkwargs["url_prefix"] = marker.kwargs.pop("prefix")
60 | if "subdomain" in marker.kwargs:
61 | bpkwargs["subdomain"] = marker.kwargs.pop("subdomain")
62 | kwargs = marker.kwargs
63 | blueprint = Blueprint("api", __name__, **bpkwargs)
64 | api = restx.Api(blueprint, **kwargs)
65 | app.register_blueprint(blueprint)
66 | yield api
67 |
68 |
69 | @pytest.fixture
70 | def mock_app(mocker):
71 | app = mocker.Mock(Flask)
72 | # mock Flask app object doesn't have any real loggers -> mock logging
73 | # set up on Api object
74 | mocker.patch.object(restx.Api, "_configure_namespace_logger")
75 | app.view_functions = {}
76 | app.extensions = {}
77 | app.config = {}
78 | return app
79 |
80 |
81 | @pytest.fixture(autouse=True)
82 | def _push_custom_request_context(request):
83 | app = request.getfixturevalue("app")
84 | options = request.node.get_closest_marker("request_context")
85 |
86 | if options is None:
87 | return
88 |
89 | ctx = app.test_request_context(*options.args, **options.kwargs)
90 | ctx.push()
91 |
92 | def teardown():
93 | ctx.pop()
94 |
95 | request.addfinalizer(teardown)
96 |
--------------------------------------------------------------------------------
/tests/legacy/test_api_with_blueprint.py:
--------------------------------------------------------------------------------
1 | import flask
2 |
3 | from flask import Blueprint, request
4 |
5 | import flask_restx as restx
6 |
7 |
8 | # Add a dummy Resource to verify that the app is properly set.
9 | class HelloWorld(restx.Resource):
10 | def get(self):
11 | return {}
12 |
13 |
14 | class GoodbyeWorld(restx.Resource):
15 | def __init__(self, err):
16 | self.err = err
17 |
18 | def get(self):
19 | flask.abort(self.err)
20 |
21 |
22 | class APIWithBlueprintTest(object):
23 | def test_api_base(self, app):
24 | blueprint = Blueprint("test", __name__)
25 | api = restx.Api(blueprint)
26 | app.register_blueprint(blueprint)
27 | assert api.urls == {}
28 | assert api.prefix == ""
29 | assert api.default_mediatype == "application/json"
30 |
31 | def test_api_delayed_initialization(self, app):
32 | blueprint = Blueprint("test", __name__)
33 | api = restx.Api()
34 | api.init_app(blueprint)
35 | app.register_blueprint(blueprint)
36 | api.add_resource(HelloWorld, "/", endpoint="hello")
37 |
38 | def test_add_resource_endpoint(self, app, mocker):
39 | blueprint = Blueprint("test", __name__)
40 | api = restx.Api(blueprint)
41 | view = mocker.Mock(**{"as_view.return_value.__name__": str("test_view")})
42 | api.add_resource(view, "/foo", endpoint="bar")
43 | app.register_blueprint(blueprint)
44 | view.as_view.assert_called_with("bar", api)
45 |
46 | def test_add_resource_endpoint_after_registration(self, app, mocker):
47 | blueprint = Blueprint("test", __name__)
48 | api = restx.Api(blueprint)
49 | app.register_blueprint(blueprint)
50 | view = mocker.Mock(**{"as_view.return_value.__name__": str("test_view")})
51 | api.add_resource(view, "/foo", endpoint="bar")
52 | view.as_view.assert_called_with("bar", api)
53 |
54 | def test_url_with_api_prefix(self, app):
55 | blueprint = Blueprint("test", __name__)
56 | api = restx.Api(blueprint, prefix="/api")
57 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
58 | app.register_blueprint(blueprint)
59 | with app.test_request_context("/api/hi"):
60 | assert request.endpoint == "test.hello"
61 |
62 | def test_url_with_blueprint_prefix(self, app):
63 | blueprint = Blueprint("test", __name__, url_prefix="/bp")
64 | api = restx.Api(blueprint)
65 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
66 | app.register_blueprint(blueprint)
67 | with app.test_request_context("/bp/hi"):
68 | assert request.endpoint == "test.hello"
69 |
70 | def test_url_with_registration_prefix(self, app):
71 | blueprint = Blueprint("test", __name__)
72 | api = restx.Api(blueprint)
73 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
74 | app.register_blueprint(blueprint, url_prefix="/reg")
75 | with app.test_request_context("/reg/hi"):
76 | assert request.endpoint == "test.hello"
77 |
78 | def test_registration_prefix_overrides_blueprint_prefix(self, app):
79 | blueprint = Blueprint("test", __name__, url_prefix="/bp")
80 | api = restx.Api(blueprint)
81 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
82 | app.register_blueprint(blueprint, url_prefix="/reg")
83 | with app.test_request_context("/reg/hi"):
84 | assert request.endpoint == "test.hello"
85 |
86 | def test_url_with_api_and_blueprint_prefix(self, app):
87 | blueprint = Blueprint("test", __name__, url_prefix="/bp")
88 | api = restx.Api(blueprint, prefix="/api")
89 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
90 | app.register_blueprint(blueprint)
91 | with app.test_request_context("/bp/api/hi"):
92 | assert request.endpoint == "test.hello"
93 |
94 | def test_error_routing(self, app, mocker):
95 | blueprint = Blueprint("test", __name__)
96 | api = restx.Api(blueprint)
97 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
98 | api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye")
99 | app.register_blueprint(blueprint)
100 | with app.test_request_context("/hi", method="POST"):
101 | assert api._should_use_fr_error_handler() is True
102 | assert api._has_fr_route() is True
103 | with app.test_request_context("/bye"):
104 | api._should_use_fr_error_handler = mocker.Mock(return_value=False)
105 | assert api._has_fr_route() is True
106 |
107 | def test_non_blueprint_rest_error_routing(self, app, mocker):
108 | blueprint = Blueprint("test", __name__)
109 | api = restx.Api(blueprint)
110 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
111 | api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye")
112 | app.register_blueprint(blueprint, url_prefix="/blueprint")
113 | api2 = restx.Api(app)
114 | api2.add_resource(HelloWorld(api), "/hi", endpoint="hello")
115 | api2.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye")
116 | with app.test_request_context("/hi", method="POST"):
117 | assert api._should_use_fr_error_handler() is False
118 | assert api2._should_use_fr_error_handler() is True
119 | assert api._has_fr_route() is False
120 | assert api2._has_fr_route() is True
121 | with app.test_request_context("/blueprint/hi", method="POST"):
122 | assert api._should_use_fr_error_handler() is True
123 | assert api2._should_use_fr_error_handler() is False
124 | assert api._has_fr_route() is True
125 | assert api2._has_fr_route() is False
126 | api._should_use_fr_error_handler = mocker.Mock(return_value=False)
127 | api2._should_use_fr_error_handler = mocker.Mock(return_value=False)
128 | with app.test_request_context("/bye"):
129 | assert api._has_fr_route() is False
130 | assert api2._has_fr_route() is True
131 | with app.test_request_context("/blueprint/bye"):
132 | assert api._has_fr_route() is True
133 | assert api2._has_fr_route() is False
134 |
135 | def test_non_blueprint_non_rest_error_routing(self, app, mocker):
136 | blueprint = Blueprint("test", __name__)
137 | api = restx.Api(blueprint)
138 | api.add_resource(HelloWorld, "/hi", endpoint="hello")
139 | api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye")
140 | app.register_blueprint(blueprint, url_prefix="/blueprint")
141 |
142 | @app.route("/hi")
143 | def hi():
144 | return "hi"
145 |
146 | @app.route("/bye")
147 | def bye():
148 | flask.abort(404)
149 |
150 | with app.test_request_context("/hi", method="POST"):
151 | assert api._should_use_fr_error_handler() is False
152 | assert api._has_fr_route() is False
153 | with app.test_request_context("/blueprint/hi", method="POST"):
154 | assert api._should_use_fr_error_handler() is True
155 | assert api._has_fr_route() is True
156 | api._should_use_fr_error_handler = mocker.Mock(return_value=False)
157 | with app.test_request_context("/bye"):
158 | assert api._has_fr_route() is False
159 | with app.test_request_context("/blueprint/bye"):
160 | assert api._has_fr_route() is True
161 |
--------------------------------------------------------------------------------
/tests/test_accept.py:
--------------------------------------------------------------------------------
1 | import flask_restx as restx
2 |
3 |
4 | class Foo(restx.Resource):
5 | def get(self):
6 | return "data"
7 |
8 |
9 | class ErrorsTest(object):
10 | def test_accept_default_application_json(self, app, client):
11 | api = restx.Api(app)
12 | api.add_resource(Foo, "/test/")
13 |
14 | res = client.get("/test/", headers={"Accept": None})
15 | assert res.status_code == 200
16 | assert res.content_type == "application/json"
17 |
18 | def test_accept_application_json_by_default(self, app, client):
19 | api = restx.Api(app)
20 | api.add_resource(Foo, "/test/")
21 |
22 | res = client.get("/test/", headers=[("Accept", "application/json")])
23 | assert res.status_code == 200
24 | assert res.content_type == "application/json"
25 |
26 | def test_accept_no_default_match_acceptable(self, app, client):
27 | api = restx.Api(app, default_mediatype=None)
28 | api.add_resource(Foo, "/test/")
29 |
30 | res = client.get("/test/", headers=[("Accept", "application/json")])
31 | assert res.status_code == 200
32 | assert res.content_type == "application/json"
33 |
34 | def test_accept_default_override_accept(self, app, client):
35 | api = restx.Api(app)
36 | api.add_resource(Foo, "/test/")
37 |
38 | res = client.get("/test/", headers=[("Accept", "text/plain")])
39 | assert res.status_code == 200
40 | assert res.content_type == "application/json"
41 |
42 | def test_accept_default_any_pick_first(self, app, client):
43 | api = restx.Api(app)
44 |
45 | @api.representation("text/plain")
46 | def text_rep(data, status_code, headers=None):
47 | resp = app.make_response((str(data), status_code, headers))
48 | return resp
49 |
50 | api.add_resource(Foo, "/test/")
51 |
52 | res = client.get("/test/", headers=[("Accept", "*/*")])
53 | assert res.status_code == 200
54 | assert res.content_type == "application/json"
55 |
56 | def test_accept_no_default_no_match_not_acceptable(self, app, client):
57 | api = restx.Api(app, default_mediatype=None)
58 | api.add_resource(Foo, "/test/")
59 |
60 | res = client.get("/test/", headers=[("Accept", "text/plain")])
61 | assert res.status_code == 406
62 | assert res.content_type == "application/json"
63 |
64 | def test_accept_no_default_custom_repr_match(self, app, client):
65 | api = restx.Api(app, default_mediatype=None)
66 | api.representations = {}
67 |
68 | @api.representation("text/plain")
69 | def text_rep(data, status_code, headers=None):
70 | resp = app.make_response((str(data), status_code, headers))
71 | return resp
72 |
73 | api.add_resource(Foo, "/test/")
74 |
75 | res = client.get("/test/", headers=[("Accept", "text/plain")])
76 | assert res.status_code == 200
77 | assert res.content_type == "text/plain"
78 |
79 | def test_accept_no_default_custom_repr_not_acceptable(self, app, client):
80 | api = restx.Api(app, default_mediatype=None)
81 | api.representations = {}
82 |
83 | @api.representation("text/plain")
84 | def text_rep(data, status_code, headers=None):
85 | resp = app.make_response((str(data), status_code, headers))
86 | return resp
87 |
88 | api.add_resource(Foo, "/test/")
89 |
90 | res = client.get("/test/", headers=[("Accept", "application/json")])
91 | assert res.status_code == 406
92 | assert res.content_type == "text/plain"
93 |
94 | def test_accept_no_default_match_q0_not_acceptable(self, app, client):
95 | """
96 | q=0 should be considered NotAcceptable,
97 | but this depends on werkzeug >= 1.0 which is not yet released
98 | so this test is expected to fail until we depend on werkzeug >= 1.0
99 | """
100 | api = restx.Api(app, default_mediatype=None)
101 | api.add_resource(Foo, "/test/")
102 |
103 | res = client.get("/test/", headers=[("Accept", "application/json; q=0")])
104 | assert res.status_code == 406
105 | assert res.content_type == "application/json"
106 |
107 | def test_accept_no_default_accept_highest_quality_of_two(self, app, client):
108 | api = restx.Api(app, default_mediatype=None)
109 |
110 | @api.representation("text/plain")
111 | def text_rep(data, status_code, headers=None):
112 | resp = app.make_response((str(data), status_code, headers))
113 | return resp
114 |
115 | api.add_resource(Foo, "/test/")
116 |
117 | res = client.get(
118 | "/test/", headers=[("Accept", "application/json; q=0.1, text/plain; q=1.0")]
119 | )
120 | assert res.status_code == 200
121 | assert res.content_type == "text/plain"
122 |
123 | def test_accept_no_default_accept_highest_quality_of_three(self, app, client):
124 | api = restx.Api(app, default_mediatype=None)
125 |
126 | @api.representation("text/html")
127 | @api.representation("text/plain")
128 | def text_rep(data, status_code, headers=None):
129 | resp = app.make_response((str(data), status_code, headers))
130 | return resp
131 |
132 | api.add_resource(Foo, "/test/")
133 |
134 | res = client.get(
135 | "/test/",
136 | headers=[
137 | (
138 | "Accept",
139 | "application/json; q=0.1, text/plain; q=0.3, text/html; q=0.2",
140 | )
141 | ],
142 | )
143 | assert res.status_code == 200
144 | assert res.content_type == "text/plain"
145 |
146 | def test_accept_no_default_no_representations(self, app, client):
147 | api = restx.Api(app, default_mediatype=None)
148 | api.representations = {}
149 |
150 | api.add_resource(Foo, "/test/")
151 |
152 | res = client.get("/test/", headers=[("Accept", "text/plain")])
153 | assert res.status_code == 406
154 | assert res.content_type == "text/plain"
155 |
156 | def test_accept_invalid_default_no_representations(self, app, client):
157 | api = restx.Api(app, default_mediatype="nonexistant/mediatype")
158 | api.representations = {}
159 |
160 | api.add_resource(Foo, "/test/")
161 |
162 | res = client.get("/test/", headers=[("Accept", "text/plain")])
163 | assert res.status_code == 500
164 |
--------------------------------------------------------------------------------
/tests/test_apidoc.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask import url_for, Blueprint
4 | from werkzeug.routing import BuildError
5 |
6 | import flask_restx as restx
7 |
8 |
9 | class APIDocTest(object):
10 | def test_default_apidoc_on_root(self, app, client):
11 | restx.Api(app, version="1.0")
12 |
13 | assert url_for("doc") == url_for("root")
14 |
15 | response = client.get(url_for("doc"))
16 | assert response.status_code == 200
17 | assert response.content_type == "text/html; charset=utf-8"
18 |
19 | def test_default_apidoc_on_root_lazy(self, app, client):
20 | api = restx.Api(version="1.0")
21 | api.init_app(app)
22 |
23 | assert url_for("doc") == url_for("root")
24 |
25 | response = client.get(url_for("doc"))
26 | assert response.status_code == 200
27 | assert response.content_type == "text/html; charset=utf-8"
28 |
29 | def test_default_apidoc_on_root_with_blueprint(self, app, client):
30 | blueprint = Blueprint("api", __name__, url_prefix="/api")
31 | restx.Api(blueprint, version="1.0")
32 | app.register_blueprint(blueprint)
33 |
34 | assert url_for("api.doc") == url_for("api.root")
35 |
36 | response = client.get(url_for("api.doc"))
37 | assert response.status_code == 200
38 | assert response.content_type == "text/html; charset=utf-8"
39 |
40 | def test_apidoc_with_custom_validator(self, app, client):
41 | app.config["SWAGGER_VALIDATOR_URL"] = "http://somewhere.com/validator"
42 | restx.Api(app, version="1.0")
43 |
44 | response = client.get(url_for("doc"))
45 | assert response.status_code == 200
46 | assert response.content_type == "text/html; charset=utf-8"
47 | assert 'validatorUrl: "http://somewhere.com/validator" || null,' in str(
48 | response.data
49 | )
50 |
51 | def test_apidoc_doc_expansion_parameter(self, app, client):
52 | restx.Api(app)
53 |
54 | response = client.get(url_for("doc"))
55 | assert 'docExpansion: "none"' in str(response.data)
56 |
57 | app.config["SWAGGER_UI_DOC_EXPANSION"] = "list"
58 | response = client.get(url_for("doc"))
59 | assert 'docExpansion: "list"' in str(response.data)
60 |
61 | app.config["SWAGGER_UI_DOC_EXPANSION"] = "full"
62 | response = client.get(url_for("doc"))
63 | assert 'docExpansion: "full"' in str(response.data)
64 |
65 | def test_apidoc_doc_display_operation_id(self, app, client):
66 | restx.Api(app)
67 |
68 | response = client.get(url_for("doc"))
69 | assert "displayOperationId: false" in str(response.data)
70 |
71 | app.config["SWAGGER_UI_OPERATION_ID"] = False
72 | response = client.get(url_for("doc"))
73 | assert "displayOperationId: false" in str(response.data)
74 |
75 | app.config["SWAGGER_UI_OPERATION_ID"] = True
76 | response = client.get(url_for("doc"))
77 | assert "displayOperationId: true" in str(response.data)
78 |
79 | def test_apidoc_doc_display_request_duration(self, app, client):
80 | restx.Api(app)
81 |
82 | response = client.get(url_for("doc"))
83 | assert "displayRequestDuration: false" in str(response.data)
84 |
85 | app.config["SWAGGER_UI_REQUEST_DURATION"] = False
86 | response = client.get(url_for("doc"))
87 | assert "displayRequestDuration: false" in str(response.data)
88 |
89 | app.config["SWAGGER_UI_REQUEST_DURATION"] = True
90 | response = client.get(url_for("doc"))
91 | assert "displayRequestDuration: true" in str(response.data)
92 |
93 | def test_custom_apidoc_url(self, app, client):
94 | restx.Api(app, version="1.0", doc="/doc/")
95 |
96 | doc_url = url_for("doc")
97 | root_url = url_for("root")
98 |
99 | assert doc_url != root_url
100 |
101 | response = client.get(root_url)
102 | assert response.status_code == 404
103 |
104 | assert doc_url == "/doc/"
105 | response = client.get(doc_url)
106 | assert response.status_code == 200
107 | assert response.content_type == "text/html; charset=utf-8"
108 |
109 | def test_custom_api_prefix(self, app, client):
110 | prefix = "/api"
111 | api = restx.Api(app, prefix=prefix)
112 | api.namespace("resource")
113 | assert url_for("root") == prefix
114 |
115 | def test_custom_apidoc_page(self, app, client):
116 | api = restx.Api(app, version="1.0")
117 | content = "My Custom API Doc"
118 |
119 | @api.documentation
120 | def api_doc():
121 | return content
122 |
123 | response = client.get(url_for("doc"))
124 | assert response.status_code == 200
125 | assert response.data.decode("utf8") == content
126 |
127 | def test_custom_apidoc_page_lazy(self, app, client):
128 | blueprint = Blueprint("api", __name__, url_prefix="/api")
129 | api = restx.Api(blueprint, version="1.0")
130 | content = "My Custom API Doc"
131 |
132 | @api.documentation
133 | def api_doc():
134 | return content
135 |
136 | app.register_blueprint(blueprint)
137 |
138 | response = client.get(url_for("api.doc"))
139 | assert response.status_code == 200
140 | assert response.data.decode("utf8") == content
141 |
142 | def test_disabled_apidoc(self, app, client):
143 | restx.Api(app, version="1.0", doc=False)
144 |
145 | with pytest.raises(BuildError):
146 | url_for("doc")
147 |
148 | response = client.get(url_for("root"))
149 | assert response.status_code == 404
150 |
--------------------------------------------------------------------------------
/tests/test_cors.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Api, Resource, cors
2 |
3 |
4 | class ErrorsTest(object):
5 | def test_crossdomain(self, app, client):
6 | class Foo(Resource):
7 | @cors.crossdomain(origin="*")
8 | def get(self):
9 | return "data"
10 |
11 | api = Api(app)
12 | api.add_resource(Foo, "/test/")
13 |
14 | res = client.get("/test/")
15 | assert res.status_code == 200
16 | assert res.headers["Access-Control-Allow-Origin"] == "*"
17 | assert res.headers["Access-Control-Max-Age"] == "21600"
18 | assert "HEAD" in res.headers["Access-Control-Allow-Methods"]
19 | assert "OPTIONS" in res.headers["Access-Control-Allow-Methods"]
20 | assert "GET" in res.headers["Access-Control-Allow-Methods"]
21 |
22 | def test_access_control_expose_headers(self, app, client):
23 | class Foo(Resource):
24 | @cors.crossdomain(
25 | origin="*", expose_headers=["X-My-Header", "X-Another-Header"]
26 | )
27 | def get(self):
28 | return "data"
29 |
30 | api = Api(app)
31 | api.add_resource(Foo, "/test/")
32 |
33 | res = client.get("/test/")
34 | assert res.status_code == 200
35 | assert "X-MY-HEADER" in res.headers["Access-Control-Expose-Headers"]
36 | assert "X-ANOTHER-HEADER" in res.headers["Access-Control-Expose-Headers"]
37 |
38 | def test_no_crossdomain(self, app, client):
39 | class Foo(Resource):
40 | def get(self):
41 | return "data"
42 |
43 | api = Api(app)
44 | api.add_resource(Foo, "/test/")
45 |
46 | res = client.get("/test/")
47 | assert res.status_code == 200
48 | assert "Access-Control-Allow-Origin" not in res.headers
49 | assert "Access-Control-Allow-Methods" not in res.headers
50 | assert "Access-Control-Max-Age" not in res.headers
51 |
--------------------------------------------------------------------------------
/tests/test_logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import flask_restx as restx
4 |
5 |
6 | class LoggingTest(object):
7 | def test_namespace_loggers_log_to_flask_app_logger(self, app, client, caplog):
8 | # capture Flask app logs
9 | caplog.set_level(logging.INFO, logger=app.logger.name)
10 |
11 | api = restx.Api(app)
12 | ns1 = api.namespace("ns1", path="/ns1")
13 | ns2 = api.namespace("ns2", path="/ns2")
14 |
15 | @ns1.route("/")
16 | class Ns1(restx.Resource):
17 | def get(self):
18 | ns1.logger.info("hello from ns1")
19 | pass
20 |
21 | @ns2.route("/")
22 | class Ns2(restx.Resource):
23 | def get(self):
24 | ns2.logger.info("hello from ns2")
25 | pass
26 |
27 | # debug log not shown
28 | client.get("/ns1/")
29 | matching = [r for r in caplog.records if r.message == "hello from ns1"]
30 | assert len(matching) == 1
31 |
32 | # info log shown
33 | client.get("/ns2/")
34 | matching = [r for r in caplog.records if r.message == "hello from ns2"]
35 | assert len(matching) == 1
36 |
37 | def test_defaults_to_app_level(self, app, client, caplog):
38 | caplog.set_level(logging.INFO, logger=app.logger.name)
39 |
40 | api = restx.Api(app)
41 | ns1 = api.namespace("ns1", path="/ns1")
42 | ns2 = api.namespace("ns2", path="/ns2")
43 |
44 | @ns1.route("/")
45 | class Ns1(restx.Resource):
46 | def get(self):
47 | ns1.logger.debug("hello from ns1")
48 | pass
49 |
50 | @ns2.route("/")
51 | class Ns2(restx.Resource):
52 | def get(self):
53 | ns2.logger.info("hello from ns2")
54 | pass
55 |
56 | # debug log not shown
57 | client.get("/ns1/")
58 | matching = [r for r in caplog.records if r.message == "hello from ns1"]
59 | assert len(matching) == 0
60 |
61 | # info log shown
62 | client.get("/ns2/")
63 | matching = [r for r in caplog.records if r.message == "hello from ns2"]
64 | assert len(matching) == 1
65 |
66 | def test_override_app_level(self, app, client, caplog):
67 | caplog.set_level(logging.DEBUG, logger=app.logger.name)
68 |
69 | api = restx.Api(app)
70 | ns1 = api.namespace("ns1", path="/ns1")
71 | ns1.logger.setLevel(logging.DEBUG)
72 | ns2 = api.namespace("ns2", path="/ns2")
73 | ns2.logger.setLevel(logging.INFO)
74 |
75 | @ns1.route("/")
76 | class Ns1(restx.Resource):
77 | def get(self):
78 | ns1.logger.debug("hello from ns1")
79 | pass
80 |
81 | @ns2.route("/")
82 | class Ns2(restx.Resource):
83 | def get(self):
84 | ns2.logger.debug("hello from ns2")
85 | pass
86 |
87 | # debug log shown from ns1
88 | client.get("/ns1/")
89 | matching = [r for r in caplog.records if r.message == "hello from ns1"]
90 | assert len(matching) == 1
91 |
92 | # debug not shown from ns2
93 | client.get("/ns2/")
94 | matching = [r for r in caplog.records if r.message == "hello from ns2"]
95 | assert len(matching) == 0
96 |
97 | def test_namespace_additional_handler(self, app, client, caplog, tmp_path):
98 | caplog.set_level(logging.INFO, logger=app.logger.name)
99 | log_file = tmp_path / "v1.log"
100 |
101 | api = restx.Api(app)
102 | ns1 = api.namespace("ns1", path="/ns1")
103 | # set up a file handler for ns1 only
104 | # FileHandler only supports Path object on Python >= 3.6 -> cast to str
105 | fh = logging.FileHandler(str(log_file))
106 | ns1.logger.addHandler(fh)
107 |
108 | ns2 = api.namespace("ns2", path="/ns2")
109 |
110 | @ns1.route("/")
111 | class Ns1(restx.Resource):
112 | def get(self):
113 | ns1.logger.info("hello from ns1")
114 | pass
115 |
116 | @ns2.route("/")
117 | class Ns2(restx.Resource):
118 | def get(self):
119 | ns2.logger.info("hello from ns2")
120 | pass
121 |
122 | # ns1 logs go to stdout and log_file
123 | client.get("/ns1/")
124 | matching = [r for r in caplog.records if r.message == "hello from ns1"]
125 | assert len(matching) == 1
126 | with log_file.open() as f:
127 | assert "hello from ns1" in f.read()
128 |
129 | # ns2 logs go to stdout only
130 | client.get("/ns2/")
131 | matching = [r for r in caplog.records if r.message == "hello from ns2"]
132 | assert len(matching) == 1
133 | with log_file.open() as f:
134 | assert "hello from ns1" in f.read()
135 |
--------------------------------------------------------------------------------
/tests/test_namespace.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import re
3 |
4 | import flask_restx as restx
5 |
6 | from flask_restx import Namespace, Model, OrderedModel
7 |
8 |
9 | class NamespaceTest(object):
10 | def test_parser(self):
11 | api = Namespace("test")
12 | assert isinstance(api.parser(), restx.reqparse.RequestParser)
13 |
14 | def test_doc_decorator(self):
15 | api = Namespace("test")
16 | params = {"q": {"description": "some description"}}
17 |
18 | @api.doc(params=params)
19 | class TestResource(restx.Resource):
20 | pass
21 |
22 | assert hasattr(TestResource, "__apidoc__")
23 | assert TestResource.__apidoc__ == {"params": params}
24 |
25 | def test_doc_with_inheritance(self):
26 | api = Namespace("test")
27 | base_params = {
28 | "q": {
29 | "description": "some description",
30 | "type": "string",
31 | "paramType": "query",
32 | }
33 | }
34 | child_params = {
35 | "q": {"description": "some new description"},
36 | "other": {"description": "another param"},
37 | }
38 |
39 | @api.doc(params=base_params)
40 | class BaseResource(restx.Resource):
41 | pass
42 |
43 | @api.doc(params=child_params)
44 | class TestResource(BaseResource):
45 | pass
46 |
47 | assert TestResource.__apidoc__ == {
48 | "params": {
49 | "q": {
50 | "description": "some new description",
51 | "type": "string",
52 | "paramType": "query",
53 | },
54 | "other": {"description": "another param"},
55 | }
56 | }
57 |
58 | def test_model(self):
59 | api = Namespace("test")
60 | api.model("Person", {})
61 | assert "Person" in api.models
62 | assert isinstance(api.models["Person"], Model)
63 |
64 | def test_ordered_model(self):
65 | api = Namespace("test", ordered=True)
66 | api.model("Person", {})
67 | assert "Person" in api.models
68 | assert isinstance(api.models["Person"], OrderedModel)
69 |
70 | def test_schema_model(self):
71 | api = Namespace("test")
72 | api.schema_model("Person", {})
73 | assert "Person" in api.models
74 |
75 | def test_clone(self):
76 | api = Namespace("test")
77 | parent = api.model("Parent", {})
78 | api.clone("Child", parent, {})
79 |
80 | assert "Child" in api.models
81 | assert "Parent" in api.models
82 |
83 | def test_clone_with_multiple_parents(self):
84 | api = Namespace("test")
85 | grand_parent = api.model("GrandParent", {})
86 | parent = api.model("Parent", {})
87 | api.clone("Child", grand_parent, parent, {})
88 |
89 | assert "Child" in api.models
90 | assert "Parent" in api.models
91 | assert "GrandParent" in api.models
92 |
93 | def test_inherit(self):
94 | authorizations = {
95 | "apikey": {"type": "apiKey", "in": "header", "name": "X-API-KEY"}
96 | }
97 | api = Namespace("test", authorizations=authorizations)
98 | parent = api.model("Parent", {})
99 | api.inherit("Child", parent, {})
100 |
101 | assert "Parent" in api.models
102 | assert "Child" in api.models
103 | assert api.authorizations == authorizations
104 |
105 | def test_inherit_from_multiple_parents(self):
106 | api = Namespace("test")
107 | grand_parent = api.model("GrandParent", {})
108 | parent = api.model("Parent", {})
109 | api.inherit("Child", grand_parent, parent, {})
110 |
111 | assert "GrandParent" in api.models
112 | assert "Parent" in api.models
113 | assert "Child" in api.models
114 |
115 | def test_api_payload(self, app, client):
116 | api = restx.Api(app, validate=True)
117 | ns = restx.Namespace("apples")
118 | api.add_namespace(ns)
119 |
120 | fields = ns.model(
121 | "Person",
122 | {
123 | "name": restx.fields.String(required=True),
124 | "age": restx.fields.Integer,
125 | "birthdate": restx.fields.DateTime,
126 | },
127 | )
128 |
129 | @ns.route("/validation/")
130 | class Payload(restx.Resource):
131 | payload = None
132 |
133 | @ns.expect(fields)
134 | def post(self):
135 | Payload.payload = ns.payload
136 | return {}
137 |
138 | data = {
139 | "name": "John Doe",
140 | "age": 15,
141 | }
142 |
143 | client.post_json("/apples/validation/", data)
144 |
145 | assert Payload.payload == data
146 |
147 | def test_api_payload_strict_verification(self, app, client):
148 | api = restx.Api(app, validate=True)
149 | ns = restx.Namespace("apples")
150 | api.add_namespace(ns)
151 |
152 | fields = ns.model(
153 | "Person",
154 | {
155 | "name": restx.fields.String(required=True),
156 | "age": restx.fields.Integer,
157 | "birthdate": restx.fields.DateTime,
158 | },
159 | strict=True,
160 | )
161 |
162 | @ns.route("/validation/")
163 | class Payload(restx.Resource):
164 | payload = None
165 |
166 | @ns.expect(fields)
167 | def post(self):
168 | Payload.payload = ns.payload
169 | return {}
170 |
171 | data = {
172 | "name": "John Doe",
173 | "agge": 15, # typo
174 | }
175 |
176 | resp = client.post_json("/apples/validation/", data, status=400)
177 | assert re.match(
178 | "Additional properties are not allowed \(u*'agge' was unexpected\)",
179 | resp["errors"][""],
180 | )
181 |
--------------------------------------------------------------------------------
/tests/test_schemas.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from jsonschema import ValidationError
4 |
5 | from flask_restx import errors, schemas
6 |
7 |
8 | class SchemasTest:
9 | def test_lazyness(self):
10 | schema = schemas.LazySchema("oas-2.0.json")
11 | assert schema._schema is None
12 |
13 | "" in schema # Trigger load
14 | assert schema._schema is not None
15 | assert isinstance(schema._schema, dict)
16 |
17 | def test_oas2_schema_is_present(self):
18 | assert hasattr(schemas, "OAS_20")
19 | assert isinstance(schemas.OAS_20, schemas.LazySchema)
20 |
21 |
22 | class ValidationTest:
23 | def test_oas_20_valid(self):
24 | assert schemas.validate(
25 | {
26 | "swagger": "2.0",
27 | "info": {
28 | "title": "An empty minimal specification",
29 | "version": "1.0",
30 | },
31 | "paths": {},
32 | }
33 | )
34 |
35 | def test_oas_20_invalid(self):
36 | with pytest.raises(schemas.SchemaValidationError) as excinfo:
37 | schemas.validate(
38 | {
39 | "swagger": "2.0",
40 | "should": "not be here",
41 | }
42 | )
43 | for error in excinfo.value.errors:
44 | assert isinstance(error, ValidationError)
45 |
46 | def test_unknown_schema(self):
47 | with pytest.raises(errors.SpecsError):
48 | schemas.validate({"valid": "no"})
49 |
50 | def test_unknown_version(self):
51 | with pytest.raises(errors.SpecsError):
52 | schemas.validate({"swagger": "42.0"})
53 |
--------------------------------------------------------------------------------
/tests/test_swagger_utils.py:
--------------------------------------------------------------------------------
1 | from flask_restx.swagger import extract_path, extract_path_params, parse_docstring
2 |
3 |
4 | class ExtractPathTest(object):
5 | def test_extract_static_path(self):
6 | path = "/test"
7 | assert extract_path(path) == "/test"
8 |
9 | def test_extract_path_with_a_single_simple_parameter(self):
10 | path = "/test/"
11 | assert extract_path(path) == "/test/{parameter}"
12 |
13 | def test_extract_path_with_a_single_typed_parameter(self):
14 | path = "/test/"
15 | assert extract_path(path) == "/test/{parameter}"
16 |
17 | def test_extract_path_with_a_single_typed_parameter_with_arguments(self):
18 | path = "/test/"
19 | assert extract_path(path) == "/test/{parameter}"
20 |
21 | def test_extract_path_with_multiple_parameters(self):
22 | path = "/test///"
23 | assert extract_path(path) == "/test/{parameter}/{other}/"
24 |
25 |
26 | class ExtractPathParamsTestCase(object):
27 | def test_extract_static_path(self):
28 | path = "/test"
29 | assert extract_path_params(path) == {}
30 |
31 | def test_extract_single_simple_parameter(self):
32 | path = "/test/"
33 | assert extract_path_params(path) == {
34 | "parameter": {
35 | "name": "parameter",
36 | "type": "string",
37 | "in": "path",
38 | "required": True,
39 | }
40 | }
41 |
42 | def test_single_int_parameter(self):
43 | path = "/test/"
44 | assert extract_path_params(path) == {
45 | "parameter": {
46 | "name": "parameter",
47 | "type": "integer",
48 | "in": "path",
49 | "required": True,
50 | }
51 | }
52 |
53 | def test_single_float_parameter(self):
54 | path = "/test/"
55 | assert extract_path_params(path) == {
56 | "parameter": {
57 | "name": "parameter",
58 | "type": "number",
59 | "in": "path",
60 | "required": True,
61 | }
62 | }
63 |
64 | def test_extract_path_with_multiple_parameters(self):
65 | path = "/test///"
66 | assert extract_path_params(path) == {
67 | "parameter": {
68 | "name": "parameter",
69 | "type": "string",
70 | "in": "path",
71 | "required": True,
72 | },
73 | "other": {
74 | "name": "other",
75 | "type": "integer",
76 | "in": "path",
77 | "required": True,
78 | },
79 | }
80 |
81 | def test_extract_parameter_with_arguments(self):
82 | path = "/test/"
83 | assert extract_path_params(path) == {
84 | "parameter": {
85 | "name": "parameter",
86 | "type": "string",
87 | "in": "path",
88 | "required": True,
89 | }
90 | }
91 |
92 | # def test_extract_registered_converters(self):
93 | # class ListConverter(BaseConverter):
94 | # def to_python(self, value):
95 | # return value.split(',')
96 |
97 | # def to_url(self, values):
98 | # return ','.join(super(ListConverter, self).to_url(value) for value in values)
99 |
100 | # self.app.url_map.converters['list'] = ListConverter
101 |
102 | # path = '/test/'
103 | # with self.context():
104 | # self.assertEqual(extract_path_params(path), [{
105 | # 'name': 'parameters',
106 | # 'type': 'number',
107 | # 'in': 'path',
108 | # 'required': True
109 | # }])
110 |
111 |
112 | class ParseDocstringTest(object):
113 | def test_empty(self):
114 | def without_doc():
115 | pass
116 |
117 | parsed = parse_docstring(without_doc)
118 |
119 | assert parsed["raw"] is None
120 | assert parsed["summary"] is None
121 | assert parsed["details"] is None
122 | assert parsed["returns"] is None
123 | assert parsed["raises"] == {}
124 | assert parsed["params"] == []
125 |
126 | def test_single_line(self):
127 | def func():
128 | """Some summary"""
129 | pass
130 |
131 | parsed = parse_docstring(func)
132 |
133 | assert parsed["raw"] == "Some summary"
134 | assert parsed["summary"] == "Some summary"
135 | assert parsed["details"] is None
136 | assert parsed["returns"] is None
137 | assert parsed["raises"] == {}
138 | assert parsed["params"] == []
139 |
140 | def test_multi_line(self):
141 | def func():
142 | """
143 | Some summary
144 | Some details
145 | """
146 | pass
147 |
148 | parsed = parse_docstring(func)
149 |
150 | assert parsed["raw"] == "Some summary\nSome details"
151 | assert parsed["summary"] == "Some summary"
152 | assert parsed["details"] == "Some details"
153 | assert parsed["returns"] is None
154 | assert parsed["raises"] == {}
155 | assert parsed["params"] == []
156 |
157 | def test_multi_line_and_dot(self):
158 | def func():
159 | """
160 | Some summary. bla bla
161 | Some details
162 | """
163 | pass
164 |
165 | parsed = parse_docstring(func)
166 |
167 | assert parsed["raw"] == "Some summary. bla bla\nSome details"
168 | assert parsed["summary"] == "Some summary"
169 | assert parsed["details"] == "bla bla\nSome details"
170 | assert parsed["returns"] is None
171 | assert parsed["raises"] == {}
172 | assert parsed["params"] == []
173 |
174 | def test_raises(self):
175 | def func():
176 | """
177 | Some summary.
178 | :raises SomeException: in case of something
179 | """
180 | pass
181 |
182 | parsed = parse_docstring(func)
183 |
184 | assert (
185 | parsed["raw"]
186 | == "Some summary.\n:raises SomeException: in case of something"
187 | )
188 | assert parsed["summary"] == "Some summary"
189 | assert parsed["details"] is None
190 | assert parsed["returns"] is None
191 | assert parsed["params"] == []
192 | assert parsed["raises"] == {"SomeException": "in case of something"}
193 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_restx import utils
4 |
5 |
6 | class MergeTestCase(object):
7 | def test_merge_simple_dicts_without_precedence(self):
8 | a = {"a": "value"}
9 | b = {"b": "other value"}
10 | assert utils.merge(a, b) == {"a": "value", "b": "other value"}
11 |
12 | def test_merge_simple_dicts_with_precedence(self):
13 | a = {"a": "value", "ab": "overwritten"}
14 | b = {"b": "other value", "ab": "keep"}
15 | assert utils.merge(a, b) == {"a": "value", "b": "other value", "ab": "keep"}
16 |
17 | def test_recursions(self):
18 | a = {
19 | "a": "value",
20 | "ab": "overwritten",
21 | "nested_a": {"a": "nested"},
22 | "nested_a_b": {"a": "a only", "ab": "overwritten"},
23 | }
24 | b = {
25 | "b": "other value",
26 | "ab": "keep",
27 | "nested_b": {"b": "nested"},
28 | "nested_a_b": {"b": "b only", "ab": "keep"},
29 | }
30 | assert utils.merge(a, b) == {
31 | "a": "value",
32 | "b": "other value",
33 | "ab": "keep",
34 | "nested_a": {"a": "nested"},
35 | "nested_b": {"b": "nested"},
36 | "nested_a_b": {"a": "a only", "b": "b only", "ab": "keep"},
37 | }
38 |
39 | def test_recursions_with_empty(self):
40 | a = {}
41 | b = {
42 | "b": "other value",
43 | "ab": "keep",
44 | "nested_b": {"b": "nested"},
45 | "nested_a_b": {"b": "b only", "ab": "keep"},
46 | }
47 | assert utils.merge(a, b) == b
48 |
49 |
50 | class UnpackImportResponse(object):
51 | def test_import_werkzeug_response(self):
52 | assert utils.import_werkzeug_response() != None
53 |
54 |
55 | class CamelToDashTestCase(object):
56 | def test_no_transform(self):
57 | assert utils.camel_to_dash("test") == "test"
58 |
59 | @pytest.mark.parametrize(
60 | "value,expected",
61 | [
62 | ("aValue", "a_value"),
63 | ("aLongValue", "a_long_value"),
64 | ("Upper", "upper"),
65 | ("UpperCase", "upper_case"),
66 | ],
67 | )
68 | def test_transform(self, value, expected):
69 | assert utils.camel_to_dash(value) == expected
70 |
71 |
72 | class UnpackTest(object):
73 | def test_single_value(self):
74 | data, code, headers = utils.unpack("test")
75 | assert data == "test"
76 | assert code == 200
77 | assert headers == {}
78 |
79 | def test_single_value_with_default_code(self):
80 | data, code, headers = utils.unpack("test", 500)
81 | assert data == "test"
82 | assert code == 500
83 | assert headers == {}
84 |
85 | def test_value_code(self):
86 | data, code, headers = utils.unpack(("test", 201))
87 | assert data == "test"
88 | assert code == 201
89 | assert headers == {}
90 |
91 | def test_value_code_headers(self):
92 | data, code, headers = utils.unpack(("test", 201, {"Header": "value"}))
93 | assert data == "test"
94 | assert code == 201
95 | assert headers == {"Header": "value"}
96 |
97 | def test_value_headers_default_code(self):
98 | data, code, headers = utils.unpack(("test", None, {"Header": "value"}))
99 | assert data == "test"
100 | assert code == 200
101 | assert headers == {"Header": "value"}
102 |
103 | def test_too_many_values(self):
104 | with pytest.raises(ValueError):
105 | utils.unpack((None, None, None, None))
106 |
107 |
108 | class ToViewNameTest(object):
109 | def test_none(self):
110 | with pytest.raises(AssertionError):
111 | _ = utils.to_view_name(None)
112 |
113 | def test_name(self):
114 | assert utils.to_view_name(self.test_none) == self.test_none.__name__
115 |
116 |
117 | class ImportCheckViewFuncTest(object):
118 | def test_callable(self):
119 | assert callable(utils.import_check_view_func())
120 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://tox.testrun.org/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist =
8 | py{39, 310, 311}-flask2,
9 | py{311, 312}-flask3
10 | pypy3.9
11 | doc
12 |
13 | [testenv]
14 | commands = {posargs:inv test qa}
15 | deps =
16 | flask2: flask<3.0.0
17 | flask3: flask>=3.0.0
18 | -r{toxinidir}/requirements/test.pip
19 | -r{toxinidir}/requirements/develop.pip
20 |
21 | [testenv:doc]
22 | changedir = doc
23 | deps = .[doc]
24 | commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
25 |
--------------------------------------------------------------------------------