├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── pull_request_template.md └── workflows │ ├── lock.yaml │ ├── pre-commit.yaml │ ├── publish.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── docs ├── Makefile ├── _static │ ├── quart-icon.svg │ ├── quart-logo.svg │ ├── quart-name-dark.svg │ └── quart-name.svg ├── changes.md ├── conf.py ├── discussion │ ├── async_compatibility.rst │ ├── background_tasks.rst │ ├── contexts.rst │ ├── design_choices.rst │ ├── dos_mitigations.rst │ ├── flask_evolution.rst │ ├── globals.rst │ ├── index.rst │ ├── python_versions.rst │ └── websockets_discussion.rst ├── how_to_guides │ ├── background_tasks.rst │ ├── blueprints.rst │ ├── command_line.rst │ ├── configuration.rst │ ├── developing.rst │ ├── disconnections.rst │ ├── event_loop.rst │ ├── flask_extensions.rst │ ├── flask_migration.rst │ ├── index.rst │ ├── json_encoding.rst │ ├── logging.rst │ ├── middleware.rst │ ├── quart_extensions.rst │ ├── request_body.rst │ ├── routing.rst │ ├── server_sent_events.rst │ ├── session_storage.rst │ ├── startup_shutdown.rst │ ├── streaming_response.rst │ ├── sync_code.rst │ ├── templating.rst │ ├── testing.rst │ ├── using_http2.rst │ └── websockets.rst ├── index.rst ├── license.md ├── make.bat ├── reference │ ├── api.rst │ ├── cheatsheet.rst │ ├── index.rst │ ├── logo.rst │ ├── response_values.rst │ └── versioning.rst └── tutorials │ ├── api_tutorial.rst │ ├── asyncio.rst │ ├── blog_tutorial.rst │ ├── chat_tutorial.rst │ ├── deployment.rst │ ├── index.rst │ ├── installation.rst │ ├── quickstart.rst │ └── video_tutorial.rst ├── examples ├── api │ ├── README.rst │ ├── pyproject.toml │ ├── src │ │ └── api │ │ │ └── __init__.py │ └── tests │ │ ├── __init__.py │ │ └── test_api.py ├── blog │ ├── README.rst │ ├── pyproject.toml │ ├── src │ │ └── blog │ │ │ ├── __init__.py │ │ │ ├── schema.sql │ │ │ └── templates │ │ │ ├── create.html │ │ │ └── posts.html │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_blog.py ├── chat │ ├── README.rst │ ├── pyproject.toml │ ├── src │ │ └── chat │ │ │ ├── __init__.py │ │ │ ├── broker.py │ │ │ └── templates │ │ │ └── index.html │ └── tests │ │ ├── __init__.py │ │ └── test_chat.py └── video │ ├── README.rst │ ├── pyproject.toml │ ├── src │ └── video │ │ ├── __init__.py │ │ ├── static │ │ └── video.mp4 │ │ └── templates │ │ └── index.html │ └── tests │ ├── __init__.py │ └── test_video.py ├── pyproject.toml ├── src └── quart │ ├── __init__.py │ ├── __main__.py │ ├── app.py │ ├── asgi.py │ ├── blueprints.py │ ├── cli.py │ ├── config.py │ ├── ctx.py │ ├── datastructures.py │ ├── debug.py │ ├── formparser.py │ ├── globals.py │ ├── helpers.py │ ├── json │ ├── __init__.py │ ├── provider.py │ └── tag.py │ ├── logging.py │ ├── py.typed │ ├── routing.py │ ├── sessions.py │ ├── signals.py │ ├── templating.py │ ├── testing │ ├── __init__.py │ ├── app.py │ ├── client.py │ ├── connections.py │ └── utils.py │ ├── typing.py │ ├── utils.py │ ├── views.py │ └── wrappers │ ├── __init__.py │ ├── base.py │ ├── request.py │ ├── response.py │ └── websocket.py ├── tests ├── assets │ └── config.cfg ├── conftest.py ├── test_app.py ├── test_asgi.py ├── test_background_tasks.py ├── test_basic.py ├── test_blueprints.py ├── test_cli.py ├── test_ctx.py ├── test_debug.py ├── test_exceptions.py ├── test_formparser.py ├── test_helpers.py ├── test_routing.py ├── test_sessions.py ├── test_static_hosting.py ├── test_sync.py ├── test_templating.py ├── test_testing.py ├── test_utils.py ├── test_views.py └── wrappers │ ├── test_base.py │ ├── test_request.py │ └── test_response.py └── uv.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | charset = utf-8 10 | max_line_length = 88 11 | 12 | [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in Quart (not other projects which depend on Quart) 4 | --- 5 | 6 | <!-- 7 | This issue tracker is a tool to address bugs in Quart itself. Please use 8 | GitHub Discussions or the Pallets Discord for questions about your own code. 9 | 10 | Replace this comment with a clear outline of what the bug is. 11 | --> 12 | 13 | <!-- 14 | Describe how to replicate the bug. 15 | 16 | Include a minimal reproducible example that demonstrates the bug. 17 | Include the full traceback if there was an exception. 18 | --> 19 | 20 | <!-- 21 | Describe the expected behavior that should have happened but didn't. 22 | --> 23 | 24 | Environment: 25 | 26 | - Python version: 27 | - Quart version: 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Security issue 4 | url: https://github.com/pallets/quart/security/advisories/new 5 | about: Do not report security issues publicly. Create a private advisory. 6 | - name: Questions 7 | url: https://github.com/pallets/quart/discussions/ 8 | about: Ask questions about your own code on the Discussions tab. 9 | - name: Questions on 10 | url: https://discord.gg/pallets 11 | about: Ask questions about your own code on our Discord chat. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for Quart 4 | --- 5 | 6 | <!-- 7 | Replace this comment with a description of what the feature should do. 8 | Include details such as links to relevant specs or previous discussions. 9 | --> 10 | 11 | <!-- 12 | Replace this comment with an example of the problem which this feature 13 | would resolve. Is this problem solvable without changes to Quart, such 14 | as by subclassing or using an extension? 15 | --> 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | <!-- 2 | Before opening a PR, open a ticket describing the issue or feature the 3 | PR will address. An issue is not required for fixing typos in 4 | documentation, or other simple non-code changes. 5 | 6 | Replace this comment with a description of the change. Describe how it 7 | addresses the linked ticket. 8 | --> 9 | 10 | <!-- 11 | Link to relevant issues or previous PRs, one per line. Use "fixes" to 12 | automatically close an issue. 13 | 14 | fixes #<issue number> 15 | --> 16 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: Lock inactive closed issues 2 | # Lock closed issues that have not received any further activity for two weeks. 3 | # This does not close open issues, only humans may do that. It is easier to 4 | # respond to new issues with fresh examples rather than continuing discussions 5 | # on old issues. 6 | 7 | on: 8 | schedule: 9 | - cron: '0 0 * * *' 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | discussions: write 14 | concurrency: 15 | group: lock 16 | jobs: 17 | lock: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 21 | with: 22 | issue-inactive-days: 14 23 | pr-inactive-days: 14 24 | discussion-inactive-days: 14 25 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, stable] 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 12 | with: 13 | enable-cache: true 14 | prune-cache: false 15 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 16 | id: setup-python 17 | with: 18 | python-version-file: pyproject.toml 19 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 20 | with: 21 | path: ~/.cache/pre-commit 22 | key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }} 23 | - run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files 24 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 25 | if: ${{ !cancelled() }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: ['*'] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 11 | with: 12 | enable-cache: true 13 | prune-cache: false 14 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 15 | with: 16 | python-version-file: pyproject.toml 17 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 18 | - run: uv build 19 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 20 | with: 21 | path: ./dist 22 | create-release: 23 | needs: [build] 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: write 27 | steps: 28 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 29 | - name: create release 30 | run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} artifact/* 31 | env: 32 | GH_TOKEN: ${{ github.token }} 33 | publish-pypi: 34 | needs: [build] 35 | environment: 36 | name: publish 37 | url: https://pypi.org/project/Quart/${{ github.ref_name }} 38 | runs-on: ubuntu-latest 39 | permissions: 40 | id-token: write 41 | steps: 42 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 43 | - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 44 | with: 45 | packages-dir: artifact/ 46 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | paths-ignore: ['docs/**', 'README.md'] 5 | push: 6 | branches: [main, stable] 7 | paths-ignore: ['docs/**', 'README.md'] 8 | jobs: 9 | tests: 10 | name: ${{ matrix.name || matrix.python }} 11 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - {python: '3.13'} 17 | - {name: Windows, python: '3.13', os: windows-latest} 18 | - {name: Mac, python: '3.13', os: macos-latest} 19 | - {python: '3.12'} 20 | - {python: '3.11'} 21 | - {python: '3.10'} 22 | - {python: '3.9'} 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 26 | with: 27 | enable-cache: true 28 | prune-cache: false 29 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 30 | with: 31 | python-version: ${{ matrix.python }} 32 | - run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} 33 | typing: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 38 | with: 39 | enable-cache: true 40 | prune-cache: false 41 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 42 | with: 43 | python-version-file: pyproject.toml 44 | - name: cache mypy 45 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 46 | with: 47 | path: ./.mypy_cache 48 | key: mypy|${{ hashFiles('pyproject.toml') }} 49 | - run: uv run --locked tox run -e typing 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | __pycache__/ 4 | dist/ 5 | .coverage* 6 | htmlcov/ 7 | .tox/ 8 | .hypothesis/ 9 | docs/reference/source 10 | docs/_build/ 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 9aeda5d1f4bbd212c557da1ea78eca9e8c829e19 # frozen: v0.11.13 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/astral-sh/uv-pre-commit 8 | rev: a621b109bab2e7e832d98c88fd3e83399f4e6657 # frozen: 0.7.12 9 | hooks: 10 | - id: uv-lock 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 13 | hooks: 14 | - id: check-merge-conflict 15 | - id: debug-statements 16 | - id: fix-byte-order-marker 17 | - id: trailing-whitespace 18 | - id: end-of-file-fixer 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-24.04 4 | tools: 5 | python: '3.13' 6 | commands: 7 | - asdf plugin add uv 8 | - asdf install uv latest 9 | - asdf global uv latest 10 | # TODO fix warnings and add -W 11 | - uv run --group docs sphinx-build -b dirhtml docs $READTHEDOCS_OUTPUT/html 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Quick Reference 2 | 3 | This document assumes you have some familiarity with Git, GitHub, and Python 4 | virutalenvs. We are working on a more thorough guide about the different ways to 5 | contribute with in depth explanations, which will be available soon. 6 | 7 | These instructions will work with at least Bash and PowerShell, and should work 8 | on other shells. On Windows, use PowerShell, not CMD. 9 | 10 | You need Python and Git installed, as well as the [GitHub CLI]. Log in with 11 | `gh auth login`. Choose and install an editor; we suggest [PyCharm] or 12 | [VS Code]. 13 | 14 | [GitHub CLI]: https://cli.github.com/ 15 | [PyCharm]: https://www.jetbrains.com/pycharm/ 16 | [VS Code]: https://code.visualstudio.com/ 17 | 18 | ## Set Up the Repository 19 | 20 | Fork and clone the project's repository ("pallets/flask" for example). To work 21 | on a bug or documentation fix, switch to the "stable" branch (if the project has 22 | it), otherwise switch to the "main" branch. To update this target branch, pull 23 | from "upstream". Create a work branch with a short descriptive name. 24 | 25 | ``` 26 | $ gh repo fork --clone pallets/flask 27 | $ cd flask 28 | $ git switch stable 29 | $ git pull upstream 30 | $ git switch -c short-descriptive-name 31 | ``` 32 | 33 | ## Install Development Dependencies 34 | 35 | Create a virtualenv and activate it. Install the dev dependencies, and the 36 | project in editable mode. Install the pre-commit hooks. 37 | 38 | Create a virtualenv (Mac and Linux): 39 | 40 | ``` 41 | $ python3 -m venv .venv 42 | $ . .venv/bin/activate 43 | ``` 44 | 45 | Create a virtualenv (Windows): 46 | 47 | ``` 48 | > py -m venv .venv 49 | > .\.venv\Scripts\activate 50 | ``` 51 | 52 | Install (all platforms): 53 | 54 | ``` 55 | $ pip install -r requirements/dev.txt && pip install -e . 56 | $ pre-commit install --install-hooks 57 | ``` 58 | 59 | Any time you open a new terminal, you need to activate the virtualenv again. If 60 | you've pulled from upstream recently, you can re-run the `pip` command above to 61 | get the current dev dependencies. 62 | 63 | ## Run Tests 64 | 65 | These are the essential test commands you can run while developing: 66 | 67 | * `pytest` - Run the unit tests. 68 | * `mypy` - Run the main type checker. 69 | * `tox run -e docs` - Build the documentation. 70 | 71 | These are some more specific commands if you need them: 72 | 73 | * `tox parallel` - Run all test environments that will be run in CI, in 74 | parallel. Python versions that are not installed are skipped. 75 | * `pre-commit` - Run the linter and formatter tools. Only runs against changed 76 | files that have been staged with `git add -u`. This will run automatically 77 | before each commit. 78 | * `pre-commit run --all-files` - Run the pre-commit hooks against all files, 79 | including unchanged and unstaged. 80 | * `tox run -e py3.11` - Run unit tests with a specific Python version. The 81 | version must be installed. `-e pypy` will run against PyPy. 82 | * `pyright` - A second type checker. 83 | * `tox run -e typing` - Run all typing checks. This includes `pyright` and its 84 | export check as well. 85 | * `python -m http.server -b 127.0.0.1 -d docs/_build/html` - Serve the 86 | documentation. 87 | 88 | ## Create a Pull Request 89 | 90 | Make your changes and commit them. Add tests that demonstrate that your code 91 | works, and ensure all tests pass. Change documentation if needed to reflect your 92 | change. Adding a changelog entry is optional, a maintainer will write one if 93 | you're not sure how to. Add the entry to the end of the relevant section, match 94 | the writing and formatting style of existing entries. Don't add an entry for 95 | changes that only affect documentation or project internals. 96 | 97 | Use the GitHub CLI to start creating your pull request. Specify the target 98 | branch with `-B`. The "stable" branch is the target for bug and documentation 99 | fixes, otherwise the target is "main". 100 | 101 | ``` 102 | $ gh pr create --web --base stable 103 | ``` 104 | 105 | CI will run after you create the PR. If CI fails, you can click to see the logs 106 | and address those failures, pushing new commits. Once you feel your PR is ready, 107 | click the "Ready for review" button. A maintainer will review and merge the PR 108 | when they are available. 109 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2017 Pallets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <div align="center"><img src="https://raw.githubusercontent.com/pallets/quart/refs/heads/main/docs/_static/quart-name.svg" alt="" height="150"></div> 2 | 3 | # Quart 4 | 5 | Quart is an async Python web application framework. Using Quart you can, 6 | 7 | - render and serve HTML templates, 8 | - write (RESTful) JSON APIs, 9 | - serve WebSockets, 10 | - stream request and response data, 11 | - do pretty much anything over the HTTP or WebSocket protocols. 12 | 13 | ## Quickstart 14 | 15 | Install from PyPI using an installer such as pip. 16 | 17 | ``` 18 | $ pip install quart 19 | ``` 20 | 21 | Save the following as `app.py`. This shows off rendering a template, returning 22 | JSON data, and using a WebSocket. 23 | 24 | ```python 25 | from quart import Quart, render_template, websocket 26 | 27 | app = Quart(__name__) 28 | 29 | @app.route("/") 30 | async def hello(): 31 | return await render_template("index.html") 32 | 33 | @app.route("/api") 34 | async def json(): 35 | return {"hello": "world"} 36 | 37 | @app.websocket("/ws") 38 | async def ws(): 39 | while True: 40 | await websocket.send("hello") 41 | await websocket.send_json({"hello": "world"}) 42 | ``` 43 | 44 | ``` 45 | $ quart run 46 | * Running on http://127.0.0.1:5000 (CTRL + C to quit) 47 | ``` 48 | 49 | To deploy this app in a production setting see the [deployment] documentation. 50 | 51 | [deployment]: https://quart.palletsprojects.com/en/latest/tutorials/deployment.html 52 | 53 | ## Contributing 54 | 55 | Quart is developed on [GitHub]. If you come across a bug, or have a feature 56 | request, please open an [issue]. To contribute a fix or implement a feature, 57 | follow our [contributing guide]. 58 | 59 | [GitHub]: https://github.com/pallets/quart 60 | [issue]: https://github.com/pallets/quart/issues 61 | [contributing guide]: https://github.com/pallets/quart/CONTRIBUTING.rst 62 | 63 | ## Help 64 | 65 | If you need help with your code, the Quart [documentation] and [cheatsheet] are 66 | the best places to start. You can ask for help on the [Discussions tab] or on 67 | our [Discord chat]. 68 | 69 | [documentation]: https://quart.palletsprojects.com 70 | [cheatsheet]: https://quart.palletsprojects.com/en/latest/reference/cheatsheet.html 71 | [Discussions tab]: https://github.com/pallets/quart/discussions 72 | [Discord chat]: https://discord.gg 73 | 74 | ## Relationship with Flask 75 | 76 | Quart is an asyncio reimplementation of the popular [Flask] web application 77 | framework. This means that if you understand Flask you understand Quart. 78 | 79 | Like Flask, Quart has an ecosystem of extensions for more specific needs. In 80 | addition, a number of the Flask extensions work with Quart. 81 | 82 | [Flask]: https://flask.palletsprojects.com 83 | 84 | ### Migrating from Flask 85 | 86 | It should be possible to migrate to Quart from Flask by a find and replace of 87 | `flask` to `quart` and then adding `async` and `await` keywords. See the 88 | [migration] documentation for more help. 89 | 90 | [migration]: https://quart.palletsprojects.com/en/latest/how_to_guides/flask_migration.html 91 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = Quart 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/quart-icon.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <rect id="Icon" x="0" y="0" width="500" height="500" style="fill:none;"/> 5 | <clipPath id="_clip1"> 6 | <rect x="0" y="0" width="500" height="500"/> 7 | </clipPath> 8 | <g clip-path="url(#_clip1)"> 9 | <g> 10 | <path d="M415.574,136.117l-50.302,58.634c53.079,70.053 52.154,169.732 -7.407,238.859c-68.509,79.928 -188.864,88.877 -268.484,20.368c-79.928,-68.51 -88.878,-188.557 -20.368,-268.485c54.623,-63.88 142.266,-82.397 216.022,-52.462l54.006,-62.955c1.543,-1.852 4.629,-2.16 6.789,-0.617l69.435,59.56c1.852,2.469 2.161,5.246 0.309,7.098Zm-37.032,181.458c-27.157,-93.506 -122.207,-14.504 -187.013,-54.931c-64.498,-40.427 -33.638,-108.628 -33.638,-108.628c-27.157,9.875 -51.228,26.231 -70.053,48.142c-59.252,69.127 -51.228,173.743 17.899,232.995c69.127,59.252 173.435,51.537 232.995,-17.899c24.997,-29.009 49.685,-65.732 39.81,-99.679Z" style="fill:#2952e1;"/> 11 | <path d="M422.363,121.613l-71.287,-61.104c-5.863,-4.937 -6.172,-13.578 -1.543,-19.442c4.938,-5.863 13.579,-6.172 19.442,-1.543l71.287,61.104c5.864,4.937 6.172,13.578 1.543,19.442c-4.937,5.863 -13.578,6.48 -19.442,1.543Z" style="fill:#2952e1;fill-rule:nonzero;"/> 12 | <path d="M446.126,88.283l-62.647,-53.388c-3.394,-2.777 -3.703,-8.023 -0.925,-11.418l17.59,-20.676c2.777,-3.395 8.024,-3.704 11.418,-0.926l62.647,53.388c3.394,2.777 3.703,8.024 0.925,11.418l-17.59,20.677c-3.086,3.394 -8.024,4.012 -11.418,0.925Z" style="fill:#2952e1;fill-rule:nonzero;"/> 13 | </g> 14 | </g> 15 | </svg> 16 | -------------------------------------------------------------------------------- /docs/_static/quart-logo.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <rect id="Logo" x="0" y="0" width="500" height="500" style="fill:none;"/> 5 | <g> 6 | <path id="Box" d="M500,50l0,400c0,27.596 -22.404,50 -50,50l-400,0c-27.596,0 -50,-22.404 -50,-50l0,-400c0,-27.596 22.404,-50 50,-50l400,0c27.596,0 50,22.404 50,50Z" style="fill:url(#_Linear1);"/> 7 | <path id="Shadow" d="M500,248.633l0,42.217l-169.912,-169.913l37.587,32.033c2.037,1.852 5,1.481 6.851,-0.555l10.555,-12.406c1.475,-1.804 1.499,-4.478 0.071,-6.224l114.848,114.848Zm0,47.586l0,29.441l-189.354,-189.354l42.772,36.662c3.518,2.962 8.703,2.592 11.665,-0.926c2.492,-3.157 2.599,-7.656 0.053,-10.687l134.864,134.864Zm0,32.033l0,121.748c0,27.596 -22.404,50 -50,50l-168.758,-0l-127.613,-127.613c47.771,41.105 119.984,35.736 161.09,-12.221c35.736,-41.476 36.292,-101.283 4.444,-143.315l30.181,-35.181c1.111,-1.111 0.926,-2.777 -0.185,-4.259l150.841,150.841Z" style="fill:#140b5b;"/> 8 | <g id="Icon"> 9 | <path d="M349.344,181.67l-30.181,35.181c31.848,42.032 31.292,101.839 -4.444,143.315c-41.106,47.957 -113.319,53.326 -161.09,12.221c-47.957,-41.106 -53.327,-113.134 -12.221,-161.091c32.774,-38.328 85.359,-49.438 129.613,-31.477l32.403,-37.773c0.926,-1.111 2.778,-1.297 4.074,-0.371l41.661,35.736c1.111,1.482 1.296,3.148 0.185,4.259Z" style="fill:#fff;"/> 10 | <path d="M327.125,290.545c-16.294,-56.104 -73.324,-8.702 -112.208,-32.959c-38.699,-24.256 -20.183,-65.176 -20.183,-65.176c-16.294,5.925 -30.736,15.738 -42.031,28.885c-35.551,41.476 -30.737,104.246 10.739,139.797c41.476,35.551 104.061,30.922 139.797,-10.74c14.998,-17.405 29.811,-39.439 23.886,-59.807Z" style="fill:#140b5b;"/> 11 | <path d="M353.418,172.968l-42.772,-36.662c-3.518,-2.963 -3.704,-8.148 -0.926,-11.666c2.962,-3.518 8.147,-3.703 11.665,-0.925l42.772,36.662c3.518,2.962 3.704,8.147 0.926,11.665c-2.962,3.518 -8.147,3.888 -11.665,0.926Z" style="fill:#fff;fill-rule:nonzero;"/> 12 | <path d="M367.675,152.97l-37.587,-32.033c-2.037,-1.666 -2.222,-4.814 -0.556,-6.851l10.554,-12.406c1.667,-2.036 4.815,-2.222 6.851,-0.555l37.588,32.033c2.037,1.666 2.222,4.814 0.556,6.851l-10.555,12.406c-1.851,2.036 -4.814,2.407 -6.851,0.555Z" style="fill:#fff;fill-rule:nonzero;"/> 13 | </g> 14 | </g> 15 | <defs> 16 | <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.06162e-14,500,-500,3.06162e-14,267.59,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient> 17 | </defs> 18 | </svg> 19 | -------------------------------------------------------------------------------- /docs/_static/quart-name-dark.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 713 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <g> 5 | <path id="Name" d="M434.9,147l-0,6.15c-0,17 -4.25,28.75 -12.75,35.25c5.5,-0.3 10.6,-1.15 15.3,-2.55l-0,18.45l-45,0c-6.2,0 -12.15,-1.025 -17.85,-3.075c-5.7,-2.05 -10.625,-5.15 -14.775,-9.3c-4.15,-4.15 -6.825,-9.125 -8.025,-14.925c-1.2,-5.8 -1.8,-13.85 -1.8,-24.15l-0,-5.7c-0,-10.3 0.6,-18.35 1.8,-24.15c1.2,-5.8 3.875,-10.775 8.025,-14.925c4.15,-4.15 9.075,-7.25 14.775,-9.3c5.7,-2.05 11.65,-3.075 17.85,-3.075c6.2,0 12.125,1.025 17.775,3.075c5.65,2.05 10.6,5.2 14.85,9.45c4.25,4.25 6.95,9.125 8.1,14.625c1.15,5.5 1.725,13.55 1.725,24.15Zm-20.85,5.25l-0,-7.2c-0,-12.5 -1.7,-20.7 -5.1,-24.6c-3.4,-4.1 -8.9,-6.15 -16.5,-6.15c-7.6,0 -13.2,2.1 -16.8,6.3c-2.6,3.1 -4.1,8.5 -4.5,16.2c-0.2,2.5 -0.3,5.95 -0.3,10.35l-0,5.55c0.1,10.6 0.9,17.85 2.4,21.75c2.8,7.5 9.2,11.25 19.2,11.25c8.7,0 14.5,-2.5 17.4,-7.5c1.9,-3.6 3.05,-7.1 3.45,-10.5c0.5,-4.3 0.75,-9.45 0.75,-15.45Z" style="fill:#fff;fill-rule:nonzero;"/> 6 | <path d="M515.6,203.4l-14.55,-0l-4.5,-10.2l-24,11.1c-6.7,-0 -12.525,-2.475 -17.475,-7.425c-4.95,-4.95 -7.425,-12.175 -7.425,-21.675l-0,-57.45l19.5,-0l-0,53.25c-0,9.5 3.8,14.25 11.4,14.25l17.55,-9.45l-0,-58.05l19.5,-0l-0,85.65Z" style="fill:#fff;fill-rule:nonzero;"/> 7 | <path d="M596.45,203.4l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c-0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l-0,86.25Zm-19.5,-27.3l-0,-40.2l-14.85,0c-5.5,0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c-0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill:#fff;fill-rule:nonzero;"/> 8 | <path d="M663.05,118.05l-5.4,19.2c-1.5,-0.6 -3.2,-0.9 -5.1,-0.9c-5,-0 -11.15,2.15 -18.45,6.45l-0,60.6l-19.5,0l-0,-85.65l14.55,0l4.5,7.5l20.7,-8.55c1,-0.1 2.35,-0.15 4.05,-0.15c1.7,0 3.25,0.5 4.65,1.5Z" style="fill:#fff;fill-rule:nonzero;"/> 9 | <path d="M712.7,203.4l-12.15,-0c-7.4,-0 -13.025,-2.2 -16.875,-6.6c-3.85,-4.4 -5.775,-9.65 -5.775,-15.75l-0,-45l-8.25,-0l-0,-14.85l8.25,-0l-0,-16.8l19.5,-0l-0,16.8l13.8,-0l-0,14.85l-13.8,-0l-0,43.8c-0,4.7 2.25,7.05 6.75,7.05l8.55,-0l-0,16.5Z" style="fill:#fff;fill-rule:nonzero;"/> 10 | <g id="Logo"> 11 | <path id="Box" d="M300,30l0,240c0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/> 12 | <path id="Shadow" d="M300,149.18l0,25.33l-101.947,-101.948l22.552,19.22c1.222,1.111 3,0.889 4.111,-0.333l6.332,-7.444c0.886,-1.082 0.9,-2.687 0.043,-3.734l68.909,68.909Zm0,28.552l0,17.664l-113.613,-113.613l25.664,21.998c2.111,1.777 5.221,1.555 6.999,-0.556c1.495,-1.894 1.559,-4.593 0.032,-6.412l80.918,80.919Zm0,19.219l0,73.049c0,16.557 -13.443,30 -30,30l-101.255,-0l-76.568,-76.568c28.663,24.663 71.991,21.442 96.655,-7.332c21.441,-24.886 21.775,-60.77 2.666,-85.989l18.109,-21.109c0.666,-0.666 0.555,-1.666 -0.111,-2.555l90.504,90.504Z" style="fill:#140b5b;"/> 13 | <g id="Icon"> 14 | <path d="M209.607,109.002l-18.109,21.109c19.109,25.219 18.775,61.103 -2.666,85.989c-24.664,28.774 -67.992,31.995 -96.655,7.332c-28.774,-24.664 -31.996,-67.88 -7.332,-96.654c19.664,-22.997 51.215,-29.663 77.768,-18.887l19.442,-22.664c0.555,-0.666 1.666,-0.777 2.444,-0.222l24.997,21.442c0.666,0.889 0.777,1.889 0.111,2.555Z" style="fill:#fff;"/> 15 | <path d="M196.275,174.327c-9.777,-33.662 -43.994,-5.221 -67.325,-19.775c-23.219,-14.554 -12.109,-39.106 -12.109,-39.106c-9.777,3.555 -18.442,9.443 -25.219,17.331c-21.331,24.886 -18.442,62.547 6.443,83.878c24.886,21.331 62.437,18.553 83.879,-6.444c8.998,-10.443 17.886,-23.663 14.331,-35.884Z" style="fill:#140b5b;"/> 16 | <path d="M212.051,103.781l-25.664,-21.998c-2.11,-1.777 -2.222,-4.888 -0.555,-6.999c1.777,-2.111 4.888,-2.222 6.999,-0.555l25.663,21.997c2.111,1.777 2.222,4.888 0.556,6.999c-1.778,2.111 -4.888,2.333 -6.999,0.556Z" style="fill:#fff;fill-rule:nonzero;"/> 17 | <path d="M220.605,91.782l-22.552,-19.22c-1.222,-1 -1.334,-2.888 -0.334,-4.11l6.333,-7.444c1,-1.222 2.888,-1.333 4.11,-0.333l22.553,19.22c1.222,1 1.333,2.888 0.333,4.11l-6.332,7.444c-1.111,1.222 -2.889,1.444 -4.111,0.333Z" style="fill:#fff;fill-rule:nonzero;"/> 18 | </g> 19 | </g> 20 | </g> 21 | <defs> 22 | <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient> 23 | </defs> 24 | </svg> 25 | -------------------------------------------------------------------------------- /docs/_static/quart-name.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 713 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <g> 5 | <path id="Name" d="M434.9,147l0,6.15c0,17 -4.25,28.75 -12.75,35.25c5.5,-0.3 10.6,-1.15 15.3,-2.55l0,18.45l-45,-0c-6.2,-0 -12.15,-1.025 -17.85,-3.075c-5.7,-2.05 -10.625,-5.15 -14.775,-9.3c-4.15,-4.15 -6.825,-9.125 -8.025,-14.925c-1.2,-5.8 -1.8,-13.85 -1.8,-24.15l0,-5.7c0,-10.3 0.6,-18.35 1.8,-24.15c1.2,-5.8 3.875,-10.775 8.025,-14.925c4.15,-4.15 9.075,-7.25 14.775,-9.3c5.7,-2.05 11.65,-3.075 17.85,-3.075c6.2,-0 12.125,1.025 17.775,3.075c5.65,2.05 10.6,5.2 14.85,9.45c4.25,4.25 6.95,9.125 8.1,14.625c1.15,5.5 1.725,13.55 1.725,24.15Zm-20.85,5.25l0,-7.2c0,-12.5 -1.7,-20.7 -5.1,-24.6c-3.4,-4.1 -8.9,-6.15 -16.5,-6.15c-7.6,-0 -13.2,2.1 -16.8,6.3c-2.6,3.1 -4.1,8.5 -4.5,16.2c-0.2,2.5 -0.3,5.95 -0.3,10.35l0,5.55c0.1,10.6 0.9,17.85 2.4,21.75c2.8,7.5 9.2,11.25 19.2,11.25c8.7,-0 14.5,-2.5 17.4,-7.5c1.9,-3.6 3.05,-7.1 3.45,-10.5c0.5,-4.3 0.75,-9.45 0.75,-15.45Z" style="fill-rule:nonzero;"/> 6 | <path d="M515.6,203.4l-14.55,-0l-4.5,-10.2l-24,11.1c-6.7,-0 -12.525,-2.475 -17.475,-7.425c-4.95,-4.95 -7.425,-12.175 -7.425,-21.675l0,-57.45l19.5,-0l0,53.25c0,9.5 3.8,14.25 11.4,14.25l17.55,-9.45l0,-58.05l19.5,-0l0,85.65Z" style="fill-rule:nonzero;"/> 7 | <path d="M596.45,203.4l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l0,86.25Zm-19.5,-27.3l0,-40.2l-14.85,-0c-5.5,-0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill-rule:nonzero;"/> 8 | <path d="M663.05,118.05l-5.4,19.2c-1.5,-0.6 -3.2,-0.9 -5.1,-0.9c-5,-0 -11.15,2.15 -18.45,6.45l0,60.6l-19.5,-0l0,-85.65l14.55,-0l4.5,7.5l20.7,-8.55c1,-0.1 2.35,-0.15 4.05,-0.15c1.7,-0 3.25,0.5 4.65,1.5Z" style="fill-rule:nonzero;"/> 9 | <path d="M712.7,203.4l-12.15,-0c-7.4,-0 -13.025,-2.2 -16.875,-6.6c-3.85,-4.4 -5.775,-9.65 -5.775,-15.75l0,-45l-8.25,-0l0,-14.85l8.25,-0l0,-16.8l19.5,-0l0,16.8l13.8,-0l0,14.85l-13.8,-0l0,43.8c0,4.7 2.25,7.05 6.75,7.05l8.55,-0l0,16.5Z" style="fill-rule:nonzero;"/> 10 | <g id="Logo"> 11 | <path id="Box" d="M300,30l0,240c0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/> 12 | <path id="Shadow" d="M300,149.18l0,25.33l-101.947,-101.948l22.552,19.22c1.222,1.111 3,0.889 4.111,-0.333l6.332,-7.444c0.886,-1.082 0.9,-2.687 0.043,-3.734l68.909,68.909Zm0,28.552l0,17.664l-113.613,-113.613l25.664,21.998c2.111,1.777 5.221,1.555 6.999,-0.556c1.495,-1.894 1.559,-4.593 0.032,-6.412l80.918,80.919Zm0,19.219l0,73.049c0,16.557 -13.443,30 -30,30l-101.255,-0l-76.568,-76.568c28.663,24.663 71.991,21.442 96.655,-7.332c21.441,-24.886 21.775,-60.77 2.666,-85.989l18.109,-21.109c0.666,-0.666 0.555,-1.666 -0.111,-2.555l90.504,90.504Z" style="fill:#140b5b;"/> 13 | <g id="Icon"> 14 | <path d="M209.607,109.002l-18.109,21.109c19.109,25.219 18.775,61.103 -2.666,85.989c-24.664,28.774 -67.992,31.995 -96.655,7.332c-28.774,-24.664 -31.996,-67.88 -7.332,-96.654c19.664,-22.997 51.215,-29.663 77.768,-18.887l19.442,-22.664c0.555,-0.666 1.666,-0.777 2.444,-0.222l24.997,21.442c0.666,0.889 0.777,1.889 0.111,2.555Z" style="fill:#fff;"/> 15 | <path d="M196.275,174.327c-9.777,-33.662 -43.994,-5.221 -67.325,-19.775c-23.219,-14.554 -12.109,-39.106 -12.109,-39.106c-9.777,3.555 -18.442,9.443 -25.219,17.331c-21.331,24.886 -18.442,62.547 6.443,83.878c24.886,21.331 62.437,18.553 83.879,-6.444c8.998,-10.443 17.886,-23.663 14.331,-35.884Z" style="fill:#140b5b;"/> 16 | <path d="M212.051,103.781l-25.664,-21.998c-2.11,-1.777 -2.222,-4.888 -0.555,-6.999c1.777,-2.111 4.888,-2.222 6.999,-0.555l25.663,21.997c2.111,1.777 2.222,4.888 0.556,6.999c-1.778,2.111 -4.888,2.333 -6.999,0.556Z" style="fill:#fff;fill-rule:nonzero;"/> 17 | <path d="M220.605,91.782l-22.552,-19.22c-1.222,-1 -1.334,-2.888 -0.334,-4.11l6.333,-7.444c1,-1.222 2.888,-1.333 4.11,-0.333l22.553,19.22c1.222,1 1.333,2.888 0.333,4.11l-6.332,7.444c-1.111,1.222 -2.889,1.444 -4.111,0.333Z" style="fill:#fff;fill-rule:nonzero;"/> 18 | </g> 19 | </g> 20 | </g> 21 | <defs> 22 | <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient> 23 | </defs> 24 | </svg> 25 | -------------------------------------------------------------------------------- /docs/changes.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ```{include} ../CHANGES.md 4 | ``` 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import os 3 | 4 | from sphinx.ext import apidoc 5 | 6 | # Project -------------------------------------------------------------- 7 | 8 | project = "Quart" 9 | copyright = "2017 Pallets" 10 | version = release = importlib.metadata.version("quart").partition(".dev")[0] 11 | 12 | # General -------------------------------------------------------------- 13 | 14 | default_role = "code" 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx.ext.napoleon", 18 | "myst_parser", 19 | ] 20 | autodoc_member_order = "bysource" 21 | autodoc_typehints = "description" 22 | autodoc_preserve_defaults = True 23 | myst_enable_extensions = [ 24 | "fieldlist", 25 | ] 26 | myst_heading_anchors = 2 27 | 28 | # HTML ----------------------------------------------------------------- 29 | 30 | html_theme = "pydata_sphinx_theme" 31 | html_theme_options = { 32 | "logo": {"text": "Quart"}, 33 | "external_links": [ 34 | {"name": "Source code", "url": "https://github.com/pallets/quart"}, 35 | {"name": "Issues", "url": "https://github.com/pallets/quart/issues"}, 36 | ], 37 | "icon_links": [ 38 | { 39 | "name": "Github", 40 | "url": "https://github.com/pallets/quart", 41 | "icon": "fab fa-github", 42 | }, 43 | ], 44 | } 45 | html_static_path = ["_static"] 46 | html_favicon = "_static/quart-icon.svg" 47 | html_logo = "_static/quart-logo.svg" 48 | 49 | 50 | def run_apidoc(_): 51 | # generate API documentation via sphinx-apidoc 52 | # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html 53 | base_path = os.path.abspath(os.path.dirname(__file__)) 54 | apidoc.main( 55 | [ 56 | "-f", 57 | "-e", 58 | "-o", 59 | f"{base_path}/reference/source", 60 | f"{base_path}/../src/quart", 61 | f"{base_path}/../src/quart/datastructures.py", 62 | ] 63 | ) 64 | 65 | 66 | def setup(app): 67 | app.connect("builder-inited", run_apidoc) 68 | -------------------------------------------------------------------------------- /docs/discussion/async_compatibility.rst: -------------------------------------------------------------------------------- 1 | .. _async_compatibility: 2 | 3 | Async compatibility 4 | =================== 5 | 6 | Synchronous and asynchronous code are not directly compatible in that 7 | the functions must be called differently depending on the type. This 8 | limits what can be done, for example in how Quart interacts with Flask 9 | extensions and any effort to make Flask directly asynchronous. 10 | 11 | In my opinion it is much easier to start with an asynchronous codebase 12 | that calls synchronous code than vice versa in Python. I will try and 13 | reason why below. 14 | 15 | Calling sync code from async functions 16 | -------------------------------------- 17 | 18 | This is mostly easy in that you can either call, or via a simple wrapper 19 | await a synchronous function, 20 | 21 | .. code-block:: python 22 | 23 | async def example(): 24 | sync_call() 25 | await asyncio.coroutine(sync_call)() 26 | 27 | whilst this doesn't actually change the nature, the call is 28 | synchronous, it does work. 29 | 30 | Calling async code from sync functions 31 | -------------------------------------- 32 | 33 | This is where things get difficult, as it is only possible to create a 34 | single event loop. Hence this can only be used once, 35 | 36 | .. code-block:: python 37 | 38 | def example(): 39 | loop = asyncio.get_event_loop() 40 | loop.run_until_complete(async_call()) 41 | 42 | therefore if you are not at the very outer scope it isn't really 43 | possible to call asynchronous code from a synchronous function. 44 | 45 | This is problematic when dealing with Flask extensions as for example the 46 | extension may have something like, 47 | 48 | .. code-block:: python 49 | 50 | @app.route('/') 51 | def route(): 52 | data = request.form 53 | return render_template_string("{{ name }}", name=data['name']) 54 | 55 | whilst the route function can be wrapped with the 56 | ``asyncio.coroutine`` function and hence awaited, there is no (easy?) 57 | way to insert the ``await`` before the ``request.form`` and 58 | ``render_template`` calls. 59 | 60 | It is for this reason that Quart-Flask-Patch creates sync wrapped 61 | versions for the Flask extensions. The former adding synchronous 62 | request methods and the other providing synchronous functions. 63 | 64 | Quart monkey patches a ``sync_wait`` method onto the base event loop 65 | allowing for definitions such as, 66 | 67 | .. code-block:: python 68 | 69 | from quart.templating import render_template as quart_render_template 70 | 71 | def render_template(*args): 72 | return asyncio.sync_wait(quart_render_template(*args)) 73 | -------------------------------------------------------------------------------- /docs/discussion/background_tasks.rst: -------------------------------------------------------------------------------- 1 | .. _background_task_discussion: 2 | 3 | Background tasks 4 | ================ 5 | 6 | The API for background tasks follows Sanic and Starlette by taking a 7 | callable and arguments. However, Quart will ensure that the tasks 8 | finish during the shutdown (unless the server times out and 9 | cancels). This is as you'd hope in a production environment. 10 | 11 | Errors raised in a background task are logged but otherwise ignored 12 | allowing the app to continue - much like with request/websocket 13 | handling errors. 14 | -------------------------------------------------------------------------------- /docs/discussion/contexts.rst: -------------------------------------------------------------------------------- 1 | .. _contexts: 2 | 3 | Contexts 4 | ======== 5 | 6 | Quart, like Flask, has two contexts the *application context* and the 7 | *request context*. Both of these contexts exist per request and allow 8 | the global proxies ``current_app``, ``request``, etc... to be resolved. 9 | Note that these contexts are task local, and hence will not exist if a 10 | task is spawned by ``ensure_future`` or ``create_task``. 11 | 12 | The design principle of these contexts is that they are likely needed 13 | in all routes, and hence rather than pass these objects around they 14 | are made available via global proxies. This has its downsides, notably 15 | all the arguments relating to global variables. Therefore, it is 16 | recommended that these proxies are only used within routes so as to 17 | isolate the scope. 18 | 19 | Application Context 20 | ------------------- 21 | 22 | The application context is a reference point for any information that 23 | isn't specifically related to a request. This includes the app itself, 24 | the ``g`` global object and a ``url_adapter`` bound only to the app. The 25 | context is created and destroyed implicitly by the request context. 26 | 27 | Request Context 28 | --------------- 29 | 30 | The request context is a reference point for any information that is 31 | related to a request. This includes the request itself, a ``url_adapter`` 32 | bound to the request and the session. It is created and destroyed by 33 | the :func:`~quart.Quart.handle_request` method per request. 34 | 35 | Websocket Context 36 | ----------------- 37 | 38 | The websocket context is analogous to the request context, but is 39 | related only to websocket requests. It is created and destroyed by the 40 | :func:`~quart.Quart.handle_websocket_request` method per websocket 41 | connection. 42 | 43 | Tasks and contexts 44 | ------------------ 45 | 46 | Context is bound to a ContextVar and will be copied to tasks created 47 | from an existing task. To explicitly copy a context Quart provides the 48 | decorators :func:`~quart.ctx.copy_current_request_context` and 49 | :func:`copy_current_websocket_context` which can be used as so, 50 | 51 | .. code-block:: python 52 | 53 | @app.route('/') 54 | async def index(): 55 | 56 | @copy_current_request_context 57 | async def background_task(): 58 | method = request.method 59 | ... 60 | 61 | asyncio.ensure_future(background_task()) 62 | ... 63 | 64 | If you need to provide the ``request`` context in an asynchronous 65 | generator, use the :func:`quart.helpers.stream_with_context` decorator 66 | as discussed in :ref:`streaming_response`: 67 | 68 | .. code-block:: python 69 | 70 | @app.route('/') 71 | async def index(): 72 | 73 | @stream_with_context 74 | async def async_generator(): 75 | async for data in request.body: 76 | yield data 77 | 78 | await consume_data(async_generator()) 79 | ... 80 | 81 | .. note:: These decorators must be used within an existing context, hence 82 | the background task is defined as a nested function. 83 | -------------------------------------------------------------------------------- /docs/discussion/design_choices.rst: -------------------------------------------------------------------------------- 1 | .. _design_choices: 2 | 3 | Design Choices 4 | ============== 5 | 6 | Coroutines or functions 7 | ----------------------- 8 | 9 | It is quite easy to call sync and trigger async execution from a 10 | coroutine and hard to trigger async execution from a function, see 11 | :ref:`async_compatibility`. For this reason coroutines are preferred even in 12 | cases where IO seems unlikely. 13 | -------------------------------------------------------------------------------- /docs/discussion/flask_evolution.rst: -------------------------------------------------------------------------------- 1 | .. _flask_evolution: 2 | 3 | Flask evolution 4 | =============== 5 | 6 | (The author) sees Quart as an evolution of Flask, primarily to support 7 | asyncio and secondarily to support websockets and HTTP/2. These 8 | additions are designed following (the author's interpretation) of 9 | Flask's design choices. It is for this reason that the websocket 10 | context and global exist, rather than as an argument to the route 11 | handler. 12 | 13 | Omissions from the Flask API 14 | ---------------------------- 15 | 16 | There are parts of the Flask API that I've decided to either not 17 | implement, these are, 18 | 19 | request.stream 20 | ^^^^^^^^^^^^^^ 21 | 22 | The ``stream`` method present on Flask request instances allows the 23 | request body to be 'streamed' via a file like interface. In Quart 24 | :ref:`request_body` is done differently in order to make use of the 25 | ``async`` keyword. 26 | -------------------------------------------------------------------------------- /docs/discussion/globals.rst: -------------------------------------------------------------------------------- 1 | .. _globals: 2 | 3 | Globals 4 | ======= 5 | 6 | As Quart follows the Flask API, it also has globals, specifically 7 | ``current_app, g, request, session``. These are globals as they are 8 | likely needed in all request handlers. 9 | 10 | Locals 11 | ------ 12 | 13 | As is rather confusing from a naming point of view, the globals are 14 | local to the task being executed. This is detailed from a contextual 15 | view in :ref:`contexts`. This is necessary to ensure that Quart can 16 | asynchronously handle many requests with a single global object. 17 | -------------------------------------------------------------------------------- /docs/discussion/index.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Discussions 3 | =========== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | async_compatibility.rst 9 | background_tasks.rst 10 | contexts.rst 11 | design_choices.rst 12 | dos_mitigations.rst 13 | flask_evolution.rst 14 | globals.rst 15 | python_versions.rst 16 | websockets_discussion.rst 17 | -------------------------------------------------------------------------------- /docs/discussion/python_versions.rst: -------------------------------------------------------------------------------- 1 | .. _python_versions: 2 | 3 | Python version support 4 | ====================== 5 | 6 | The main branch and releases >= 0.20.0 onwards only support Python 7 | 3.9.0 or greater. 8 | -------------------------------------------------------------------------------- /docs/discussion/websockets_discussion.rst: -------------------------------------------------------------------------------- 1 | .. _websockets_discussion: 2 | 3 | Websockets 4 | ========== 5 | 6 | Websockets start as a GET request that can either be upgraded to a 7 | websocket via a 101, switching protocols, response or any other 8 | response. The choice of what to do, or how to respond, is often not 9 | possible in other frameworks and is one of the motivating aims in 10 | Quart. In addition as websockets are very similar to requests, Quart 11 | aims to have analogues functionality between websockets and requests. 12 | 13 | Request analogues 14 | ----------------- 15 | 16 | Websockets are very similar to GET requests, to the extent that is was 17 | tempting to simply extend the Flask request API to include websocket 18 | functionality. This would likely cause surprise to users of 19 | Flask-Sockets or Flask-SocketIO which set the de-facto Flask 20 | standard. Therefore I decided to introduce the websocket functionality 21 | alongside the existing request functionality. 22 | 23 | As websockets are so similar to GET requests it makes sense to produce 24 | an analogue for all the functionality available for requests. For 25 | example. :meth:`~quart.app.Quart.before_request` and 26 | :meth:`~quart.app.Quart.before_websocket` and there is a *websocket 27 | context* alongside the *request context*. 28 | 29 | Response or Upgrade 30 | ------------------- 31 | 32 | The utility of being able to choose how to respond, or whether to 33 | upgrade, is best shown when considering authentication. In the example 34 | below a typical login_required decorator can be used to prevent 35 | unauthorised usage of the websocket. 36 | 37 | .. code-block:: python 38 | 39 | def login_required(func): 40 | @wraps(func) 41 | async def wrapper(*args, **kwargs): 42 | if websocket.authentication == (...): 43 | return await func(*args, **kwargs) 44 | else: 45 | abort(401) 46 | return wrapper 47 | 48 | @app.websocket('/ws') 49 | @login_required 50 | async def ws(): 51 | while True: 52 | await websocket.receive() 53 | ... 54 | 55 | Quart also allows for the acceptance response (101) to be manually 56 | sent via :meth:`~quart.wrappers.Websocket.accept` as this gives the 57 | framework user full control. 58 | 59 | .. note:: 60 | 61 | This functionality is only useable with ASGI servers that 62 | implement the ``Websocket Denial Response`` extension. If the 63 | server does not support this extension Quart will instruct the 64 | server to close the connection without a response. Hypercorn, the 65 | recommended ASGI server, supports this extension. 66 | -------------------------------------------------------------------------------- /docs/how_to_guides/background_tasks.rst: -------------------------------------------------------------------------------- 1 | .. _background_tasks: 2 | 3 | Background tasks 4 | ================ 5 | 6 | If you have a task to perform where the outcome or result isn't 7 | required you can utilise a background task to run it. Background tasks 8 | run concurrently with the route handlers etc, i.e. in the 9 | background. Background tasks are very useful when they contain actions 10 | that take a lot of time to complete, as they allow a response to be 11 | sent to the client whilst the task itself is carried out. Equally some 12 | tasks just don't need to be completed before the response is sent and 13 | instead can be done in the background. 14 | 15 | Background tasks in Quart are created via the ``add_background_task`` 16 | method: 17 | 18 | .. code-block:: python 19 | 20 | async def background_task(): 21 | ... 22 | 23 | @app.route('/jobs/', methods=['POST']) 24 | async def create_job(): 25 | app.add_background_task(background_task) 26 | return 'Success' 27 | 28 | @app.before_serving 29 | async def startup(): 30 | app.add_background_task(background_task) 31 | 32 | 33 | The background tasks will have access to the app context. The tasks 34 | will be awaited during shutdown to ensure they complete before the app 35 | shuts down. If your task does not complete within the config 36 | ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` it will be cancelled. 37 | 38 | Note ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` should ideally be less than 39 | any server shutdown timeout. 40 | 41 | Synchronous background tasks are supported and will run in a separate 42 | thread. 43 | 44 | .. warning:: 45 | 46 | As Quart is based on asyncio it will run on a single execution and 47 | switch between tasks as they become blocked on waiting on IO, if a 48 | task does not need to wait on IO it will instead block the event 49 | loop and Quart could become unresponsive. Additionally the task 50 | will consume the same CPU resources as the server and hence could 51 | slow the server. 52 | 53 | 54 | Testing background tasks 55 | ------------------------ 56 | 57 | To ensure that background tasks complete in tests utilise the 58 | ``test_app`` context manager. This will wait for any background 59 | tasks to complete before allowing the test to continue: 60 | 61 | .. code-block:: python 62 | 63 | async def test_tasks_complete(): 64 | async with app.test_app(): 65 | app.add_background_task(...) 66 | # Background task has completed here 67 | assert task_has_done_something 68 | 69 | Note when testing an app the ``test_client`` usage should be within 70 | the ``test_app`` context block. 71 | 72 | The background task coroutine function can be tested by creating an 73 | app context and await the function, 74 | 75 | .. code-block:: python 76 | 77 | async def test_background_task(): 78 | async with app.app_context(): 79 | await background_task() 80 | assert something_to_test 81 | -------------------------------------------------------------------------------- /docs/how_to_guides/blueprints.rst: -------------------------------------------------------------------------------- 1 | .. _blueprints: 2 | 3 | Blueprints 4 | ========== 5 | 6 | Blueprints allow for modular code, they should be used whenever the 7 | routes start to span multiple modules. Blueprints, like the app can have 8 | template and static files, therefore a typical folder structure for a 9 | blueprint termed ``store`` would be, 10 | 11 | :: 12 | 13 | blueprints/ 14 | blueprints/store/__init__.py 15 | blueprints/store/templates/ 16 | blueprints/store/templates/index.html 17 | blueprints/store/static/ 18 | 19 | the ``__init__.py`` file should contain something like, 20 | 21 | .. code-block:: python 22 | 23 | from quart import Blueprint 24 | 25 | blueprint = Blueprint('store', __name__) 26 | 27 | @blueprint.route('/') 28 | async def index(): 29 | return await render_template('index.html') 30 | 31 | 32 | the endpoint is then identified as ``store.index`` for example when 33 | using ``url_for('store.index')``. 34 | 35 | Nested Blueprints 36 | ----------------- 37 | 38 | It is possible to register a blueprint on another blueprint. 39 | 40 | .. code-block:: python 41 | 42 | parent = Blueprint("parent", __name__, url_prefix="/parent") 43 | child = Blueprint("child", __name__, url_prefix="/child") 44 | parent.register_blueprint(child) 45 | app.register_blueprint(parent) 46 | 47 | The child blueprint will gain the parent's name as a prefix to its 48 | name, and child URLs will be prefixed with the parent's URL prefix. 49 | 50 | .. code-block:: python 51 | 52 | url_for('parent.child.create') 53 | /parent/child/create 54 | 55 | Blueprint-specific before request functions, etc. registered with the 56 | parent will trigger for the child. If a child does not have an error 57 | handler that can handle a given exception, the parent's will be tried. 58 | -------------------------------------------------------------------------------- /docs/how_to_guides/command_line.rst: -------------------------------------------------------------------------------- 1 | .. _command_line: 2 | 3 | Custom Command Line Commands 4 | ============================ 5 | 6 | The ``quart`` command can be customised by the app to add additional 7 | functionality. A very typical use case is to add a database 8 | initialisation command, 9 | 10 | .. code-block:: python 11 | 12 | import click 13 | 14 | @app.cli.command() 15 | def initdb(): 16 | click.echo('Database is migrating') 17 | ... 18 | 19 | which will then work as, 20 | 21 | .. code-block:: console 22 | 23 | $ quart initdb 24 | Database is migrating 25 | 26 | .. note:: 27 | 28 | Unlike Flask the Quart commands do not run within an app context, 29 | as click commands are synchronous rather than asynchronous. 30 | 31 | Asynchronous usage 32 | ------------------ 33 | 34 | The best way to use some asynchronous code in a custom command is to 35 | create an event loop and run it manually, for example, 36 | 37 | .. code-block:: python 38 | 39 | import asyncio 40 | 41 | @app.cli.command() 42 | def fetch_db_data(): 43 | result = asyncio.get_event_loop().run_until_complete(_fetch()) 44 | 45 | 46 | async def _fetch(): 47 | return await db.execute(...) 48 | 49 | Including a CLI Command in an extension or another module 50 | --------------------------------------------------------- 51 | 52 | To include CLI commands in a Quart extension or blueprint, register the methods in the "run" factory function 53 | 54 | .. code-block:: python 55 | 56 | from quart import Quart 57 | from my_extension import my_cli 58 | 59 | def create_app(): 60 | app = Quart(__name__) 61 | app = my_cli(app) 62 | return app 63 | 64 | And in your module or extension: 65 | 66 | .. code-block:: python 67 | 68 | import click 69 | 70 | def my_cli(app): 71 | # @click.option("--my-option") 72 | @app.cli.command("mycli") 73 | def my_cli_command(): 74 | print("quart ran this command") 75 | 76 | return app 77 | 78 | This can be run with: 79 | 80 | .. code-block:: console 81 | 82 | $ quart mycli 83 | $ quart ran this command 84 | -------------------------------------------------------------------------------- /docs/how_to_guides/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuration 4 | ============= 5 | 6 | A common pattern is to store configuration values in the 7 | environment. Quart supports this via 8 | :meth:`~quart.Config.from_prefixed_env` which can be used to load 9 | environment variables into the configuration. Only environment 10 | variables starting with the prefix, default ``QUART_`` will be 11 | loaded. For example if the environment variable ``QUART_TESTING=true`` 12 | is set then, 13 | 14 | .. code-block:: python 15 | 16 | app = Quart(__name__) 17 | app.config.from_prefixed_env() 18 | assert app.config["TESTING"] is True 19 | 20 | Another common pattern for configuration loading is to use class 21 | inheritance to define common settings with production and development 22 | overrides, for example, 23 | 24 | .. code-block:: python 25 | 26 | class Config: 27 | DEBUG = False 28 | TESTING = False 29 | SECRET_KEY = 'secret' 30 | 31 | class Development(Config): 32 | DEBUG = True 33 | 34 | class Production(Config): 35 | SECRET_KEY = 'an actually secret key' 36 | 37 | This can then be loaded in say a ``create_app`` function, for example: 38 | 39 | .. code-block:: python 40 | 41 | def create_app(mode='Development'): 42 | """In production create as app = create_app('Production')""" 43 | app = Quart(__name__) 44 | app.config.from_object(f"config.{mode}") 45 | return app 46 | 47 | Custom configuration class 48 | -------------------------- 49 | 50 | The :attr:`~quart.Quart.config_class` can be set to a custom class, 51 | however it must be changed before the app is initialised as the 52 | :meth:`~quart.Quart.make_config` is called on construction. 53 | 54 | Instance folders 55 | ---------------- 56 | 57 | An instance folder is a deployment specific location to store files 58 | and configuration settings. As opposed to loading files relative to 59 | the app root path :meth:`~quart.Quart.open_resource` you can load 60 | files relative to an instance path 61 | :meth:`~quart.Quart.open_instance_resource` including the 62 | configuration. To load the configuration from this folder, instead of 63 | relative to the app root path simply provide the 64 | ``instance_relative_config`` argument as ``True`` when initialising 65 | the app ``app = Quart(__name__, instance_relative_config=True)``. 66 | 67 | The instance path can be specified when initialising the app, or found 68 | automatically if it exists. The search locations are:: 69 | 70 | /app.py 71 | /instance/ 72 | 73 | or if the app has been installed:: 74 | 75 | $PREFIX/var/app-instance/ 76 | -------------------------------------------------------------------------------- /docs/how_to_guides/developing.rst: -------------------------------------------------------------------------------- 1 | .. _developing: 2 | 3 | Developing with Quart 4 | ===================== 5 | 6 | When developing it is best to have your Quart app running so you can 7 | test any changes you make directly. This is made easier by the 8 | reloader which reloads your app whenever a file is changed. The 9 | reloader is active if you use ``app.run()``, ``quart run`` command or 10 | ``run_task`` method. 11 | 12 | 13 | Quart run 14 | --------- 15 | 16 | The ``quart run`` command is the recommended way to develop with Quart 17 | and will run whichever app is specified by the ``QUART_APP`` 18 | environment variable. For example, 19 | 20 | .. code-block:: python 21 | :caption: run.py 22 | 23 | from quart import Quart 24 | 25 | app = Quart(__name__) 26 | 27 | ... 28 | 29 | .. code-block:: console 30 | 31 | $ QUART_APP=run:app quart run 32 | 33 | The ``quart run`` command comes with ``--host``, and ``--port`` to 34 | specify where the app is served, and ``--certfile`` and ``--keyfile`` 35 | to specify the SSL certificates to use. 36 | 37 | app.run() 38 | --------- 39 | 40 | The Quart class, instances typically named ``app``, has a 41 | :meth:`~quart.Quart.run` method. This method runs a development server, 42 | automatically turning on debug mode and code reloading. This can be 43 | used to run the app via this snippet, 44 | 45 | .. code-block:: python 46 | :caption: run.py 47 | 48 | from quart import Quart 49 | 50 | app = Quart(__name__) 51 | 52 | ... 53 | 54 | if __name__ == "__main__": 55 | app.run() 56 | 57 | with the ``if`` ensuring that this code only runs if the file is run 58 | directly, i.e. 59 | 60 | .. code-block:: console 61 | 62 | $ python run.py 63 | 64 | which ensures that it doesn't run in production. 65 | 66 | The :meth:`~quart.Quart.run` method has options to set the ``host``, 67 | and ``port`` the app will be served over, to turn off the reloader via 68 | ``use_reloader=False``, and to add specify SSL certificates via the 69 | ``certfile`` and ``keyfile`` options. 70 | 71 | .. note:: 72 | 73 | The :meth:`~quart.Quart.run` method will create a new event loop, 74 | use ``run_task`` instead if you wish to control the event loop. 75 | 76 | app.run_task 77 | ------------ 78 | 79 | The Quart class also has a :meth:`~quart.Quart.run_task` method with 80 | the same options as the :meth:`~quart.Quart.run` method. The 81 | ``run_task`` returns an asyncio task that when awaited will run the 82 | app. This is as useful as it makes no alterations to the event 83 | loop. The ``run_task`` can be used as so, 84 | 85 | .. code-block:: python 86 | :caption: run.py 87 | 88 | import asyncio 89 | 90 | from quart import Quart 91 | 92 | app = Quart(__name__) 93 | 94 | ... 95 | 96 | if __name__ == "__main__": 97 | asyncio.run(app.run_task()) 98 | 99 | with the ``if`` ensuring that this code only runs if the file is run 100 | directly, i.e. 101 | 102 | .. code-block:: console 103 | 104 | $ python run.py 105 | 106 | which ensures that it doesn't run in production. 107 | 108 | 109 | Curl 110 | ---- 111 | 112 | To test the app locally I like to use a web browser, and the curl 113 | command line tool. I'd recommend reading the curl `documentation 114 | <https://curl.se/docs/>`_ and always using the ``-v``, ``--verbose`` 115 | option. For example, 116 | 117 | .. code-block:: console 118 | 119 | $ curl -v localhost:5000/ 120 | -------------------------------------------------------------------------------- /docs/how_to_guides/disconnections.rst: -------------------------------------------------------------------------------- 1 | .. _detecting_disconnection: 2 | 3 | Detecting disconnection 4 | ======================= 5 | 6 | If in your route or websocket handler (or code called from within it) 7 | you are awaiting and the client disconnects the await will raise a 8 | ``CancelledError``. This can be used to detect when a client 9 | disconnects, to allow for cleanup, for example the sse handler from 10 | the :ref:`broadcast_tutorial` uses this to remove clients on 11 | disconnect, 12 | 13 | .. code-block:: python 14 | 15 | @app.route('/sse') 16 | async def sse(): 17 | queue = asyncio.Queue() 18 | app.clients.add(queue) 19 | async def send_events(): 20 | while True: 21 | try: 22 | data = await queue.get() 23 | event = ServerSentEvent(data) 24 | yield event.encode() 25 | except asyncio.CancelledError: 26 | app.clients.remove(queue) 27 | 28 | or with only the relevant parts, 29 | 30 | .. code-block:: python 31 | 32 | @app.route('/sse') 33 | async def sse(): 34 | try: 35 | await ... 36 | except asyncio.CancelledError: 37 | # Has disconnected 38 | 39 | The same applies for WebSockets, streaming the request, etc... 40 | -------------------------------------------------------------------------------- /docs/how_to_guides/event_loop.rst: -------------------------------------------------------------------------------- 1 | .. _event_loop: 2 | 3 | Customise the Event Loop 4 | ======================== 5 | 6 | Customising the event loop is often desired in order to use Quart with 7 | another library whilst ensuring both use the same loop. The best practice is 8 | to create/initialise the third party within the loop created by Quart, 9 | by using :ref:`startup_shutdown` ``before_serving`` functions as so, 10 | 11 | .. code-block:: python 12 | 13 | @app.before_serving 14 | async def startup(): 15 | loop = asyncio.get_event_loop() 16 | app.smtp_server = loop.create_server(aiosmtpd.smtp.SMTP, port=1025) 17 | loop.create_task(app.smtp_server) 18 | 19 | @app.after_serving 20 | async def shutdown(): 21 | app.smtp_server.close() 22 | 23 | Do not follow this pattern, typically seen in examples, because this creates a 24 | new loop separate from the Quart loop for ThirdParty, 25 | 26 | .. code-block:: python 27 | 28 | loop = asyncio.get_event_loop() 29 | third_party = ThirdParty(loop) 30 | app.run() # A new loop is created by default 31 | 32 | Controlling the event loop 33 | -------------------------- 34 | 35 | It is the ASGI server running running Quart that owns the event loop 36 | that Quart runs within, by default the server is Hypercorn. Both Quart 37 | and Hypercorn allow the loop to be specified, the Quart shortcut in 38 | development is to pass the loop to the ``app.run`` method, 39 | 40 | .. code-block:: python 41 | 42 | loop = asyncio.get_event_loop() 43 | third_party = ThirdParty(loop) 44 | app.run(loop=loop) 45 | 46 | or to use the ``app.run_task`` method, 47 | 48 | .. code-block:: python 49 | 50 | loop = asyncio.get_event_loop() 51 | third_party = ThirdParty(loop) 52 | loop.run_until_complete(app.run_task()) 53 | 54 | the Hypercorn (production) solution is to utilise the `Hypercorn API 55 | <https://hypercorn.readthedocs.io/en/latest/how_to_guides/api_usage.html>`_ to do the 56 | following, 57 | 58 | .. code-block:: python 59 | 60 | from hypercorn.asyncio import serve 61 | from hypercorn.config import Config 62 | ... 63 | loop = asyncio.get_event_loop() 64 | third_party = ThirdParty(loop) 65 | loop.run_until_complete(serve(app, Config())) 66 | # or even 67 | await serve(app, config) 68 | -------------------------------------------------------------------------------- /docs/how_to_guides/flask_extensions.rst: -------------------------------------------------------------------------------- 1 | .. _flask_extensions: 2 | 3 | Using Flask Extensions 4 | ====================== 5 | 6 | Some Flask extensions can be used with Quart by patching Quart to act 7 | as Flask, to patch Quart see the `Quart-Flask-Patch 8 | <https://github.com/pgjones/quart-flask-patch>`_ extension. This was 9 | part of Quart until release 0.19.0. 10 | 11 | Reference 12 | --------- 13 | 14 | More information about Flask extensions can be found 15 | `here <https://flask.palletsprojects.com/extensions>`_. 16 | -------------------------------------------------------------------------------- /docs/how_to_guides/flask_migration.rst: -------------------------------------------------------------------------------- 1 | .. _flask_migration: 2 | 3 | Migration from Flask 4 | ==================== 5 | 6 | As Quart is compatible with the Flask public API it should be 7 | relatively straight forward to migrate to Quart from Flask. This 8 | migration basically consists of two steps, firstly replacing Flask 9 | imports with Quart imports and secondly inserting the relevant 10 | ``async`` and ``await`` keywords. 11 | 12 | Import changes 13 | -------------- 14 | 15 | Any import of a module from the flask package can be changed to be an 16 | import from the same module in the quart package. For example the 17 | following in Flask, 18 | 19 | .. code-block:: python 20 | 21 | from flask import Flask, g, request 22 | from flask.helpers import make_response 23 | 24 | becomes in Quart, 25 | 26 | .. code-block:: python 27 | 28 | from quart import Quart, g, request 29 | from quart.helpers import make_response 30 | 31 | noting that the imported objects have the same name in both packages 32 | except for the ``Quart`` and ``Flask`` classes themselves. 33 | 34 | This can largely be automated via the use of find and replace. 35 | 36 | Async and Await 37 | --------------- 38 | 39 | As Quart is an asynchronous framework based on asyncio, it is 40 | necessary to explicitly add ``async`` and ``await`` keywords. The most 41 | notable place in which to do this is route functions, for example the 42 | following in Flask, 43 | 44 | .. code-block:: python 45 | 46 | @app.route('/') 47 | def route(): 48 | data = request.get_json() 49 | return render_template_string("Hello {{name}}", name=data['name']) 50 | 51 | becomes in Quart, 52 | 53 | .. code-block:: python 54 | 55 | @app.route('/') 56 | async def route(): 57 | data = await request.get_json() 58 | return await render_template_string("Hello {{name}}", name=data['name']) 59 | 60 | If you have sufficient test coverage it is possible to search for 61 | awaitables by searching for ``RuntimeWarning: coroutine 'XX' was never 62 | awaited``. 63 | 64 | The following common lines require awaiting, note that these must be 65 | awaited in functions/methods that are async. Awaiting in a non-async 66 | function/method is a syntax error. 67 | 68 | .. code-block:: python 69 | 70 | await request.data 71 | await request.get_data() 72 | await request.json 73 | await request.get_json() 74 | await request.form 75 | await request.files 76 | await render_template() 77 | await render_template_string() 78 | 79 | Testing 80 | ------- 81 | 82 | The test client also requires the usage of async and await keywords, 83 | mostly to await test requests i.e. 84 | 85 | .. code-block:: python 86 | 87 | await test_client.get('/') 88 | await test_client.post('/') 89 | await test_client.open('/', 'PUT') 90 | 91 | Extensions 92 | ---------- 93 | 94 | To use a Flask extension with Quart see the :ref:`flask_extensions` 95 | documentation. 96 | -------------------------------------------------------------------------------- /docs/how_to_guides/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | How to guides 3 | ============= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | 9 | background_tasks.rst 10 | blueprints.rst 11 | command_line.rst 12 | configuration.rst 13 | developing.rst 14 | disconnections.rst 15 | event_loop.rst 16 | flask_extensions.rst 17 | flask_migration.rst 18 | json_encoding.rst 19 | logging.rst 20 | middleware.rst 21 | request_body.rst 22 | routing.rst 23 | server_sent_events.rst 24 | session_storage.rst 25 | startup_shutdown.rst 26 | streaming_response.rst 27 | sync_code.rst 28 | templating.rst 29 | testing.rst 30 | quart_extensions.rst 31 | using_http2.rst 32 | websockets.rst 33 | -------------------------------------------------------------------------------- /docs/how_to_guides/json_encoding.rst: -------------------------------------------------------------------------------- 1 | .. _json_encoding: 2 | 3 | JSON Encoding 4 | ============= 5 | 6 | It is often useful to be able to control how objects are encoded to 7 | and decoded from JSON. Quart makes this possible via a JSONProvider 8 | :class:`~quart.json.provider.JSONProvider`. 9 | 10 | Money example 11 | ------------- 12 | 13 | As an example lets consider a Money object, 14 | 15 | .. code-block:: python 16 | 17 | class Money: 18 | 19 | def __init__(self, amount: Decimal, currency: str) -> None: 20 | self.amount = amount 21 | self.currency = currency 22 | 23 | which we desire to translate to JSON as, 24 | 25 | .. code-block:: json 26 | 27 | { 28 | "amount": "10.00", 29 | "currency": "GBP" 30 | } 31 | 32 | using encoders and decoders as so, 33 | 34 | .. code-block:: python 35 | 36 | from quart.json.provider import _default, DefaultJSONProvider 37 | 38 | 39 | class MoneyJSONProvider(DefaultJSONProvider): 40 | 41 | @staticmethod 42 | def default(object_): 43 | if isinstance(object_, date): 44 | return http_date(object_) 45 | if isinstance(object_, (Decimal, UUID)): 46 | return str(object_) 47 | if is_dataclass(object_): 48 | return asdict(object_) 49 | if hasattr(object_, "__html__"): 50 | return str(object_.__html__()) 51 | if isinstance(object_, Money): 52 | return {'amount': object_.amount, 'currency': object_.currency} 53 | 54 | raise TypeError(f"Object of type {type(object_).__name__} is not JSON serializable") 55 | 56 | @staticmethod 57 | def dict_to_object(dict_): 58 | if 'amount' in dict_ and 'currency' in dict_: 59 | return Money(Decimal(dict_['amount']), dict_['currency']) 60 | else: 61 | return dict_ 62 | 63 | def loads(self, object_, **kwargs): 64 | return super().loads(object_, object_hook=self.dict_to_object, **kwargs) 65 | -------------------------------------------------------------------------------- /docs/how_to_guides/logging.rst: -------------------------------------------------------------------------------- 1 | .. _how_to_log: 2 | 3 | Logging 4 | ======= 5 | 6 | Quart has a standard Python logger sharing the same name as the 7 | ``app.name``. To use it, simply make use of 8 | :attr:`~quart.app.Quart.logger`, for example: 9 | 10 | .. code-block:: python 11 | 12 | app.logger.info('Interesting') 13 | app.logger.warning('Easy Now') 14 | 15 | Configuration 16 | ------------- 17 | 18 | The Quart logger is not created until its first usage, which may occur 19 | as the app is created. These loggers on creation respect any existing 20 | configuration. This allows the loggers to be configured like any other 21 | python logger, for example 22 | 23 | .. code-block:: python 24 | 25 | from logging.config import dictConfig 26 | 27 | dictConfig({ 28 | 'version': 1, 29 | 'loggers': { 30 | 'quart.app': { 31 | 'level': 'ERROR', 32 | }, 33 | }, 34 | }) 35 | 36 | Disabling/removing handlers 37 | --------------------------- 38 | 39 | The handler :attr:`~quart.logging.default_handler` attached to the 40 | quart logger can be removed like so, 41 | 42 | .. code-block:: python 43 | 44 | from logging import getLogger 45 | from quart.logging import default_handler 46 | 47 | getLogger(app.name).removeHandler(default_handler) 48 | -------------------------------------------------------------------------------- /docs/how_to_guides/middleware.rst: -------------------------------------------------------------------------------- 1 | .. _middleware: 2 | 3 | Middleware 4 | ========== 5 | 6 | Middleware can be used to wrap a Quart app instance and alter the ASGI 7 | process. A very simple example would be to reject requests based on 8 | the presence of a header, 9 | 10 | .. code-block:: python 11 | 12 | class RejectMiddleware: 13 | 14 | def __init__(self, app): 15 | self.app = app 16 | 17 | async def __call__(self, scope, receive, send): 18 | if "headers" not in scope: 19 | return await self.app(scope, receive, send) 20 | 21 | for header, value in scope['headers']: 22 | if header.lower() == b'x-secret' and value == b'very-secret': 23 | return await self.app(scope, receive, send) 24 | 25 | return await self.error_response(receive, send) 26 | 27 | async def error_response(self, receive, send): 28 | await send({ 29 | 'type': 'http.response.start', 30 | 'status': 401, 31 | 'headers': [(b'content-length', b'0')], 32 | }) 33 | await send({ 34 | 'type': 'http.response.body', 35 | 'body': b'', 36 | 'more_body': False, 37 | }) 38 | 39 | Whilst middleware can always be used as a wrapper around the app 40 | instance, it is best to assign to and wrap the ``asgi_app`` attribute, 41 | 42 | .. code-block:: python 43 | 44 | quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app) 45 | 46 | as this ensures that the middleware is applied in any test code. 47 | 48 | You can combine multiple middleware wrappers, 49 | 50 | .. code-block:: python 51 | 52 | quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app) 53 | quart_app.asgi_app = AdditionalMiddleware(quart_app.asgi_app) 54 | 55 | and use any ASGI middleware. 56 | 57 | .. warning:: 58 | 59 | Middleware runs before any Quart code, which means that if the 60 | middleware returns a response no Quart functionality nor any Quart 61 | extensions will run. 62 | -------------------------------------------------------------------------------- /docs/how_to_guides/quart_extensions.rst: -------------------------------------------------------------------------------- 1 | .. _quart_extensions: 2 | 3 | Using Quart Extensions 4 | ====================== 5 | 6 | There are a number of extensions for Quart, some of which are listed 7 | here, 8 | 9 | - `Quart-Auth <https://github.com/pgjones/quart-auth>`_ Secure cookie 10 | sessions, allows login, authentication and logout. 11 | - `Quart-Babel <https://github.com/Quart-Addons/quart-babel>`_ Implements i18n and l10n support for Quart. 12 | - `Quart-Bcrypt <https://github.com/Quart-Addons/quart-bcrypt>`_ Provides bcrypt hashing utilities for your application. 13 | - `Quart-compress <https://github.com/AceFire6/quart-compress>`_ 14 | compress your application's responses with gzip. 15 | - `Quart-compress2 16 | <https://github.com/DahlitzFlorian/quart-compress>`_ A package to 17 | compress responses in your Quart app with gzip . 18 | - `Quart-CORS <https://github.com/pgjones/quart-cors>`_ Cross Origin 19 | Resource Sharing (access control) support. 20 | - `Quart-DB <https://github.com/pgjones/quart-db>`_ Managed 21 | connection(s) to postgresql database(s). 22 | - `Quart-events <https://github.com/smithk86/quart-events>`_ event 23 | broadcasting via WebSockets or SSE. 24 | - `Quart-Login <https://github.com/0000matteo0000/quart-login>`_ a 25 | port of Flask-Login to work natively with Quart. 26 | - `Quart-minify <https://github.com/AceFire6/quart_minify/>`_ minify 27 | quart response for HTML, JS, CSS and less. 28 | - `Quart-Mongo <https://github.com/Quart-Addons/quart-mongo>`_ Bridges Quart, Motor, and Odmantic to create a powerful MongoDB 29 | extension. 30 | - `Quart-Motor <https://github.com/marirs/quart-motor>`_ Motor 31 | (MongoDB) support for Quart applications. 32 | - `Quart-OpenApi <https://github.com/factset/quart-openapi/>`_ RESTful 33 | API building. 34 | - `Quart-Keycloak <https://github.com/kroketio/quart-keycloak>`_ 35 | Support for Keycloak's OAuth2 OpenID Connect (OIDC). 36 | - `Quart-Rapidoc <https://github.com/marirs/quart-rapidoc>`_ API 37 | documentation from OpenAPI Specification. 38 | - `Quart-Rate-Limiter 39 | <https://github.com/pgjones/quart-rate-limiter>`_ Rate limiting 40 | support. 41 | - `Quart-Redis 42 | <https://github.com/enchant97/quart-redis>`_ Redis connection handling 43 | - `Webargs-Quart <https://github.com/esfoobar/webargs-quart>`_ Webargs 44 | parsing for Quart. 45 | - `Quart-SqlAlchemy <https://github.com/joeblackwaslike/quart-sqlalchemy>`_ Quart-SQLAlchemy provides a simple wrapper for SQLAlchemy. 46 | - `Quart-WTF <https://github.com/Quart-Addons/quart-wtf>`_ Simple integration of Quart 47 | and WTForms. Including CSRF and file uploading. 48 | - `Quart-Schema <https://github.com/pgjones/quart-schema>`_ Schema 49 | validation and auto-generated API documentation. 50 | - `Quart-session <https://github.com/sanderfoobar/quart-session>`_ server 51 | side session support. 52 | - `Quart-LibreTranslate <https://github.com/Quart-Addons/quart-libretranslate>`_ Simple integration to 53 | use LibreTranslate with your Quart app. 54 | - `Quart-Uploads <https://github.com/Quart-Addons/quart-uploads>`_ File upload handling for Quart. 55 | 56 | Supporting sync code in a Quart Extension 57 | ----------------------------------------- 58 | 59 | Extension authors can support sync functions by utilising the 60 | :meth:`quart.Quart.ensure_async` method. For example, if the extension 61 | provides a view function decorator add ``ensure_async`` before calling 62 | the decorated function, 63 | 64 | .. code-block:: python 65 | 66 | def extension(func): 67 | @wraps(func) 68 | async def wrapper(*args, **kwargs): 69 | ... # Extension logic 70 | return await current_app.ensure_async(func)(*args, **kwargs) 71 | return wrapper 72 | -------------------------------------------------------------------------------- /docs/how_to_guides/request_body.rst: -------------------------------------------------------------------------------- 1 | .. _request_body: 2 | 3 | Consuming the request body 4 | ========================== 5 | 6 | Requests can come with a body, for example for a POST request the body 7 | can include form encoded data from a webpage or JSON encoded data from 8 | a client. The body is sent after the request line and headers for both 9 | HTTP/1 and HTTP/2. This allows Quart to trigger the app's request 10 | handling code before the full body has been received. Additionally the 11 | requester can choose to stream the request body, especially if the 12 | body is large as is often the case when sending files. 13 | 14 | Quart follows Flask and provides methods to await the entire body 15 | before continuing, 16 | 17 | .. code-block:: python 18 | 19 | @app.route('/', methods=['POST']) 20 | async def index(): 21 | await request.get_data() 22 | 23 | Advanced usage 24 | -------------- 25 | 26 | You may wish to completely control how the request body is consumed, 27 | most likely to consume the data as it is received. To do this quart 28 | provides methods to iterate over the body, 29 | 30 | .. code-block:: python 31 | 32 | from async_timeout import timeout 33 | 34 | @app.route('/', methods=['POST']) 35 | async def index(): 36 | async with timeout(app.config['BODY_TIMEOUT']): 37 | async for data in request.body: 38 | ... 39 | 40 | .. note:: 41 | 42 | The above snippet uses `Async-Timeout 43 | <https://github.com/aio-libs/async-timeout>`_ to ensure the body is 44 | received within the timeout specified. 45 | 46 | .. warning:: 47 | 48 | Whilst the other request methods and attributes for accessing the 49 | body will timeout if the client takes to long send the 50 | request. Usage of :attr:`~quart.wrappers.request.Request.body` will 51 | not and it is up to you to wrap usage in a timeout. 52 | 53 | .. warning:: 54 | 55 | Iterating over the body consumes the data, so any further usage of 56 | the data is not possible unless it is saved during the iteration. 57 | 58 | Testing 59 | ------- 60 | 61 | To test that a route consumes the body iteratively you will need to use 62 | the :meth:`~quart.testing.client.QuartClient.request` method, 63 | 64 | .. code-block:: python 65 | 66 | async def test_stream() -> None: 67 | test_client = app.test_client() 68 | async with test_client.request(...) as connection: 69 | await connection.send(b"data") 70 | await connection.send_complete() 71 | response = await connection.as_response() 72 | assert response ... 73 | 74 | it also makes sense to check the code cleans up as you expect if the 75 | client disconnects whilst the request body is being sent, 76 | 77 | .. code-block:: python 78 | 79 | async def test_stream_closed() -> None: 80 | test_client = app.test_client() 81 | async with test_client.request(...) as connection: 82 | await connection.send(b"partial") 83 | await connection.disconnect() 84 | # Check cleanup markers.... 85 | -------------------------------------------------------------------------------- /docs/how_to_guides/routing.rst: -------------------------------------------------------------------------------- 1 | .. _routing: 2 | 3 | Routing 4 | ======= 5 | 6 | Quart allows for multiple and complex routes to be defined, allowing a 7 | client to trigger specific code depending on the method and path 8 | requested. 9 | 10 | The simplest routing is simple static rules, such as the following, 11 | 12 | .. code-block:: python 13 | 14 | @app.route('/') 15 | async def index(): 16 | ... 17 | 18 | @app.route('/about') 19 | async def about(): 20 | ... 21 | 22 | which is often sufficient for mostly static websites. 23 | 24 | Dynamic routing 25 | --------------- 26 | 27 | Dynamic routing can be achieved by using ``<variable>`` markers which 28 | specify that part of the route can be matched rather than 29 | pre-defined. For example, 30 | 31 | .. code-block:: python 32 | 33 | @app.route('/page/<page_no>') 34 | async def page(page_no): 35 | ... 36 | 37 | will match paths ``/page/1``, ``/page/2``, and ``/page/jeff`` with the 38 | ``page_no`` argument set to ``'1'``, ``'2'``, and ``'jeff'`` 39 | respectively. 40 | 41 | Converters 42 | ^^^^^^^^^^ 43 | 44 | It is often necessary and useful to specify how the variable should 45 | convert and by implication match paths. This works by adding the 46 | converter name before the variable name separated by a colon, 47 | ``<converter:variable>``. Adapting the example above to, 48 | 49 | .. code-block:: python 50 | 51 | @app.route('/page/<int:page_no>') 52 | async def page(page_no): 53 | ... 54 | 55 | will match paths ``/page/1``, and ``/page/2`` with the ``page_no`` 56 | argument set to ``1``, and ``2`` (note types) but will no longer match 57 | ``/page/jeff`` as ``jeff`` cannot be converted to an int. 58 | 59 | The available converters are, 60 | 61 | ========== ========================================== 62 | ``float`` positive floating point numbers 63 | ``int`` positive integers 64 | ``path`` like ``string`` with slashes 65 | ``string`` (default) any text without a slash 66 | ``uuid`` UUID strings 67 | ========== ========================================== 68 | 69 | note that additional converters can be added to the 70 | :attr:`~quart.app.Quart.url_map` :attr:`~quart.routing.Map.converters` 71 | dictionary. 72 | 73 | 74 | Catch all route 75 | ^^^^^^^^^^^^^^^ 76 | 77 | A ``/<path:path>`` route definition will catch all requests that do 78 | not match any other routes. 79 | 80 | 81 | Default values 82 | -------------- 83 | 84 | Variable usage can sometimes prove annoying to users, for example 85 | ``/page/<int:page_no>`` will not match ``/page`` forcing the user to 86 | specify ``/page/1``. This can be solved by specifying a default value, 87 | 88 | .. code-block:: python 89 | 90 | @app.route('/page', defaults={'page_no': 1}) 91 | @app.route('/page/<int:page_no>') 92 | async def page(page_no): 93 | ... 94 | 95 | which allows ``/page`` to match with ``page_no`` set to ``1``. 96 | 97 | 98 | Host matching, host and subdomain 99 | --------------------------------- 100 | 101 | Routes can be added to the app with an explicit ``host`` or 102 | ``subdomain`` to match if the app has host matching enabled. This 103 | results in the routes only matching if the host header matches, for 104 | example ``host='quart.com'`` will allow the route to match any request 105 | with a host header of ``quart.com`` and otherwise 404. 106 | 107 | The ``subdomain`` option can only be used if the app config 108 | ``SERVER_NAME`` is set, as the host will be built up as 109 | ``{subdomain}.{SERVER_NAME}``. 110 | 111 | Note that the variable converters can be used in the host or subdomain 112 | options. 113 | 114 | See also 115 | -------- 116 | 117 | Quart uses `Werkzeug's router <https://werkzeug.palletsprojects.com/en/2.1.x/routing/>`_ 118 | -------------------------------------------------------------------------------- /docs/how_to_guides/server_sent_events.rst: -------------------------------------------------------------------------------- 1 | .. _server_sent_events: 2 | 3 | Server Sent Events 4 | ================== 5 | 6 | Quart supports streaming `Server Sent Events 7 | <https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events>`_, 8 | which you may decide to use as an alternative to WebSockets - 9 | especially if the communication is one way. 10 | 11 | Server sent events must be encoded in a specific way, as shown by this 12 | helper class: 13 | 14 | .. code-block:: python 15 | 16 | from dataclasses import dataclass 17 | 18 | @dataclass 19 | class ServerSentEvent: 20 | data: str 21 | event: str | None = None 22 | id: int | None = None 23 | retry: int | None 24 | 25 | def encode(self) -> bytes: 26 | message = f"data: {self.data}" 27 | if self.event is not None: 28 | message = f"{message}\nevent: {self.event}" 29 | if self.id is not None: 30 | message = f"{message}\nid: {self.id}" 31 | if self.retry is not None: 32 | message = f"{message}\nretry: {self.retry}" 33 | message = f"{message}\n\n" 34 | return message.encode('utf-8') 35 | 36 | To use a GET route that returns a streaming generator is 37 | required. This generator, ``send_events`` in the code below, must 38 | yield the encoded Server Sent Event. The route itself also needs to 39 | check the client will accept ``text/event-stream`` responses and set 40 | the response headers appropriately: 41 | 42 | .. code-block:: python 43 | 44 | from quart import abort, make_response 45 | 46 | @app.get("/sse") 47 | async def sse(): 48 | if "text/event-stream" not in request.accept_mimetypes: 49 | abort(400) 50 | 51 | async def send_events(): 52 | while True: 53 | data = ... # Up to you where the events are from 54 | event = ServerSentEvent(data) 55 | yield event.encode() 56 | 57 | response = await make_response( 58 | send_events(), 59 | { 60 | 'Content-Type': 'text/event-stream', 61 | 'Cache-Control': 'no-cache', 62 | 'Transfer-Encoding': 'chunked', 63 | }, 64 | ) 65 | response.timeout = None 66 | return response 67 | 68 | Quart by default will timeout long responses to protect against 69 | possible denial of service attacks, see :ref:`dos_mitigations`. For 70 | this reason the timeout is disabled. This can be done globally, 71 | however that could make other routes DOS vulnerable, therefore the 72 | recommendation is to set the timeout attribute on the specific 73 | response to ``None``. 74 | 75 | See also 76 | -------- 77 | 78 | :ref:`streaming_response` 79 | -------------------------------------------------------------------------------- /docs/how_to_guides/session_storage.rst: -------------------------------------------------------------------------------- 1 | .. _session_storage: 2 | 3 | Session Storage 4 | =============== 5 | 6 | It is often useful to store information relevant to a user of the app 7 | for the duration of that usage session. For example the user may 8 | choose to want to save a option or be remembered as logged in. This 9 | information can either be stored client side or server side and Quart 10 | provides a system to store the information client side via Secure 11 | Cookie Sessions. 12 | 13 | Secure Cookie Sessions 14 | ---------------------- 15 | 16 | Secure Cookie Sessions store the session information on the Cookie in 17 | plain text with a signature to ensure that the information is not 18 | altered by the client. They can be used in Quart so long as the 19 | :attr:`~quart.app.Quart.secret_key` is set to a **secret** 20 | value. 21 | 22 | An example usage to store a users colour preference would be, 23 | 24 | .. code-block:: python 25 | 26 | from quart import session 27 | ... 28 | 29 | 30 | @app.route('/') 31 | async def index(): 32 | return await render_template( 33 | 'index.html', 34 | colour=session.get('colour', 'black'), 35 | ) 36 | 37 | @app.route('/colour/', methods=['POST']) 38 | async def set_colour(): 39 | ... 40 | session['colour'] = colour 41 | return redirect(url_for('index')) 42 | 43 | Permanent Sessions 44 | ------------------ 45 | 46 | The cookies used by default are not set to be permanent (deleted when 47 | the browser's session ends) to have permanent cookies 48 | ``session.permanent`` must be ``True`` when the session is 49 | modified. To set this as the default use this snippet, 50 | 51 | .. code-block:: python 52 | 53 | @app.before_request 54 | def make_session_permanent(): 55 | session.permanent = True 56 | 57 | WebSockets 58 | ---------- 59 | 60 | Sessions can be used with WebSockets with an important caveat about 61 | cookies. A cookie can only be set on a HTTP response, and an accepted 62 | WebSocket connection cannot return a HTTP response. Therefore the 63 | default implementation, being based on cookies, will lose any 64 | modifications made during an accepted WebSocket connection. 65 | -------------------------------------------------------------------------------- /docs/how_to_guides/startup_shutdown.rst: -------------------------------------------------------------------------------- 1 | .. _startup_shutdown: 2 | 3 | Startup and Shutdown 4 | ==================== 5 | 6 | The `ASGI lifespan specification`_ includes the ability for awaiting 7 | coroutines before the first byte is received and after the final byte 8 | is sent, through the ``startup`` and ``shutdown`` lifespan events. 9 | This is particularly useful for creating and destroying connection 10 | pools. Quart supports this via the decorators 11 | :func:`~quart.app.Quart.before_serving`, 12 | :func:`~quart.app.Quart.after_serving`, and 13 | :func:`~quart.app.Quart.while_serving` which expects a function that 14 | returns a generator. 15 | 16 | .. _ASGI lifespan specification: https://github.com/django/asgiref/blob/master/specs/lifespan.rst 17 | 18 | The decorated functions are all called within the app context, 19 | allowing ``current_app`` and ``g`` to be used. 20 | 21 | .. warning:: 22 | 23 | Use ``g`` with caution, as it will reset after startup, i.e. after 24 | all the ``before_serving`` functions complete and after the 25 | initial yield in a while serving generator. It can still be used 26 | within this context. If you want to create something used in 27 | routes, try storing it on the app instead. 28 | 29 | To use this functionality simply do the following: 30 | 31 | .. code-block:: python 32 | 33 | @app.before_serving 34 | async def create_db_pool(): 35 | app.db_pool = await ... 36 | g.something = something 37 | 38 | @app.before_serving 39 | async def use_g(): 40 | g.something.do_something() 41 | 42 | @app.while_serving 43 | async def lifespan(): 44 | ... # startup 45 | yield 46 | ... # shutdown 47 | 48 | @app.route("/") 49 | async def index(): 50 | app.db_pool.execute(...) 51 | # g.something is not available here 52 | 53 | @app.after_serving 54 | async def create_db_pool(): 55 | await app.db_pool.close() 56 | 57 | Testing 58 | ------- 59 | 60 | Quart's test client works on a request lifespan and hence does not 61 | call ``before_serving``, or ``after_serving`` functions, nor advance 62 | the ``while_serving`` generator. Instead Quart's test app can be used, 63 | for example 64 | 65 | .. code-block:: python 66 | 67 | @pytest.fixture(name="app", scope="function") 68 | async def _app(): 69 | app = create_app() # Initialize app 70 | async with app.test_app() as test_app: 71 | yield test_app 72 | 73 | The app fixture can then be used as normal, knowing that the 74 | ``before_serving``, and ``after_serving`` functions have been called, 75 | and the ``while_serving`` generator has been advanced, 76 | 77 | .. code-block:: python 78 | 79 | async def test_index(app): 80 | test_client = app.test_client() 81 | await test_client.get("/") 82 | ... 83 | -------------------------------------------------------------------------------- /docs/how_to_guides/streaming_response.rst: -------------------------------------------------------------------------------- 1 | .. _streaming_response: 2 | 3 | Streaming responses 4 | =================== 5 | 6 | Quart supports responses that are meant to be streamed to the client, 7 | rather than received in one block. If you are interested in streaming 8 | the request data see :ref:`request_body` or for duplex streaming see 9 | :ref:`websockets`. 10 | 11 | To stream a response the view-function should return an asynchronous 12 | generator that yields bytes. This generator can be returned with a 13 | status code and headers as normal. For example to stream the time 14 | every second, 15 | 16 | .. code-block:: python 17 | 18 | @app.route('/') 19 | async def stream_time(): 20 | async def async_generator(): 21 | time = datetime.isoformat() 22 | yield time.encode() 23 | return async_generator(), 200, {'X-Something': 'value'} 24 | 25 | With context 26 | ------------ 27 | 28 | If you want to make use of the ``request`` context whilst streaming 29 | you will need to use the :func:`quart.helpers.stream_with_context` 30 | decorator, 31 | 32 | .. code-block:: python 33 | 34 | @app.route('/') 35 | async def stream_time(): 36 | @stream_with_context 37 | async def async_generator(): 38 | time = datetime.isoformat() 39 | yield time.encode() 40 | return async_generator(), 200, {'X-Something': 'value'} 41 | 42 | Timeout 43 | ------- 44 | 45 | Quart by default will timeout long responses to protect against 46 | possible denial of service attacks, see :ref:`dos_mitigations`. This 47 | may be undesired for streaming responses, e.g. an indefinite 48 | stream. The timeout can be disabled globally, however this could make 49 | other routes DOS vulnerable, therefore the recommendation is to set 50 | the timeout attribute on a specific response to ``None``, 51 | 52 | .. code-block:: python 53 | 54 | from quart import make_response 55 | 56 | @app.route('/sse') 57 | async def stream_time(): 58 | ... 59 | response = await make_response(async_generator()) 60 | response.timeout = None # No timeout for this route 61 | return response 62 | 63 | Testing 64 | ------- 65 | 66 | The test client :meth:`~quart.testing.client.QuartClient.get` and 67 | associated methods will collate the entire streamed response. If you 68 | want to test that the route actually streams the response, or to test 69 | routes that stream until the client disconnects you will need to use 70 | the :meth:`~quart.testing.client.QuartClient.request` method, 71 | 72 | .. code-block:: python 73 | 74 | async def test_stream() -> None: 75 | test_client = app.test_client() 76 | async with test_client.request(..) as connection: 77 | data = await connection.receive() 78 | assert data ... 79 | assert connection.status_code == 200 80 | ... 81 | await connection.disconnect() # For infinite streams 82 | 83 | See also 84 | -------- 85 | 86 | :ref:`server_sent_events` 87 | -------------------------------------------------------------------------------- /docs/how_to_guides/sync_code.rst: -------------------------------------------------------------------------------- 1 | .. _sync_code: 2 | 3 | Run synchronous code 4 | ==================== 5 | 6 | Synchronous code will block the event loop and degrade the performance 7 | of the Quart application it is run in. This is because the synchronous 8 | code will block the task it is run in and in addition block the event 9 | loop. It is for this reason that synchronous code is best avoided, 10 | with asynchronous versions used in preference. 11 | 12 | It is likely though that you will need to use a third party library 13 | that is synchronous as there is no asynchronous version to use in its 14 | place. In this situation it is common to run the synchronous code in a 15 | thread pool executor so that it doesn't block the event loop and hence 16 | degrade the performance of the Quart application. This can be a bit 17 | tricky to do, so Quart provides some helpers to do so. Firstly any 18 | synchronous route will be run in an executor, i.e. 19 | 20 | .. code-block:: python 21 | 22 | @app.route("/") 23 | def sync(): 24 | method = request.method 25 | ... 26 | 27 | will result in the sync function being run in a thread. Note that you 28 | are still within the :ref:`contexts`, and hence you can still access 29 | the ``request``, ``current_app`` and other globals. 30 | 31 | The following functionality accepts synchronous functions and will run 32 | them in a thread, 33 | 34 | - Route handlers 35 | - Endpoint handlers 36 | - Error handlers 37 | - Context processors 38 | - Before request 39 | - Before websocket 40 | - Before first request 41 | - Before serving 42 | - After request 43 | - After websocket 44 | - After serving 45 | - Teardown request 46 | - Teardown websocket 47 | - Teardown app context 48 | - Open session 49 | - Make null session 50 | - Save session 51 | 52 | Context usage 53 | ------------- 54 | 55 | Whilst you can access the ``request`` and other globals in synchronous 56 | routes you will be unable to await coroutine functions. To work around 57 | this Quart provides :meth:`~quart.app.Quart.run_sync` which can be 58 | used as so, 59 | 60 | .. code-block:: python 61 | 62 | @app.route("/") 63 | async def sync_within(): 64 | data = await request.get_json() 65 | 66 | def sync_processor(): 67 | # does something with data 68 | ... 69 | 70 | result = await run_sync(sync_processor)() 71 | return result 72 | 73 | this is similar to utilising the asyncio run_in_executor function, 74 | 75 | .. code-block:: python 76 | 77 | @app.route("/") 78 | async def sync_within(): 79 | data = await request.get_json() 80 | 81 | def sync_processor(): 82 | # does something with data 83 | ... 84 | 85 | result = await asyncio.get_running_loop().run_in_executor( 86 | None, sync_processor 87 | ) 88 | return result 89 | 90 | .. note:: 91 | 92 | The run_in_executor function does not copy the current context, 93 | whereas the run_sync method does. It is for this reason that the 94 | latter is recommended. Without the copied context the ``request`` 95 | and other globals will not be accessible. 96 | -------------------------------------------------------------------------------- /docs/how_to_guides/templating.rst: -------------------------------------------------------------------------------- 1 | .. _templating: 2 | 3 | Templates 4 | ========= 5 | 6 | Quart uses the `Jinja <https://jinja.palletsprojects.com>`_ templating engine, 7 | which is well `documented 8 | <https://jinja.palletsprojects.com/templates/>`_. Quart adds a standard 9 | context, and some standard filters to the Jinja defaults. Quart also 10 | adds the ability to define custom filters, tests and contexts at an 11 | app and blueprint level. 12 | 13 | There are two functions to use when templating, 14 | :func:`~quart.templating.render_template` and 15 | :func:`~quart.templating.render_template_string`, both must be 16 | awaited. The return value from either function is a string and can 17 | form a route response directly or be otherwise combined. Both 18 | functions take an variable number of additional keyword arguments to 19 | pass to the template as context, for example, 20 | 21 | .. code-block:: python 22 | 23 | @app.route('/') 24 | async def index(): 25 | return await render_template('index.html', hello='world') 26 | 27 | Quart standard extras 28 | --------------------- 29 | 30 | The standard context includes the ``config``, ``request``, 31 | ``session``, and ``g`` with these objects referencing the 32 | ``current_app.config`` and those defined in :mod:`~quart.globals` 33 | respectively. The can be accessed as expected, 34 | 35 | .. code-block:: python 36 | 37 | @app.route('/') 38 | async def index(): 39 | return await render_template_string("{{ request.endpoint }}") 40 | 41 | The standard global functions are :func:`~quart.helpers.url_for` and 42 | :func:`~quart.helpers.get_flashed_messages`. These can be used as expected, 43 | 44 | .. code-block:: python 45 | 46 | @app.route('/') 47 | async def index(): 48 | return await render_template_string("<a href="{{ url_for('index') }}>index</a>") 49 | 50 | Adding filters, tests, globals and context 51 | ------------------------------------------ 52 | 53 | To add a filter for usage in templates, make use of 54 | :meth:`~quart.app.Quart.template_filter` or 55 | :meth:`~quart.blueprints.Blueprint.app_template_filter` as decorators, 56 | or :meth:`~quart.app.Quart.add_template_filter` or 57 | :meth:`~quart.blueprints.Blueprint.add_app_template_filter` as 58 | functions. These expect the filter to take in Any value and return a 59 | str, e.g. 60 | 61 | .. code-block:: python 62 | 63 | @app.template_filter(name='upper') 64 | def upper_case(value): 65 | return value.upper() 66 | 67 | @app.route('/') 68 | async def index(): 69 | return await render_template_string("{{ lower | upper }}") 70 | 71 | tests and globals work in a very similar way only with the test and 72 | global methods rather than filter. 73 | 74 | The context processors however have an additional feature, in that 75 | they can be specified on a per blueprint basis. This allows contextual 76 | information to be present only for requests that are routed to the 77 | blueprint. By default 78 | :meth:`~quart.blueprints.Blueprint.context_processor` adds contextual 79 | information to blueprint routed requests whereas 80 | :meth:`~quart.blueprints.Blueprint.app_context_processor` adds the 81 | information to all requests to the app. An example, 82 | 83 | .. code-block:: python 84 | 85 | @blueprint.context_processor 86 | async def blueprint_only(): 87 | return {'context': 'value'} 88 | 89 | @blueprint.app_context_processor 90 | async def app_wide(): 91 | return {'context': 'value'} 92 | -------------------------------------------------------------------------------- /docs/how_to_guides/testing.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | Testing 4 | ======= 5 | 6 | Quart's usage of global variables (``request`` etc) makes testing any 7 | code that uses these variables more difficult. To combat this it is 8 | best practice to only use these variables in the code directly called 9 | by Quart e.g. route functions or before request functions. Thereafter 10 | Quart provides a testing framework to control these globals. 11 | 12 | Primarily testing should be done using a test client bound to the 13 | Quart app being tested. As this is so common there is a helper method 14 | :meth:`~quart.app.Quart.test_client` which returns a bound client, 15 | e.g. 16 | 17 | .. code-block:: python 18 | 19 | async def test_app(app): 20 | client = app.test_client() 21 | response = await client.get('/') 22 | assert response.status_code == 200 23 | 24 | Event loops 25 | ----------- 26 | 27 | To test with quart you will need to have an event loop in order to 28 | call the async functions. This is possible to do manually, for example 29 | 30 | .. code-block:: python 31 | 32 | def aiotest(func): 33 | loop = asyncio.get_event_loop() 34 | loop.run_until_complete(func()) 35 | 36 | @aiotest 37 | async def test_app(app) 38 | ... 39 | 40 | However it is much easier to use ``pytest-asyncio`` and the to do this 41 | for you. Note that ``pytest`` is the recommended test runner and the 42 | examples throughout assume ``pytest`` is used with ``pytest-asyncio``. 43 | 44 | Calling routes 45 | -------------- 46 | 47 | The test client has helper methods for all the HTTP verbs 48 | e.g. :meth:`~quart.testing.QuartClient.post`. These are helper methods 49 | for :meth:`~quart.testing.QuartClient.open`, as such all the methods at 50 | a minimum expect a path and optionally can have query parameters, json 51 | or form data. A standard :class:`~quart.wrappers.Response` class is 52 | returned. An example: 53 | 54 | .. code-block:: python 55 | 56 | async def test_create(app): 57 | test_client = app.test_client() 58 | data = {'name': 'foo'} 59 | response = await test_client.post('/resource/', json=data) 60 | assert response.status_code == 201 61 | result = await response.get_json() 62 | assert result == data 63 | 64 | To test test routes which stream requests or responses, use the 65 | :meth:`~quart.testing.client.QuartClient.request` method: 66 | 67 | .. code-block:: python 68 | 69 | async def test_stream() -> None: 70 | test_client = app.test_client() 71 | async with test_client.request(...) as connection: 72 | await connection.send(b"data") 73 | await connection.send_complete() 74 | ... 75 | # receive a chunk of the response 76 | data = await connection.receive() 77 | ... 78 | # assemble the rest of the response without the first bit 79 | response = await connection.as_response() 80 | 81 | To learn more about streaming requests and responses, read :ref:`request_body` 82 | and :ref:`streaming_response`. 83 | 84 | Context testing 85 | --------------- 86 | 87 | It is often necessary to test something within the app or request 88 | contexts. This is simple enough for the app context, 89 | 90 | .. code-block:: python 91 | 92 | async def test_app_context(app): 93 | async with app.app_context(): 94 | current_app.[use] 95 | 96 | for the request context however the request context has to be faked, 97 | at a minimum this means the method and path must be supplied, e.g. 98 | 99 | .. code-block:: python 100 | 101 | async def test_app_context(app): 102 | async with app.test_request_context("/", method="GET"): 103 | request.[use] 104 | 105 | .. note:: 106 | 107 | Any ``before_request`` or ``after_request`` functions are not 108 | called when using the ``test_request_context``. You can add 109 | ``await app.preprocess_request()`` to ensure the 110 | ``before_request`` functions are called. 111 | 112 | .. code-block:: python 113 | 114 | async def test_app_context(app): 115 | async with app.test_request_context("/", method="GET"): 116 | await app.preprocess_request() 117 | # The before_request functions have now been called 118 | request.[use] 119 | -------------------------------------------------------------------------------- /docs/how_to_guides/using_http2.rst: -------------------------------------------------------------------------------- 1 | .. _using_http2: 2 | 3 | Using HTTP/2 4 | ============ 5 | 6 | `HTTP/2 <https://http2.github.io/>`__ is the second major version of 7 | the Hyper Text Transfer Protocol used to transfer web data. 8 | 9 | .. note:: 10 | 11 | Not all ASGI Servers support HTTP/2. The recommended ASGI server, 12 | Hypercorn, does. 13 | 14 | To use HTTP/2 in development you will need to create some SSL 15 | certificates and run Quart with SSL. 16 | 17 | Server push or push promises 18 | ---------------------------- 19 | 20 | With `HTTP/2 <http://httpwg.org/specs/rfc7540.html#PushResources>`__ 21 | the server can choose to pre-emptively push additional responses to 22 | the client, this is termed a server push and the response itself is 23 | called a push promise. Server push is very useful when the server 24 | knows the client will likely initiate a request, say for the css or js 25 | referenced in a html response. 26 | 27 | .. note:: 28 | 29 | Browsers are deprecating support for server push, and usage is not 30 | recommended. This section is kept for reference. 31 | 32 | In Quart server push can be initiated during a request via the 33 | function :func:`~quart.helpers.make_push_promise`, for example, 34 | 35 | .. code-block:: python 36 | 37 | async def index(): 38 | await make_push_promise(url_for('static', filename='css/minimal.css')) 39 | return await render_template('index.html') 40 | 41 | The push promise will include (copy) header values present in the 42 | request that triggers the push promise. These are to ensure that the 43 | push promise is responded too as if the request had made it. A good 44 | example is the ``Accept`` header. The full set of copied headers are 45 | ``SERVER_PUSH_HEADERS_TO_COPY`` in the request module. 46 | 47 | .. note:: 48 | 49 | This functionality is only useable with ASGI servers that 50 | implement the ``HTTP/2 Server Push`` extension. If the server does 51 | not support this extension Quart will ignore the push promises (as 52 | with HTTP/1 connections). Hypercorn, the recommended ASGI server, 53 | supports this extension. 54 | 55 | When testing server push,the :class:`~quart.testing.QuartClient` 56 | ``push_promises`` list will contain every push promise as a tuple of 57 | the path and headers, for example, 58 | 59 | .. code-block:: python 60 | 61 | async def test_push_promise(): 62 | test_client = app.test_client() 63 | await test_client.get("/push") 64 | assert test_client.push_promises[0] == ("/", {}) 65 | 66 | HTTP/2 clients 67 | -------------- 68 | 69 | At the time of writing there aren't that many HTTP/2 clients. The best 70 | option is to use a browser and inspect the network connections (turn 71 | on the protocol information). Otherwise curl can be used, if HTTP/2 72 | support is `installed <https://curl.haxx.se/docs/http2.html>`_, as so, 73 | 74 | .. code-block:: console 75 | 76 | $ curl --http2 ... 77 | 78 | If you wish to communicate via HTTP/2 in python `httpx 79 | <https://github.com/encode/httpx>`_ is the best choice. 80 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. title:: Quart documentation 4 | 5 | .. image:: _static/quart-name.svg 6 | :class: only-light 7 | :height: 200px 8 | :align: center 9 | 10 | .. image:: _static/quart-name-dark.svg 11 | :class: only-dark 12 | :height: 200px 13 | :align: center 14 | 15 | Quart 16 | ===== 17 | 18 | Quart is a Fast Python web microframework. Using Quart you can, 19 | 20 | * write JSON APIs e.g. :ref:`a RESTful API<api_tutorial>`, 21 | * render and serve HTML e.g. :ref:`a blog<blog_tutorial>`, 22 | * serve WebSockets e.g. :ref:`a simple chat<chat_tutorial>`, 23 | * stream responses e.g. :ref:`serve video<video_tutorial>`, 24 | * all of the above in a single app, 25 | * or do pretty much anything over the HTTP or WebSocket protocols. 26 | 27 | With all of the above possible using asynchronous (asyncio) 28 | libraries/code or :ref:`synchronous<sync_code>` libraries/code. 29 | 30 | If you are, 31 | 32 | * new to Python then start by reading :ref:`installation` instructions, 33 | * new to Quart then try the :ref:`quickstart`, 34 | * new to asyncio see the :ref:`asyncio` guide, 35 | * migrating from Flask see :ref:`flask_migration`, 36 | * looking for a cheatsheet then look :ref:`here<cheatsheet>`. 37 | 38 | Quart is an asyncio reimplementation of the popular `Flask 39 | <https://flask.palletsprojects.com>`_ microframework API. This means that if you 40 | understand Flask you understand Quart. See :ref:`flask_evolution` to 41 | learn more about how Quart builds on Flask. 42 | 43 | Like Flask Quart has an ecosystem of 44 | :ref:`extensions<quart_extensions>` for more specific needs. In 45 | addition a number of the Flask :ref:`extensions<flask_extensions>` 46 | work with Quart. 47 | 48 | Quart is developed on `Github <https://github.com/pallets/quart>`_. If 49 | you come across an issue, or have a feature request please open an 50 | `issue <https://github.com/pallets/quart/issues>`_.If you want to 51 | contribute a fix or the feature-implementation please do (typo fixes 52 | welcome), by proposing a `merge request 53 | <https://github.com/pallets/quart/merge_requests>`_. If you want to 54 | ask for help try `on discord <https://discord.gg/pallets>`_. 55 | 56 | .. note:: 57 | 58 | If you can't find documentation for what you are looking for here, 59 | remember that Quart is an implementation of the Flask API and 60 | hence the `Flask documentation <https://flask.palletsprojects.com>`_ is 61 | a great source of help. Quart is also built on the `Jinja 62 | <https://flask.palletsprojects.com>`_ template engine and the `Werkzeug 63 | <https://werkzeug.palletsprojects.com>`_ toolkit. 64 | 65 | The Flask documentation is so good that you may be better placed 66 | consulting it first then returning here to check how Quart 67 | differs. 68 | 69 | Tutorials 70 | --------- 71 | 72 | .. toctree:: 73 | :maxdepth: 2 74 | 75 | tutorials/index.rst 76 | 77 | How to guides 78 | ------------- 79 | 80 | .. toctree:: 81 | :maxdepth: 2 82 | 83 | how_to_guides/index.rst 84 | 85 | Discussion 86 | ---------- 87 | 88 | .. toctree:: 89 | :maxdepth: 2 90 | 91 | discussion/index.rst 92 | 93 | References 94 | ---------- 95 | 96 | .. toctree:: 97 | :maxdepth: 2 98 | 99 | reference/index 100 | license 101 | changes 102 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | ```{literalinclude} ../LICENSE.txt 5 | :language: text 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Quart 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/reference/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | source/modules.rst 9 | -------------------------------------------------------------------------------- /docs/reference/cheatsheet.rst: -------------------------------------------------------------------------------- 1 | .. _cheatsheet: 2 | 3 | Cheatsheet 4 | ========== 5 | 6 | Basic App 7 | --------- 8 | 9 | .. code-block:: python 10 | 11 | from quart import Quart 12 | 13 | app = Quart(__name__) 14 | 15 | @app.route("/hello") 16 | async def hello(): 17 | return "Hello, World!" 18 | 19 | if __name__ == "__main__": 20 | app.run(debug=True) 21 | 22 | Routing 23 | ------- 24 | 25 | .. code-block:: python 26 | 27 | @app.route("/hello/<string:name>") # example.com/hello/quart 28 | async def hello(name): 29 | return f"Hello, {name}!" 30 | 31 | Request Methods 32 | --------------- 33 | 34 | .. code-block:: python 35 | 36 | @app.route("/get") # GET Only by default 37 | @app.route("/get", methods=["GET", "POST"]) # GET and POST 38 | @app.route("/get", methods=["DELETE"]) # Just DELETE 39 | 40 | JSON Responses 41 | -------------- 42 | 43 | .. code-block:: python 44 | 45 | @app.route("/hello") 46 | async def hello(): 47 | return {"Hello": "World!"} 48 | 49 | Template Rendering 50 | ------------------ 51 | 52 | .. code-block:: python 53 | 54 | from quart import render_template 55 | 56 | @app.route("/hello") 57 | async def hello(): 58 | return await render_template("index.html") # Required to be in templates/ 59 | 60 | Configuration 61 | ------------- 62 | 63 | .. code-block:: python 64 | 65 | import json 66 | import tomllib 67 | 68 | app.config["VALUE"] = "something" 69 | 70 | app.config.from_file("filename.toml", tomllib.load) 71 | app.config.from_file("filename.json", json.load) 72 | 73 | Request 74 | ------- 75 | 76 | .. code-block:: python 77 | 78 | from quart import request 79 | 80 | @app.route("/hello") 81 | async def hello(): 82 | request.method 83 | request.url 84 | request.headers["X-Bob"] 85 | request.args.get("a") # Query string e.g. example.com/hello?a=2 86 | await request.get_data() # Full raw body 87 | (await request.form)["name"] 88 | (await request.get_json())["key"] 89 | request.cookies.get("name") 90 | 91 | WebSocket 92 | --------- 93 | 94 | .. code-block:: python 95 | 96 | from quart import websocket 97 | 98 | @app.websocket("/ws") 99 | async def ws(): 100 | websocket.headers 101 | while True: 102 | try: 103 | data = await websocket.receive() 104 | await websocket.send(f"Echo {data}") 105 | except asyncio.CancelledError: 106 | # Handle disconnect 107 | raise 108 | 109 | Cookies 110 | ------- 111 | 112 | .. code-block:: python 113 | 114 | from quart import make_response 115 | 116 | @app.route("/hello") 117 | async def hello(): 118 | response = await make_response("Hello") 119 | response.set_cookie("name", "value") 120 | return response 121 | 122 | Abort 123 | ----- 124 | 125 | .. code-block:: python 126 | 127 | from quart import abort 128 | 129 | @app.route("/hello") 130 | async def hello(): 131 | abort(409) 132 | 133 | 134 | HTTP/2 & HTTP/3 Server Push 135 | --------------------------- 136 | 137 | .. code-block:: python 138 | 139 | from quart import make_push_promise, url_for 140 | 141 | @app.route("/hello") 142 | async def hello(): 143 | await make_push_promise(url_for('static', filename='css/minimal.css')) 144 | ... 145 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Reference 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | api.rst 9 | cheatsheet.rst 10 | logo.rst 11 | response_values.rst 12 | versioning.rst 13 | -------------------------------------------------------------------------------- /docs/reference/logo.rst: -------------------------------------------------------------------------------- 1 | Logo 2 | ==== 3 | 4 | The Quart logo has been kindly provided by Vic Shóstak, @koddr, 5 | `Quart-Logo <https://github.com/koddr/quart-logo>`_. 6 | 7 | The logo itself has a lot of meaning, as expressed by @koddr, 8 | 9 | This is looks like Flask fonts and give us to understand what's 10 | the quart means?, because 1 quart equal 0.9 litre (and we're 11 | assuming: full bottle on logo hold one litre, but it's not full — 12 | because hold a quart now). 13 | 14 | The bung on top of bottle makes it clear — this is production 15 | ready Python framework with speed more than Flask, but Flask-like 16 | — all in one pack!. Get, open (equal install) and use it! 17 | 18 | Unstable liquid's state in bottle give us to understand — this 19 | liquid is reactivity, like new async features on Python and 20 | HTTP/2. And some aggressive gradient's color of liquid say to us 21 | hey, I'm alive and can help you to improve yourself... just use 22 | me!. 23 | 24 | 25 | .. image:: ../_static/logo.png 26 | :alt: Quart logo 27 | -------------------------------------------------------------------------------- /docs/reference/response_values.rst: -------------------------------------------------------------------------------- 1 | .. _response_values: 2 | 3 | Response Return Values 4 | ====================== 5 | 6 | Response functions can return a number of different types as the 7 | response value which will trigger different responses to the 8 | client. The possible direct returns are, 9 | 10 | Response Values 11 | --------------- 12 | 13 | str 14 | ^^^ 15 | 16 | .. code-block:: python 17 | 18 | return "Hello" 19 | return await render_template("index.html") 20 | 21 | A solitary string return indicates that you intend to return a string 22 | mimetype ``text/html``. The string will be encoded using the default 23 | :attr:`~quart.wrappers._BaseRequestResponse.charset`. 24 | 25 | dict 26 | ^^^^ 27 | 28 | .. code-block:: python 29 | 30 | return {"a": "b"} 31 | 32 | A solitary dict return indicates that you intend to return json, 33 | ``application/json``. The jsonify function will be used to encode the 34 | dictionary. 35 | 36 | list 37 | ^^^^ 38 | 39 | .. code-block:: python 40 | 41 | return ["a", "b"] 42 | 43 | A solitary list return indicates that you intend to return json, 44 | ``application/json``. The jsonify function will be used to encode the 45 | list. 46 | 47 | Response 48 | ^^^^^^^^ 49 | 50 | .. code-block:: python 51 | 52 | @app.route('/') 53 | async def route_func(): 54 | return Response("Hello") 55 | 56 | Returning a Response instance indicates that you know exactly what you 57 | wish to return. 58 | 59 | AsyncGenerator[bytes, None] 60 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 61 | 62 | .. code-block:: python 63 | 64 | @app.route('/') 65 | async def route_func(): 66 | 67 | async def agen(): 68 | data = await something 69 | yield data 70 | 71 | return agen() 72 | 73 | Returning an async generator allows for the response to be streamed to 74 | the client, thereby lowing the peak memory usage, if combined with a 75 | ``Transfer-Encoding`` header with value ``chunked``. 76 | 77 | Generator[bytes, None, None] 78 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 79 | 80 | .. code-block:: python 81 | 82 | @app.route('/') 83 | async def route_func(): 84 | 85 | def gen(): 86 | yield data 87 | 88 | return gen() 89 | 90 | Returning an generator allows for the response to be streamed to the 91 | client, thereby lowing the peak memory usage, if combined with a 92 | ``Transfer-Encoding`` header with value ``chunked``. 93 | 94 | Combinations 95 | ------------ 96 | 97 | Any of the above Response Values can be combined, as described, 98 | 99 | Tuple[ResponseValue, int] 100 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 101 | 102 | .. code-block:: python 103 | 104 | @app.route('/') 105 | async def route_func(): 106 | return "Hello", 200 107 | 108 | A tuple of a Response Value and a integer indicates that you intend to 109 | specify the status code. 110 | 111 | Tuple[str, int, Dict[str, str]] 112 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 113 | 114 | .. code-block:: python 115 | 116 | @app.route('/') 117 | async def route_func(): 118 | return "Hello", 200, {'X-Header': 'Value'} 119 | 120 | A tuple of a Response Value, integer and dictionary indicates that you intend 121 | to specify additional headers. 122 | -------------------------------------------------------------------------------- /docs/reference/versioning.rst: -------------------------------------------------------------------------------- 1 | Versioning 2 | ========== 3 | 4 | Quart uses `Semantic Versioning <https://semver.org/>`_ with the 5 | caveat that Quart has yet to reach it's first major release. This 6 | means there may be changes between minor versions that would be 7 | considered major version changes - these are avoided and indicated in 8 | the changelog. 9 | 10 | Long term support 11 | ----------------- 12 | 13 | Only the latest release is supported and hence likely to receive 14 | updates. No previous releases are supported. Please always upgrade to 15 | the latest Quart release where possible. 16 | 17 | Python versions 18 | --------------- 19 | 20 | Quart will support Python versions until they reach end of life. This 21 | may, is rare circumstances be achieved by bug fixes to a specific 22 | release branch i.e. no new features. This happended with Python 3.6 23 | and the Quart 0.5.X releases. 24 | -------------------------------------------------------------------------------- /docs/tutorials/asyncio.rst: -------------------------------------------------------------------------------- 1 | .. _asyncio: 2 | 3 | Introduction to asyncio 4 | ======================= 5 | 6 | Asyncio is the part of the Python standard library that provides an 7 | event loop with IO (input/output) operations. It exists to allow 8 | concurrent programming in Python, whereby the event loop switches to 9 | another task whilst the previous task waits on IO. This concurrency 10 | allows for greater CPU utilisation and hence greater throughput 11 | performance. 12 | 13 | The easiest way to understand this is to consider something concrete, 14 | namely a demonstrative simulation In the following we fetch a url with 15 | a simulated IO delay, 16 | 17 | .. code-block:: python 18 | 19 | import asyncio 20 | 21 | 22 | async def simulated_fetch(url, delay): 23 | await asyncio.sleep(delay) 24 | print(f"Fetched {url} after {delay}") 25 | return f"<html>{url}" 26 | 27 | 28 | def main(): 29 | loop = asyncio.get_event_loop() 30 | results = loop.run_until_complete(asyncio.gather( 31 | simulated_fetch('http://google.com', 2), 32 | simulated_fetch('http://bbc.co.uk', 1), 33 | )) 34 | print(results) 35 | 36 | you should see the following output, 37 | 38 | >>> Fetched http://bbc.co.uk after 1 39 | >>> Fetched http://google.com after 2 40 | >>> ['<html>http://google.com', '<html>http://bbc.co.uk'] 41 | 42 | which indicates that despite calling the ``google.com`` fetch first, 43 | the ``bbc.co.uk`` actually completed first i.e. the code ran 44 | concurrently. Additionally the code runs in a little over 2 seconds 45 | rather than over 3 as expected with synchronous code. 46 | 47 | Relevance to web servers 48 | ------------------------ 49 | 50 | Web servers by definition do IO, in that they receive and respond to 51 | requests from the network. This means that asyncio is a very good fit 52 | even if the code within the framework does no IO itself. Yet in 53 | practice IO is present, for example when loading a template from a 54 | file, or contacting a database or another server. 55 | 56 | Common pitfalls 57 | --------------- 58 | 59 | It is very easy to await the wrong thing, for example, 60 | 61 | .. code-block:: python 62 | 63 | await awaitable.attribute 64 | 65 | will not await the ``awaitable`` object as you might expect, but 66 | rather attempt to resolve the attribute and then await it. This is 67 | quite commonly seen as, 68 | 69 | .. code-block:: python 70 | 71 | await request.form.get('key') 72 | 73 | which fails with an error that the coroutine wrapper has no get 74 | attribute. To work around this simply use brackets to indicate what 75 | must be awaited first, 76 | 77 | .. code-block:: python 78 | 79 | (await awaitable).attribute 80 | (await request.form).get('key') 81 | -------------------------------------------------------------------------------- /docs/tutorials/deployment.rst: -------------------------------------------------------------------------------- 1 | .. _deployment: 2 | 3 | Deploying Quart 4 | =============== 5 | 6 | It is not recommended to run Quart directly (via 7 | :meth:`~quart.app.Quart.run`) in production. Instead it is recommended 8 | that Quart be run using `Hypercorn 9 | <https://github.com/pgjones/hypercorn>`_ or an alternative ASGI 10 | server. This is because the :meth:`~quart.app.Quart.run` enables 11 | features that help development yet slow production 12 | performance. Hypercorn is installed with Quart and will be used to 13 | serve requests in development mode by default (e.g. with 14 | :meth:`~quart.app.Quart.run`). 15 | 16 | To use Quart with an ASGI server simply point the server at the Quart 17 | application, for example, 18 | 19 | .. code-block:: python 20 | :caption: example.py 21 | 22 | from quart import Quart 23 | 24 | app = Quart(__name__) 25 | 26 | @app.route('/') 27 | async def hello(): 28 | return 'Hello World' 29 | 30 | you can run with Hypercorn using, 31 | 32 | .. code-block:: bash 33 | 34 | hypercorn example:app 35 | 36 | See the `Hypercorn docs <https://hypercorn.readthedocs.io/>`_. 37 | 38 | Alternative ASGI Servers 39 | ------------------------ 40 | 41 | Alongside `Hypercorn <https://github.com/pgjones/hypercorn>`_, `Daphne 42 | <https://github.com/django/daphne>`_, and `Uvicorn 43 | <https://github.com/encode/uvicorn>`_ are available ASGI servers that 44 | work with Quart. 45 | 46 | Serverless deployment 47 | --------------------- 48 | 49 | To deploy Quart in an AWS Lambda & API Gateway setting you will need to use a specialised 50 | ASGI function adapter. `Mangum <https://github.com/erm/mangum>`_ is 51 | recommended for this and can be as simple as, 52 | 53 | .. code-block:: python 54 | 55 | from mangum import Mangum 56 | from quart import Quart 57 | 58 | app = Quart(__name__) 59 | 60 | @app.route("/") 61 | async def index(): 62 | return "Hello, world!" 63 | 64 | handler = Mangum(app) # optionally set debug=True 65 | -------------------------------------------------------------------------------- /docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Tutorials 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | installation.rst 9 | quickstart.rst 10 | asyncio.rst 11 | api_tutorial.rst 12 | blog_tutorial.rst 13 | chat_tutorial.rst 14 | video_tutorial.rst 15 | deployment.rst 16 | -------------------------------------------------------------------------------- /docs/tutorials/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | Quart is only compatible with Python 3.9 or higher and can be installed 7 | using pip or your favorite python package manager: 8 | 9 | .. code-block:: console 10 | 11 | pip install quart 12 | 13 | Dependencies 14 | ------------ 15 | 16 | Quart dependends on the following packages, which will automatically 17 | be installed with Quart: 18 | 19 | - aiofiles, to load files in an asyncio compatible manner, 20 | - blinker, to manage signals, 21 | - click, to manage command line arguments 22 | - hypercorn, an ASGI server for development, 23 | - importlib_metadata only for Python 3.9, 24 | - itsdangerous, for signing secure cookies, 25 | - jinja2, for template rendering, 26 | - markupsafe, for markup rendering, 27 | - typing_extensions only for Python 3.9, 28 | - werkzeug, as the basis of many Quart classes. 29 | 30 | You can choose to install with the dotenv extra: 31 | 32 | .. code-block:: console 33 | 34 | pip install quart[dotenv] 35 | 36 | Which will install the ``python-dotenv`` package which enables support 37 | for automatically loading environment variables when running ``quart`` 38 | commands. 39 | 40 | See also 41 | -------- 42 | 43 | `Poetry <https://python-poetry.org>`_ for project management. 44 | -------------------------------------------------------------------------------- /docs/tutorials/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | Hello World 7 | ----------- 8 | 9 | A very simple app that simply returns a response containing ``hello`` 10 | is, (file ``hello-world.py``) 11 | 12 | .. code-block:: python 13 | 14 | from quart import Quart 15 | 16 | app = Quart(__name__) 17 | 18 | @app.route('/') 19 | async def hello(): 20 | return 'hello' 21 | 22 | app.run() 23 | 24 | and is simply run via 25 | 26 | .. code-block:: console 27 | 28 | python hello-world.py 29 | 30 | or alternatively 31 | 32 | .. code-block:: console 33 | 34 | $ export QUART_APP=hello-world:app 35 | $ quart run 36 | 37 | and tested by 38 | 39 | .. code-block:: sh 40 | 41 | curl localhost:5000 42 | 43 | See also 44 | -------- 45 | 46 | :ref:`cheatsheet` 47 | -------------------------------------------------------------------------------- /examples/api/README.rst: -------------------------------------------------------------------------------- 1 | Tutorial: Building a RESTful API 2 | ================================ 3 | 4 | This is the example code for the api tutorial. It is managed using 5 | `Poetry <https://python-poetry.org>`_ with the following commands, 6 | 7 | .. code-block:: console 8 | 9 | poetry install 10 | poetry run start 11 | poetry run pytest -c pyproject.toml tests/ 12 | -------------------------------------------------------------------------------- /examples/api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "api" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | quart = "*" 10 | quart-schema = "*" 11 | 12 | [tool.poetry.dev-dependencies] 13 | pytest = "*" 14 | pytest-asyncio = "^0.18.3" 15 | 16 | [tool.poetry.scripts] 17 | start = "api:run" 18 | 19 | [tool.pytest.ini_options] 20 | asyncio_mode = "auto" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.0.0"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /examples/api/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | 6 | from quart_schema import QuartSchema 7 | from quart_schema import validate_request 8 | from quart_schema import validate_response 9 | 10 | from quart import Quart 11 | from quart import request 12 | 13 | app = Quart(__name__) 14 | QuartSchema(app) 15 | 16 | 17 | @app.post("/echo") 18 | async def echo(): 19 | print(request.is_json, request.mimetype) 20 | data = await request.get_json() 21 | return {"input": data, "extra": True} 22 | 23 | 24 | @dataclass 25 | class TodoIn: 26 | task: str 27 | due: datetime | None 28 | 29 | 30 | @dataclass 31 | class Todo(TodoIn): 32 | id: int 33 | 34 | 35 | @app.post("/todos/") 36 | @validate_request(TodoIn) 37 | @validate_response(Todo) 38 | async def create_todo(data: Todo) -> Todo: 39 | return Todo(id=1, task=data.task, due=data.due) 40 | 41 | 42 | def run() -> None: 43 | app.run() 44 | -------------------------------------------------------------------------------- /examples/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/api/tests/__init__.py -------------------------------------------------------------------------------- /examples/api/tests/test_api.py: -------------------------------------------------------------------------------- 1 | from api import app 2 | from api import TodoIn 3 | 4 | 5 | async def test_echo() -> None: 6 | test_client = app.test_client() 7 | response = await test_client.post("/echo", json={"a": "b"}) 8 | data = await response.get_json() 9 | assert data == {"extra": True, "input": {"a": "b"}} 10 | 11 | 12 | async def test_create_todo() -> None: 13 | test_client = app.test_client() 14 | response = await test_client.post("/todos/", json=TodoIn(task="Abc", due=None)) 15 | data = await response.get_json() 16 | assert data == {"id": 1, "task": "Abc", "due": None} 17 | -------------------------------------------------------------------------------- /examples/blog/README.rst: -------------------------------------------------------------------------------- 1 | Tutorial: Building a simple blog 2 | ================================ 3 | 4 | This is the example code for the blog tutorial. It is managed using 5 | `Poetry <https://python-poetry.org>`_ with the following commands, 6 | 7 | .. code-block:: console 8 | 9 | poetry install 10 | poetry run start 11 | poetry run pytest -c pyproject.toml tests/ 12 | -------------------------------------------------------------------------------- /examples/blog/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "blog" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | quart = "*" 10 | 11 | [tool.poetry.dev-dependencies] 12 | pytest = "*" 13 | pytest-asyncio = "*" 14 | 15 | [tool.poetry.scripts] 16 | init_db = "blog:init_db" 17 | start = "blog:run" 18 | 19 | [tool.pytest.ini_options] 20 | asyncio_mode = "auto" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.0.0"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /examples/blog/src/blog/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import dbapi2 as sqlite3 2 | 3 | from quart import g 4 | from quart import Quart 5 | from quart import redirect 6 | from quart import render_template 7 | from quart import request 8 | from quart import url_for 9 | 10 | app = Quart(__name__) 11 | 12 | app.config.update( 13 | { 14 | "DATABASE": app.root_path / "blog.db", 15 | } 16 | ) 17 | 18 | 19 | def _connect_db(): 20 | engine = sqlite3.connect(app.config["DATABASE"]) 21 | engine.row_factory = sqlite3.Row 22 | return engine 23 | 24 | 25 | def _get_db(): 26 | if not hasattr(g, "sqlite_db"): 27 | g.sqlite_db = _connect_db() 28 | return g.sqlite_db 29 | 30 | 31 | @app.get("/") 32 | async def posts(): 33 | db = _get_db() 34 | cur = db.execute( 35 | """SELECT title, text 36 | FROM post 37 | ORDER BY id DESC""", 38 | ) 39 | posts = cur.fetchall() 40 | return await render_template("posts.html", posts=posts) 41 | 42 | 43 | @app.route("/create/", methods=["GET", "POST"]) 44 | async def create(): 45 | if request.method == "POST": 46 | db = _get_db() 47 | form = await request.form 48 | db.execute( 49 | "INSERT INTO post (title, text) VALUES (?, ?)", 50 | [form["title"], form["text"]], 51 | ) 52 | db.commit() 53 | return redirect(url_for("posts")) 54 | else: 55 | return await render_template("create.html") 56 | 57 | 58 | def init_db(): 59 | db = _connect_db() 60 | with open(app.root_path / "schema.sql") as file_: 61 | db.cursor().executescript(file_.read()) 62 | db.commit() 63 | 64 | 65 | def run() -> None: 66 | app.run() 67 | -------------------------------------------------------------------------------- /examples/blog/src/blog/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS post; 2 | CREATE TABLE post ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | title TEXT NOT NULL, 5 | 'text' TEXT NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /examples/blog/src/blog/templates/create.html: -------------------------------------------------------------------------------- 1 | <form method="POST" style="display: flex; flex-direction: column; gap: 8px"> 2 | <label>Title: <input type="text" size="30" name="title" /></label> 3 | <label>Text: <textarea name="text" rows="5" cols="40"></textarea></label> 4 | <button type="submit">Create</button> 5 | </form> 6 | -------------------------------------------------------------------------------- /examples/blog/src/blog/templates/posts.html: -------------------------------------------------------------------------------- 1 | <main> 2 | {% for post in posts %} 3 | <article> 4 | <h2>{{ post.title }}</h2> 5 | <p>{{ post.text|safe }}</p> 6 | </article> 7 | {% else %} 8 | <p>No posts available</p> 9 | {% endfor %} 10 | </main> 11 | -------------------------------------------------------------------------------- /examples/blog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/blog/tests/__init__.py -------------------------------------------------------------------------------- /examples/blog/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from blog import app 3 | from blog import init_db 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def configure_db(tmpdir): 8 | app.config["DATABASE"] = str(tmpdir.join("blog.db")) 9 | init_db() 10 | -------------------------------------------------------------------------------- /examples/blog/tests/test_blog.py: -------------------------------------------------------------------------------- 1 | from blog import app 2 | 3 | 4 | async def test_create_post(): 5 | test_client = app.test_client() 6 | response = await test_client.post( 7 | "/create/", form={"title": "Post", "text": "Text"} 8 | ) 9 | assert response.status_code == 302 10 | response = await test_client.get("/") 11 | text = await response.get_data() 12 | assert b"<h2>Post</h2>" in text 13 | assert b"<p>Text</p>" in text 14 | -------------------------------------------------------------------------------- /examples/chat/README.rst: -------------------------------------------------------------------------------- 1 | Tutorial: Building a chat server 2 | ================================ 3 | 4 | This is the example code for the chat server tutorial. It is managed 5 | using `Poetry <https://python-poetry.org>`_ with the following 6 | commands, 7 | 8 | .. code-block:: console 9 | 10 | poetry install 11 | poetry run start 12 | poetry run pytest -c pyproject.toml tests/ 13 | -------------------------------------------------------------------------------- /examples/chat/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "chat" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | quart = "*" 10 | 11 | [tool.poetry.dev-dependencies] 12 | pytest = "*" 13 | pytest-asyncio = "*" 14 | 15 | [tool.poetry.scripts] 16 | start = "chat:run" 17 | 18 | [tool.pytest.ini_options] 19 | asyncio_mode = "auto" 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /examples/chat/src/chat/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from chat.broker import Broker 4 | from quart import Quart 5 | from quart import render_template 6 | from quart import websocket 7 | 8 | app = Quart(__name__) 9 | broker = Broker() 10 | 11 | 12 | @app.get("/") 13 | async def index(): 14 | return await render_template("index.html") 15 | 16 | 17 | async def _receive() -> None: 18 | while True: 19 | message = await websocket.receive() 20 | await broker.publish(message) 21 | 22 | 23 | @app.websocket("/ws") 24 | async def ws() -> None: 25 | try: 26 | task = asyncio.ensure_future(_receive()) 27 | async for message in broker.subscribe(): 28 | await websocket.send(message) 29 | finally: 30 | task.cancel() 31 | await task 32 | 33 | 34 | def run(): 35 | app.run(debug=True) 36 | -------------------------------------------------------------------------------- /examples/chat/src/chat/broker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import AsyncGenerator 3 | 4 | 5 | class Broker: 6 | def __init__(self) -> None: 7 | self.connections = set() 8 | 9 | async def publish(self, message: str) -> None: 10 | for connection in self.connections: 11 | await connection.put(message) 12 | 13 | async def subscribe(self) -> AsyncGenerator[str, None]: 14 | connection = asyncio.Queue() 15 | self.connections.add(connection) 16 | try: 17 | while True: 18 | yield await connection.get() 19 | finally: 20 | self.connections.remove(connection) 21 | -------------------------------------------------------------------------------- /examples/chat/src/chat/templates/index.html: -------------------------------------------------------------------------------- 1 | <script type="text/javascript"> 2 | const ws = new WebSocket(`ws://${location.host}/ws`); 3 | 4 | ws.addEventListener('message', function (event) { 5 | const li = document.createElement("li"); 6 | li.appendChild(document.createTextNode(event.data)); 7 | document.getElementById("messages").appendChild(li); 8 | }); 9 | 10 | function send(event) { 11 | const message = (new FormData(event.target)).get("message"); 12 | if (message) { 13 | ws.send(message); 14 | } 15 | event.target.reset(); 16 | return false; 17 | } 18 | </script> 19 | 20 | <div style="display: flex; height: 100%; flex-direction: column"> 21 | <ul id="messages" style="flex-grow: 1; list-style-type: none"></ul> 22 | 23 | <form onsubmit="return send(event)"> 24 | <input type="text" name="message" minlength="1" /> 25 | <button type="submit">Send</button> 26 | </form> 27 | </div> 28 | -------------------------------------------------------------------------------- /examples/chat/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/chat/tests/__init__.py -------------------------------------------------------------------------------- /examples/chat/tests/test_chat.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from chat import app 4 | 5 | from quart.testing.connections import ( 6 | TestWebsocketConnection as _TestWebsocketConnection, 7 | ) 8 | 9 | 10 | async def _receive(test_websocket: _TestWebsocketConnection) -> str: 11 | return await test_websocket.receive() 12 | 13 | 14 | async def test_websocket() -> None: 15 | test_client = app.test_client() 16 | async with test_client.websocket("/ws") as test_websocket: 17 | task = asyncio.ensure_future(_receive(test_websocket)) 18 | await test_websocket.send("message") 19 | result = await task 20 | assert result == "message" 21 | -------------------------------------------------------------------------------- /examples/video/README.rst: -------------------------------------------------------------------------------- 1 | Tutorial: Serving video 2 | ======================= 3 | 4 | This is the example code for the video server tutorial. It is managed 5 | using `Poetry <https://python-poetry.org>`_ with the following 6 | commands, 7 | 8 | .. code-block:: console 9 | 10 | poetry install 11 | poetry run start 12 | poetry run pytest -c pyproject.toml tests/ 13 | -------------------------------------------------------------------------------- /examples/video/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "video" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | 10 | [tool.poetry.dev-dependencies] 11 | pytest = "*" 12 | pytest-asyncio = "*" 13 | 14 | [tool.poetry.scripts] 15 | start = "video:run" 16 | 17 | [tool.pytest.ini_options] 18 | asyncio_mode = "auto" 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /examples/video/src/video/__init__.py: -------------------------------------------------------------------------------- 1 | from quart import Quart 2 | from quart import render_template 3 | from quart import send_file 4 | 5 | app = Quart(__name__) 6 | 7 | 8 | @app.get("/") 9 | async def index(): 10 | return await render_template("index.html") 11 | 12 | 13 | @app.route("/video.mp4") 14 | async def auto_video(): 15 | return await send_file(app.static_folder / "video.mp4", conditional=True) 16 | 17 | 18 | def run() -> None: 19 | app.run() 20 | -------------------------------------------------------------------------------- /examples/video/src/video/static/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/video/src/video/static/video.mp4 -------------------------------------------------------------------------------- /examples/video/src/video/templates/index.html: -------------------------------------------------------------------------------- 1 | <video controls width="100%"> 2 | <source src="/video.mp4" type="video/mp4"> 3 | </video> 4 | -------------------------------------------------------------------------------- /examples/video/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/video/tests/__init__.py -------------------------------------------------------------------------------- /examples/video/tests/test_video.py: -------------------------------------------------------------------------------- 1 | from video import app 2 | 3 | 4 | async def test_auto_video() -> None: 5 | test_client = app.test_client() 6 | response = await test_client.get("/video.mp4") 7 | data = await response.get_data() 8 | assert len(data) == 255_849 9 | 10 | response = await test_client.get("/video.mp4", headers={"Range": "bytes=200-1000"}) 11 | data = await response.get_data() 12 | assert len(data) == 801 13 | -------------------------------------------------------------------------------- /src/quart/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from markupsafe import escape as escape 4 | from markupsafe import Markup as Markup 5 | 6 | from .app import Quart as Quart 7 | from .blueprints import Blueprint as Blueprint 8 | from .config import Config as Config 9 | from .ctx import after_this_request as after_this_request 10 | from .ctx import copy_current_app_context as copy_current_app_context 11 | from .ctx import copy_current_request_context as copy_current_request_context 12 | from .ctx import copy_current_websocket_context as copy_current_websocket_context 13 | from .ctx import has_app_context as has_app_context 14 | from .ctx import has_request_context as has_request_context 15 | from .ctx import has_websocket_context as has_websocket_context 16 | from .globals import current_app as current_app 17 | from .globals import g as g 18 | from .globals import request as request 19 | from .globals import session as session 20 | from .globals import websocket as websocket 21 | from .helpers import abort as abort 22 | from .helpers import flash as flash 23 | from .helpers import get_flashed_messages as get_flashed_messages 24 | from .helpers import get_template_attribute as get_template_attribute 25 | from .helpers import make_push_promise as make_push_promise 26 | from .helpers import make_response as make_response 27 | from .helpers import redirect as redirect 28 | from .helpers import send_file as send_file 29 | from .helpers import send_from_directory as send_from_directory 30 | from .helpers import stream_with_context as stream_with_context 31 | from .helpers import url_for as url_for 32 | from .json import jsonify as jsonify 33 | from .signals import appcontext_popped as appcontext_popped 34 | from .signals import appcontext_pushed as appcontext_pushed 35 | from .signals import appcontext_tearing_down as appcontext_tearing_down 36 | from .signals import before_render_template as before_render_template 37 | from .signals import got_request_exception as got_request_exception 38 | from .signals import got_websocket_exception as got_websocket_exception 39 | from .signals import message_flashed as message_flashed 40 | from .signals import request_finished as request_finished 41 | from .signals import request_started as request_started 42 | from .signals import request_tearing_down as request_tearing_down 43 | from .signals import signals_available as signals_available 44 | from .signals import template_rendered as template_rendered 45 | from .signals import websocket_finished as websocket_finished 46 | from .signals import websocket_started as websocket_started 47 | from .signals import websocket_tearing_down as websocket_tearing_down 48 | from .templating import render_template as render_template 49 | from .templating import render_template_string as render_template_string 50 | from .templating import stream_template as stream_template 51 | from .templating import stream_template_string as stream_template_string 52 | from .typing import ResponseReturnValue as ResponseReturnValue 53 | from .wrappers import Request as Request 54 | from .wrappers import Response as Response 55 | from .wrappers import Websocket as Websocket 56 | -------------------------------------------------------------------------------- /src/quart/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | if __name__ == "__main__": 4 | from .cli import main 5 | 6 | main() 7 | -------------------------------------------------------------------------------- /src/quart/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from typing import Any 5 | from typing import Callable 6 | 7 | from flask.config import Config as FlaskConfig # noqa: F401 8 | from flask.config import ConfigAttribute as ConfigAttribute # noqa: F401 9 | 10 | 11 | class Config(FlaskConfig): 12 | def from_prefixed_env( 13 | self, prefix: str = "QUART", *, loads: Callable[[str], Any] = json.loads 14 | ) -> bool: 15 | """Load any environment variables that start with the prefix. 16 | 17 | The prefix (default ``QUART_``) is dropped from the env key 18 | for the config key. Values are passed through a loading 19 | function to attempt to convert them to more specific types 20 | than strings. 21 | 22 | Keys are loaded in :func:`sorted` order. 23 | 24 | The default loading function attempts to parse values as any 25 | valid JSON type, including dicts and lists. Specific items in 26 | nested dicts can be set by separating the keys with double 27 | underscores (``__``). If an intermediate key doesn't exist, it 28 | will be initialized to an empty dict. 29 | 30 | Arguments: 31 | prefix: Load env vars that start with this prefix, 32 | separated with an underscore (``_``). 33 | loads: Pass each string value to this function and use the 34 | returned value as the config value. If any error is 35 | raised it is ignored and the value remains a 36 | string. The default is :func:`json.loads`. 37 | """ 38 | return super().from_prefixed_env(prefix, loads=loads) 39 | -------------------------------------------------------------------------------- /src/quart/datastructures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import PathLike 4 | from pathlib import Path 5 | from typing import IO 6 | 7 | from aiofiles import open as async_open 8 | from werkzeug.datastructures import FileStorage as WerkzeugFileStorage 9 | from werkzeug.datastructures import Headers 10 | 11 | 12 | class FileStorage(WerkzeugFileStorage): 13 | """A thin wrapper over incoming files.""" 14 | 15 | def __init__( 16 | self, 17 | stream: IO[bytes] | None = None, 18 | filename: str | None = None, 19 | name: str | None = None, 20 | content_type: str | None = None, 21 | content_length: int | None = None, 22 | headers: Headers | None = None, 23 | ) -> None: 24 | super().__init__(stream, filename, name, content_type, content_length, headers) 25 | 26 | async def save(self, destination: PathLike, buffer_size: int = 16384) -> None: # type: ignore 27 | """Save the file to the destination. 28 | 29 | Arguments: 30 | destination: A filename (str) or file object to write to. 31 | buffer_size: Buffer size to keep in memory. 32 | """ 33 | async with async_open(destination, "wb") as file_: 34 | data = self.stream.read(buffer_size) 35 | while data != b"": 36 | await file_.write(data) 37 | data = self.stream.read(buffer_size) 38 | 39 | async def load(self, source: PathLike, buffer_size: int = 16384) -> None: 40 | path = Path(source) 41 | self.filename = path.name 42 | async with async_open(path, "rb") as file_: 43 | data = await file_.read(buffer_size) 44 | while data != b"": 45 | self.stream.write(data) 46 | data = await file_.read(buffer_size) 47 | -------------------------------------------------------------------------------- /src/quart/debug.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | 5 | from jinja2 import Template 6 | 7 | from .wrappers import Response 8 | 9 | TEMPLATE = """ 10 | <style> 11 | pre { 12 | margin: 0; 13 | } 14 | 15 | .traceback, .locals { 16 | display: table; 17 | width: 100%; 18 | margin: 5px; 19 | } 20 | 21 | .traceback>div, .locals>div { 22 | display: table-row; 23 | } 24 | 25 | .traceback>div>div, .locals>div>div { 26 | display: table-cell; 27 | } 28 | 29 | .locals>div>div { 30 | border-top: 1px solid lightgrey; 31 | } 32 | 33 | .header { 34 | background-color: #ececec; 35 | margin-bottom: 5px; 36 | } 37 | 38 | .highlight { 39 | background-color: #ececec; 40 | } 41 | 42 | .info { 43 | font-weight: bold; 44 | } 45 | 46 | li { 47 | border: 1px solid lightgrey; 48 | border-radius: 5px; 49 | padding: 5px; 50 | list-style-type: none; 51 | margin-bottom: 5px; 52 | } 53 | 54 | h1>span { 55 | font-weight: lighter; 56 | } 57 | </style> 58 | 59 | <h1>{{ name }} <span>{{ value }}</span></h1> 60 | <ul> 61 | {% for frame in frames %} 62 | <li> 63 | <div class="header"> 64 | File <span class="info">{{ frame.file }}</span>, 65 | line <span class="info">{{ frame.line }}</span>, in 66 | </div> 67 | <div class="traceback"> 68 | {% for line in frame.code[0] %} 69 | <div {% if frame.line == loop.index + frame.code[1] %}class="highlight"\ 70 | {% endif %}> 71 | <div>{{ loop.index + frame.code[1] }}</div> 72 | <div><pre>{{ line }}</pre></div> 73 | </div> 74 | {% endfor %} 75 | </div> 76 | <div class="locals"> 77 | {% for name, repr in frame.locals.items() %} 78 | <div> 79 | <div>{{ name }}</div> 80 | <div>{{ repr }}</div> 81 | </div> 82 | {% endfor %} 83 | </div> 84 | </li> 85 | {% endfor %} 86 | </ul> 87 | """ 88 | 89 | 90 | async def traceback_response(error: Exception) -> Response: 91 | type_ = type(error) 92 | tb = error.__traceback__ 93 | frames = [] 94 | while tb: 95 | frame = tb.tb_frame 96 | try: 97 | code = inspect.getsourcelines(frame) 98 | except OSError: 99 | code = None 100 | 101 | frames.append( 102 | { 103 | "file": inspect.getfile(frame), 104 | "line": frame.f_lineno, 105 | "locals": frame.f_locals, 106 | "code": code, 107 | } 108 | ) 109 | tb = tb.tb_next 110 | 111 | name = type_.__name__ 112 | template = Template(TEMPLATE) 113 | html = template.render(frames=reversed(frames), name=name, value=error) 114 | return Response(html, 500) 115 | -------------------------------------------------------------------------------- /src/quart/globals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextvars import ContextVar 4 | from typing import TYPE_CHECKING 5 | 6 | from werkzeug.local import LocalProxy 7 | 8 | if TYPE_CHECKING: 9 | from .app import Quart 10 | from .ctx import _AppCtxGlobals 11 | from .ctx import AppContext 12 | from .ctx import RequestContext 13 | from .ctx import WebsocketContext 14 | from .sessions import SessionMixin 15 | from .wrappers import Request 16 | from .wrappers import Websocket 17 | 18 | _no_app_msg = "Not within an app context" 19 | _cv_app: ContextVar[AppContext] = ContextVar("quart.app_ctx") 20 | app_ctx: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] 21 | _cv_app, unbound_message=_no_app_msg 22 | ) 23 | current_app: Quart = LocalProxy( # type: ignore[assignment] 24 | _cv_app, "app", unbound_message=_no_app_msg 25 | ) 26 | g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] 27 | _cv_app, "g", unbound_message=_no_app_msg 28 | ) 29 | 30 | _no_req_msg = "Not within a request context" 31 | _cv_request: ContextVar[RequestContext] = ContextVar("quart.request_ctx") 32 | request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] 33 | _cv_request, unbound_message=_no_req_msg 34 | ) 35 | request: Request = LocalProxy( # type: ignore[assignment] 36 | _cv_request, "request", unbound_message=_no_req_msg 37 | ) 38 | 39 | _no_websocket_msg = "Not within a websocket context" 40 | _cv_websocket: ContextVar[WebsocketContext] = ContextVar("quart.websocket_ctx") 41 | websocket_ctx: WebsocketContext = LocalProxy( # type: ignore[assignment] 42 | _cv_websocket, unbound_message=_no_websocket_msg 43 | ) 44 | websocket: Websocket = LocalProxy( # type: ignore[assignment] 45 | _cv_websocket, "websocket", unbound_message=_no_websocket_msg 46 | ) 47 | 48 | 49 | def _session_lookup() -> RequestContext | WebsocketContext: 50 | try: 51 | return _cv_request.get() 52 | except LookupError: 53 | try: 54 | return _cv_websocket.get() 55 | except LookupError: 56 | raise RuntimeError("Not within a request nor websocket context") from None 57 | 58 | 59 | session: SessionMixin = LocalProxy(_session_lookup, "session") # type: ignore[assignment] 60 | -------------------------------------------------------------------------------- /src/quart/json/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from typing import Any 5 | from typing import IO 6 | from typing import TYPE_CHECKING 7 | 8 | from flask.json.provider import _default 9 | 10 | from ..globals import current_app 11 | 12 | if TYPE_CHECKING: 13 | from ..wrappers import Response # noqa: F401 14 | 15 | 16 | def dumps(object_: Any, **kwargs: Any) -> str: 17 | if current_app: 18 | return current_app.json.dumps(object_, **kwargs) 19 | else: 20 | kwargs.setdefault("default", _default) 21 | return json.dumps(object_, **kwargs) 22 | 23 | 24 | def dump(object_: Any, fp: IO[str], **kwargs: Any) -> None: 25 | if current_app: 26 | current_app.json.dump(object_, fp, **kwargs) 27 | else: 28 | kwargs.setdefault("default", _default) 29 | json.dump(object_, fp, **kwargs) 30 | 31 | 32 | def loads(object_: str | bytes, **kwargs: Any) -> Any: 33 | if current_app: 34 | return current_app.json.loads(object_, **kwargs) 35 | else: 36 | return json.loads(object_, **kwargs) 37 | 38 | 39 | def load(fp: IO[str], **kwargs: Any) -> Any: 40 | if current_app: 41 | return current_app.json.load(fp, **kwargs) 42 | else: 43 | return json.load(fp, **kwargs) 44 | 45 | 46 | def jsonify(*args: Any, **kwargs: Any) -> Response: 47 | return current_app.json.response(*args, **kwargs) # type: ignore 48 | -------------------------------------------------------------------------------- /src/quart/json/provider.py: -------------------------------------------------------------------------------- 1 | from flask.json.provider import DefaultJSONProvider as DefaultJSONProvider # noqa: F401 2 | from flask.json.provider import JSONProvider as JSONProvider # noqa: F401 3 | -------------------------------------------------------------------------------- /src/quart/json/tag.py: -------------------------------------------------------------------------------- 1 | from flask.json.tag import JSONTag as JSONTag # noqa: F401 2 | from flask.json.tag import PassDict as PassDict # noqa: F401 3 | from flask.json.tag import PassList as PassList # noqa: F401 4 | from flask.json.tag import TagBytes as TagBytes # noqa: F401 5 | from flask.json.tag import TagDateTime as TagDateTime # noqa: F401 6 | from flask.json.tag import TagDict as TagDict # noqa: F401 7 | from flask.json.tag import TaggedJSONSerializer as TaggedJSONSerializer # noqa: F401 8 | from flask.json.tag import TagMarkup as TagMarkup # noqa: F401 9 | from flask.json.tag import TagTuple as TagTuple # noqa: F401 10 | from flask.json.tag import TagUUID as TagUUID # noqa: F401 11 | -------------------------------------------------------------------------------- /src/quart/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from logging import DEBUG 5 | from logging import Formatter 6 | from logging import getLogger 7 | from logging import Handler 8 | from logging import Logger 9 | from logging import LogRecord 10 | from logging import NOTSET 11 | from logging import StreamHandler 12 | from logging.handlers import QueueHandler 13 | from logging.handlers import QueueListener 14 | from queue import SimpleQueue as Queue 15 | from typing import TYPE_CHECKING 16 | 17 | if TYPE_CHECKING: 18 | from .app import Quart # noqa 19 | 20 | default_handler = StreamHandler(sys.stderr) 21 | default_handler.setFormatter( 22 | Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") 23 | ) 24 | 25 | 26 | class LocalQueueHandler(QueueHandler): 27 | """Custom QueueHandler that skips record preparation. 28 | 29 | There is no need to prepare records that go into a local, in-process queue, 30 | we can skip that process and minimise the cost of logging further. 31 | """ 32 | 33 | def prepare(self, record: LogRecord) -> LogRecord: 34 | return record 35 | 36 | 37 | def _setup_logging_queue(*handlers: Handler) -> QueueHandler: 38 | """Create a new LocalQueueHandler and start an associated QueueListener.""" 39 | queue: Queue = Queue() 40 | queue_handler = LocalQueueHandler(queue) 41 | 42 | serving_listener = QueueListener(queue, *handlers, respect_handler_level=True) 43 | serving_listener.start() 44 | 45 | return queue_handler 46 | 47 | 48 | def has_level_handler(logger: Logger) -> bool: 49 | """Check if the logger already has a handler""" 50 | level = logger.getEffectiveLevel() 51 | current = logger 52 | 53 | while current: 54 | if any(handler.level <= level for handler in current.handlers): 55 | return True 56 | 57 | if not current.propagate: 58 | break 59 | 60 | current = current.parent 61 | 62 | return False 63 | 64 | 65 | def create_logger(app: Quart) -> Logger: 66 | """Create a logger for the app based on the app settings. 67 | 68 | This creates a logger named quart.app that has a log level based 69 | on the app configuration. 70 | """ 71 | logger = getLogger(app.name) 72 | 73 | if app.debug and logger.level == NOTSET: 74 | logger.setLevel(DEBUG) 75 | 76 | if not has_level_handler(logger): 77 | queue_handler = _setup_logging_queue(default_handler) 78 | logger.addHandler(queue_handler) 79 | 80 | return logger 81 | -------------------------------------------------------------------------------- /src/quart/py.typed: -------------------------------------------------------------------------------- 1 | Marker 2 | -------------------------------------------------------------------------------- /src/quart/routing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from collections.abc import Iterable 5 | 6 | from werkzeug.routing import Map 7 | from werkzeug.routing import MapAdapter 8 | from werkzeug.routing import Rule 9 | 10 | from .wrappers.base import BaseRequestWebsocket 11 | 12 | 13 | class QuartRule(Rule): 14 | def __init__( 15 | self, 16 | string: str, 17 | defaults: dict | None = None, 18 | subdomain: str | None = None, 19 | methods: Iterable[str] | None = None, 20 | endpoint: str | None = None, 21 | strict_slashes: bool | None = None, 22 | merge_slashes: bool | None = None, 23 | host: str | None = None, 24 | websocket: bool = False, 25 | provide_automatic_options: bool = False, 26 | ) -> None: 27 | super().__init__( 28 | string, 29 | defaults=defaults, 30 | subdomain=subdomain, 31 | methods=methods, 32 | endpoint=endpoint, 33 | strict_slashes=strict_slashes, 34 | merge_slashes=merge_slashes, 35 | host=host, 36 | websocket=websocket, 37 | ) 38 | self.provide_automatic_options = provide_automatic_options 39 | 40 | 41 | class QuartMap(Map): 42 | def bind_to_request( 43 | self, 44 | request: BaseRequestWebsocket, 45 | subdomain: str | None, 46 | server_name: str | None, 47 | ) -> MapAdapter: 48 | host: str 49 | if server_name is None: 50 | host = request.host.lower() 51 | else: 52 | host = server_name.lower() 53 | 54 | host = _normalise_host(request.scheme, host) 55 | 56 | if subdomain is None and not self.host_matching: 57 | request_host_parts = _normalise_host( 58 | request.scheme, request.host.lower() 59 | ).split(".") 60 | config_host_parts = host.split(".") 61 | offset = -len(config_host_parts) 62 | 63 | if request_host_parts[offset:] != config_host_parts: 64 | warnings.warn( 65 | f"Current server name '{request.host}' doesn't match configured" 66 | f" server name '{host}'", 67 | stacklevel=2, 68 | ) 69 | subdomain = "<invalid>" 70 | else: 71 | subdomain = ".".join(filter(None, request_host_parts[:offset])) 72 | 73 | return super().bind( 74 | host, 75 | request.root_path, 76 | subdomain, 77 | request.scheme, 78 | request.method, 79 | request.path, 80 | request.query_string.decode(), 81 | ) 82 | 83 | 84 | def _normalise_host(scheme: str, host: str) -> str: 85 | # It is not common to write port 80 or 443 for a hostname, 86 | # so strip it if present. 87 | if scheme in {"http", "ws"} and host.endswith(":80"): 88 | return host[:-3] 89 | elif scheme in {"https", "wss"} and host.endswith(":443"): 90 | return host[:-4] 91 | else: 92 | return host 93 | -------------------------------------------------------------------------------- /src/quart/signals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blinker import Namespace 4 | 5 | signals_available = True 6 | 7 | _signals = Namespace() 8 | 9 | #: Called before a template is rendered, connection functions 10 | # should have a signature of Callable[[Quart, Template, dict], None] 11 | before_render_template = _signals.signal("before-render-template") 12 | 13 | #: Called when a template has been rendered, connected functions 14 | # should have a signature of Callable[[Quart, Template, dict], None] 15 | template_rendered = _signals.signal("template-rendered") 16 | 17 | #: Called just after the request context has been created, connected 18 | # functions should have a signature of Callable[[Quart], None] 19 | request_started = _signals.signal("request-started") 20 | 21 | #: Called after a response is fully finalised, connected functions 22 | # should have a signature of Callable[[Quart, Response], None] 23 | request_finished = _signals.signal("request-finished") 24 | 25 | #: Called as the request context is teared down, connected functions 26 | # should have a signature of Callable[[Quart, Exception], None] 27 | request_tearing_down = _signals.signal("request-tearing-down") 28 | 29 | #: Called if there is an exception in a background task, connected 30 | # functions should have a signature of Callable[[Quart, Exception], None] 31 | got_background_exception = _signals.signal("got-background-exception") 32 | 33 | #: Called if there is an exception in a before or after serving 34 | # function, connected functions should have a signature of 35 | # Callable[[Quart, Exception], None] 36 | got_serving_exception = _signals.signal("got-serving-exception") 37 | 38 | #: Called if there is an exception handling the request, connected 39 | # functions should have a signature of Callable[[Quart, Exception], None] 40 | got_request_exception = _signals.signal("got-request-exception") 41 | 42 | #: Called just after the websocket context has been created, connected 43 | # functions should have a signature of Callable[[Quart], None] 44 | websocket_started = _signals.signal("websocket-started") 45 | 46 | #: Called on receipt of a message over the websocket, connected 47 | # functions should have a signature of Callable[[AnyStr], None] 48 | websocket_received = _signals.signal("websocket-received") 49 | 50 | #: Called when a message has been sent over the websocket, connected 51 | # functions should have a signature of Callable[[AnyStr], None] 52 | websocket_sent = _signals.signal("websocket-sent") 53 | 54 | #: Called after a response is fully finalised, connected functions 55 | # should have a signature of Callable[[Quart, Optional[Response]], None] 56 | websocket_finished = _signals.signal("websocket-finished") 57 | 58 | #: Called as the websocket context is teared down, connected functions 59 | # should have a signature of Callable[[Quart, Exception], None] 60 | websocket_tearing_down = _signals.signal("websocket-tearing-down") 61 | 62 | #: Called if there is an exception handling the websocket, connected 63 | # functions should have a signature of Callable[[Quart, Exception], None] 64 | got_websocket_exception = _signals.signal("got-websocket-exception") 65 | 66 | #: Called as the application context is teared down, connected functions 67 | # should have a signature of Callable[[Quart, Exception], None] 68 | appcontext_tearing_down = _signals.signal("appcontext-tearing-down") 69 | 70 | #: Called when the app context is pushed, connected functions should 71 | # have a signature of Callable[[Quart], None] 72 | appcontext_pushed = _signals.signal("appcontext-pushed") 73 | 74 | #: Called when the app context is popped, connected functions should 75 | # have a signature of Callable[[Quart], None] 76 | appcontext_popped = _signals.signal("appcontext-popped") 77 | 78 | #: Called on a flash invocation, connection functions 79 | # should have a signature of Callable[[Quart, str, str], None] 80 | message_flashed = _signals.signal("message-flashed") 81 | -------------------------------------------------------------------------------- /src/quart/testing/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import TYPE_CHECKING 5 | 6 | from click.testing import CliRunner 7 | 8 | from ..cli import ScriptInfo 9 | from .app import TestApp 10 | from .client import QuartClient 11 | from .connections import WebsocketResponseError 12 | from .utils import make_test_body_with_headers 13 | from .utils import make_test_headers_path_and_query_string 14 | from .utils import make_test_scope 15 | from .utils import no_op_push 16 | from .utils import sentinel 17 | 18 | if TYPE_CHECKING: 19 | from ..app import Quart 20 | 21 | 22 | class QuartCliRunner(CliRunner): 23 | def __init__(self, app: Quart, **kwargs: Any) -> None: 24 | self.app = app 25 | super().__init__(**kwargs) 26 | 27 | def invoke(self, cli: Any = None, args: Any = None, **kwargs: Any) -> Any: # type: ignore 28 | if cli is None: 29 | cli = self.app.cli 30 | 31 | if "obj" not in kwargs: 32 | kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) 33 | 34 | return super().invoke(cli, args, **kwargs) 35 | 36 | 37 | __all__ = ( 38 | "make_test_body_with_headers", 39 | "make_test_headers_path_and_query_string", 40 | "make_test_scope", 41 | "no_op_push", 42 | "QuartClient", 43 | "QuartCliRunner", 44 | "sentinel", 45 | "TestApp", 46 | "WebsocketResponseError", 47 | ) 48 | -------------------------------------------------------------------------------- /src/quart/testing/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from collections.abc import Awaitable 5 | from types import TracebackType 6 | from typing import TYPE_CHECKING 7 | 8 | from hypercorn.typing import ASGIReceiveEvent 9 | from hypercorn.typing import ASGISendEvent 10 | from hypercorn.typing import LifespanScope 11 | 12 | from ..typing import TestClientProtocol 13 | 14 | if TYPE_CHECKING: 15 | from ..app import Quart # noqa 16 | 17 | DEFAULT_TIMEOUT = 6 18 | 19 | 20 | class LifespanError(Exception): 21 | pass 22 | 23 | 24 | class TestApp: 25 | def __init__( 26 | self, 27 | app: Quart, 28 | startup_timeout: int = DEFAULT_TIMEOUT, 29 | shutdown_timeout: int = DEFAULT_TIMEOUT, 30 | ) -> None: 31 | self.app = app 32 | self.startup_timeout = startup_timeout 33 | self.shutdown_timeout = shutdown_timeout 34 | self._startup = asyncio.Event() 35 | self._shutdown = asyncio.Event() 36 | self._app_queue: asyncio.Queue = asyncio.Queue() 37 | self._task: Awaitable[None] = None 38 | 39 | def test_client(self) -> TestClientProtocol: 40 | return self.app.test_client() 41 | 42 | async def startup(self) -> None: 43 | scope: LifespanScope = { 44 | "type": "lifespan", 45 | "asgi": {"spec_version": "2.0"}, 46 | "state": {}, 47 | } 48 | self._task = asyncio.ensure_future( 49 | self.app(scope, self._asgi_receive, self._asgi_send) 50 | ) 51 | await self._app_queue.put({"type": "lifespan.startup"}) 52 | await asyncio.wait_for(self._startup.wait(), timeout=self.startup_timeout) 53 | if self._task.done(): 54 | # This will re-raise any exceptions in the task 55 | await self._task 56 | 57 | async def shutdown(self) -> None: 58 | await self._app_queue.put({"type": "lifespan.shutdown"}) 59 | await asyncio.wait_for(self._shutdown.wait(), timeout=self.shutdown_timeout) 60 | await self._task 61 | 62 | async def __aenter__(self) -> TestApp: 63 | await self.startup() 64 | return self 65 | 66 | async def __aexit__( 67 | self, exc_type: type, exc_value: BaseException, tb: TracebackType 68 | ) -> None: 69 | await self.shutdown() 70 | 71 | async def _asgi_receive(self) -> ASGIReceiveEvent: 72 | return await self._app_queue.get() 73 | 74 | async def _asgi_send(self, message: ASGISendEvent) -> None: 75 | if message["type"] == "lifespan.startup.complete": 76 | self._startup.set() 77 | elif message["type"] == "lifespan.shutdown.complete": 78 | self._shutdown.set() 79 | elif message["type"] == "lifespan.startup.failed": 80 | self._startup.set() 81 | raise LifespanError(f"Error during startup {message['message']}") 82 | elif message["type"] == "lifespan.shutdown.failed": 83 | self._shutdown.set() 84 | raise LifespanError(f"Error during shutdown {message['message']}") 85 | -------------------------------------------------------------------------------- /src/quart/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Collection 4 | from typing import Any 5 | from typing import Callable 6 | from typing import ClassVar 7 | 8 | from .globals import current_app 9 | from .globals import request 10 | from .typing import ResponseReturnValue 11 | from .typing import RouteCallable 12 | 13 | http_method_funcs = frozenset( 14 | ["get", "post", "head", "options", "delete", "put", "trace", "patch"] 15 | ) 16 | 17 | 18 | class View: 19 | """Use to define routes within a class structure. 20 | 21 | A View subclass must implement the :meth:`dispatch_request` in 22 | order to respond to requests. For automatic method finding based on 23 | the request HTTP Verb see :class:`MethodView`. 24 | 25 | An example usage is, 26 | 27 | .. code-block:: python 28 | 29 | class SimpleView: 30 | methods = ['GET'] 31 | 32 | async def dispatch_request(id): 33 | return f"ID is {id}" 34 | 35 | app.add_url_rule('/<id>', view_func=SimpleView.as_view('simple')) 36 | 37 | Note that class 38 | 39 | Attributes: 40 | decorators: A list of decorators to apply to a view 41 | method. The decorators are applied in the order of 42 | the list. 43 | methods: List of methods this view allows. 44 | provide_automatic_options: Override automatic OPTIONS 45 | if set, to either True or False. 46 | init_every_request: Create a new instance of this class 47 | for every request. 48 | """ 49 | 50 | decorators: ClassVar[list[Callable]] = [] 51 | methods: ClassVar[Collection[str] | None] = None 52 | provide_automatic_options: ClassVar[bool | None] = None 53 | init_every_request: ClassVar[bool] = True 54 | 55 | async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue: 56 | """Override and return a Response. 57 | 58 | This will be called with the request view_args, i.e. any url 59 | parameters. 60 | """ 61 | raise NotImplementedError() 62 | 63 | @classmethod 64 | def as_view(cls, name: str, *class_args: Any, **class_kwargs: Any) -> RouteCallable: 65 | if cls.init_every_request: 66 | 67 | async def view(**kwargs: Any) -> ResponseReturnValue: 68 | self = view.view_class(*class_args, **class_kwargs) # type: ignore 69 | return await current_app.ensure_async(self.dispatch_request)(**kwargs) 70 | 71 | else: 72 | self = cls(*class_args, **class_kwargs) 73 | 74 | async def view(**kwargs: Any) -> ResponseReturnValue: 75 | return await current_app.ensure_async(self.dispatch_request)(**kwargs) 76 | 77 | if cls.decorators: 78 | view.__name__ = name 79 | view.__module__ = cls.__module__ 80 | for decorator in cls.decorators: 81 | view = decorator(view) 82 | 83 | view.view_class: type[View] = cls # type: ignore 84 | view.__name__ = name 85 | view.__doc__ = cls.__doc__ 86 | view.__module__ = cls.__module__ 87 | view.methods = cls.methods # type: ignore 88 | view.provide_automatic_options = cls.provide_automatic_options # type: ignore 89 | return view 90 | 91 | 92 | class MethodView(View): 93 | """A HTTP Method (verb) specific view class. 94 | 95 | This has an implementation of :meth:`dispatch_request` such that 96 | it calls a method based on the verb i.e. GET requests are handled 97 | by a `get` method. For example, 98 | 99 | .. code-block:: python 100 | 101 | class SimpleView(MethodView): 102 | async def get(id): 103 | return f"Get {id}" 104 | 105 | async def post(id): 106 | return f"Post {id}" 107 | 108 | app.add_url_rule('/<id>', view_func=SimpleView.as_view('simple')) 109 | """ 110 | 111 | def __init_subclass__(cls, **kwargs: Any) -> None: 112 | super().__init_subclass__(**kwargs) 113 | 114 | if "methods" not in cls.__dict__: 115 | methods = set() 116 | 117 | for base in cls.__bases__: 118 | if getattr(base, "methods", None): 119 | methods.update(base.methods) # type: ignore[attr-defined] 120 | 121 | for key in http_method_funcs: 122 | if hasattr(cls, key): 123 | methods.add(key.upper()) 124 | 125 | if methods: 126 | cls.methods = methods 127 | 128 | async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue: 129 | handler = getattr(self, request.method.lower(), None) 130 | 131 | if handler is None and request.method == "HEAD": 132 | handler = getattr(self, "get", None) 133 | 134 | return await current_app.ensure_async(handler)(**kwargs) 135 | -------------------------------------------------------------------------------- /src/quart/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .base import BaseRequestWebsocket 4 | from .request import Body 5 | from .request import Request 6 | from .response import Response 7 | from .websocket import Websocket 8 | 9 | __all__ = ( 10 | "BaseRequestWebsocket", 11 | "Body", 12 | "Request", 13 | "Response", 14 | "Websocket", 15 | ) 16 | -------------------------------------------------------------------------------- /src/quart/wrappers/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import TYPE_CHECKING 5 | 6 | from hypercorn.typing import WWWScope 7 | from werkzeug.datastructures import Headers 8 | from werkzeug.sansio.request import Request as SansIORequest 9 | 10 | from .. import json 11 | 12 | if TYPE_CHECKING: 13 | from ..routing import QuartRule # noqa 14 | 15 | 16 | class BaseRequestWebsocket(SansIORequest): 17 | """This class is the basis for Requests and websockets.. 18 | 19 | Attributes: 20 | json_module: A custom json decoding/encoding module, it should 21 | have `dump`, `dumps`, `load`, and `loads` methods 22 | routing_exception: If an exception is raised during the route 23 | matching it will be stored here. 24 | url_rule: The rule that this request has been matched too. 25 | view_args: The keyword arguments for the view from the route 26 | matching. 27 | """ 28 | 29 | json_module: json.provider.JSONProvider = json # type: ignore 30 | routing_exception: Exception | None = None 31 | url_rule: QuartRule | None = None 32 | view_args: dict[str, Any] | None = None 33 | 34 | def __init__( 35 | self, 36 | method: str, 37 | scheme: str, 38 | path: str, 39 | query_string: bytes, 40 | headers: Headers, 41 | root_path: str, 42 | http_version: str, 43 | scope: WWWScope, 44 | ) -> None: 45 | """Create a request or websocket base object. 46 | 47 | Arguments: 48 | method: The HTTP verb. 49 | scheme: The scheme used for the request. 50 | path: The full unquoted path of the request. 51 | query_string: The raw bytes for the query string part. 52 | headers: The request headers. 53 | root_path: The root path that should be prepended to all 54 | routes. 55 | http_version: The HTTP version of the request. 56 | scope: Underlying ASGI scope dictionary. 57 | 58 | Attributes: 59 | args: The query string arguments. 60 | scheme: The URL scheme, http or https. 61 | """ 62 | super().__init__( 63 | method, 64 | scheme, 65 | scope.get("server"), 66 | root_path, 67 | path, 68 | query_string, 69 | headers, 70 | headers.get("Remote-Addr"), 71 | ) 72 | self.http_version = http_version 73 | self.scope = scope 74 | 75 | @property 76 | def endpoint(self) -> str | None: 77 | """Returns the corresponding endpoint matched for this request. 78 | 79 | This can be None if the request has not been matched with a 80 | rule. 81 | """ 82 | if self.url_rule is not None: 83 | return self.url_rule.endpoint 84 | else: 85 | return None 86 | 87 | @property 88 | def blueprint(self) -> str | None: 89 | """Returns the blueprint the matched endpoint belongs to. 90 | 91 | This can be None if the request has not been matched or the 92 | endpoint is not in a blueprint. 93 | """ 94 | if self.endpoint is not None and "." in self.endpoint: 95 | return self.endpoint.rsplit(".", 1)[0] 96 | else: 97 | return None 98 | 99 | @property 100 | def blueprints(self) -> list[str]: 101 | """Return the names of the current blueprints. 102 | The returned list is ordered from the current blueprint, 103 | upwards through parent blueprints. 104 | """ 105 | # Avoid circular import 106 | from ..helpers import _split_blueprint_path 107 | 108 | if self.blueprint is not None: 109 | return _split_blueprint_path(self.blueprint) 110 | else: 111 | return [] 112 | 113 | @property 114 | def script_root(self) -> str: 115 | return self.root_path 116 | 117 | @property 118 | def url_root(self) -> str: 119 | return self.root_url 120 | -------------------------------------------------------------------------------- /src/quart/wrappers/websocket.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from typing import Any 5 | from typing import AnyStr 6 | from typing import Callable 7 | 8 | from hypercorn.typing import WebsocketScope 9 | from werkzeug.datastructures import Headers 10 | 11 | from .base import BaseRequestWebsocket 12 | 13 | 14 | class Websocket(BaseRequestWebsocket): 15 | def __init__( 16 | self, 17 | path: str, 18 | query_string: bytes, 19 | scheme: str, 20 | headers: Headers, 21 | root_path: str, 22 | http_version: str, 23 | subprotocols: list[str], 24 | receive: Callable, 25 | send: Callable, 26 | accept: Callable, 27 | close: Callable, 28 | scope: WebsocketScope, 29 | ) -> None: 30 | """Create a request object. 31 | 32 | Arguments: 33 | path: The full unquoted path of the request. 34 | query_string: The raw bytes for the query string part. 35 | scheme: The scheme used for the request. 36 | headers: The request headers. 37 | root_path: The root path that should be prepended to all 38 | routes. 39 | http_version: The HTTP version of the request. 40 | subprotocols: The subprotocols requested. 41 | receive: Returns an awaitable of the current data 42 | accept: Idempotent callable to accept the websocket connection. 43 | scope: Underlying ASGI scope dictionary. 44 | """ 45 | super().__init__( 46 | "GET", scheme, path, query_string, headers, root_path, http_version, scope 47 | ) 48 | self._accept = accept 49 | self._close = close 50 | self._receive = receive 51 | self._send = send 52 | self._subprotocols = subprotocols 53 | 54 | @property 55 | def requested_subprotocols(self) -> list[str]: 56 | return self._subprotocols 57 | 58 | async def receive(self) -> AnyStr: 59 | await self.accept() 60 | return await self._receive() 61 | 62 | async def send(self, data: AnyStr) -> None: 63 | # Must allow for the event loop to act if the user has say 64 | # setup a tight loop sending data over a websocket (as in the 65 | # example). So yield via the sleep. 66 | await asyncio.sleep(0) 67 | await self.accept() 68 | await self._send(data) 69 | 70 | async def receive_json(self) -> Any: 71 | data = await self.receive() 72 | return self.json_module.loads(data) 73 | 74 | async def send_json(self, *args: Any, **kwargs: Any) -> None: 75 | if args and kwargs: 76 | raise TypeError( 77 | "jsonify() behavior undefined when passed both args and kwargs" 78 | ) 79 | elif len(args) == 1: 80 | data = args[0] 81 | else: 82 | data = args or kwargs 83 | 84 | raw = self.json_module.dumps(data) 85 | await self.send(raw) 86 | 87 | async def accept( 88 | self, headers: dict | Headers | None = None, subprotocol: str | None = None 89 | ) -> None: 90 | """Manually chose to accept the websocket connection. 91 | 92 | Arguments: 93 | headers: Additional headers to send with the acceptance 94 | response. 95 | subprotocol: The chosen subprotocol, optional. 96 | """ 97 | if headers is None: 98 | headers_ = Headers() 99 | else: 100 | headers_ = Headers(headers) 101 | await self._accept(headers_, subprotocol) 102 | 103 | async def close(self, code: int, reason: str = "") -> None: 104 | await self._close(code, reason) 105 | -------------------------------------------------------------------------------- /tests/assets/config.cfg: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | BOB=jeff 3 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypercorn.typing import HTTPScope 5 | from hypercorn.typing import WebsocketScope 6 | 7 | 8 | @pytest.fixture(name="http_scope") 9 | def _http_scope() -> HTTPScope: 10 | return { 11 | "type": "http", 12 | "asgi": {}, 13 | "http_version": "1.1", 14 | "method": "GET", 15 | "scheme": "https", 16 | "path": "/", 17 | "raw_path": b"/", 18 | "query_string": b"a=b", 19 | "root_path": "", 20 | "headers": [ 21 | (b"User-Agent", b"Hypercorn"), 22 | (b"X-Hypercorn", b"Hypercorn"), 23 | (b"Referer", b"hypercorn"), 24 | ], 25 | "client": ("127.0.0.1", 80), 26 | "server": None, 27 | "state": {}, # type: ignore[typeddict-item] 28 | "extensions": {}, 29 | } 30 | 31 | 32 | @pytest.fixture(name="websocket_scope") 33 | def _websocket_scope() -> WebsocketScope: 34 | return { 35 | "type": "websocket", 36 | "asgi": {}, 37 | "http_version": "1.1", 38 | "scheme": "https", 39 | "path": "/", 40 | "raw_path": b"/", 41 | "query_string": b"a=b", 42 | "root_path": "", 43 | "headers": [ 44 | (b"User-Agent", b"Hypercorn"), 45 | (b"X-Hypercorn", b"Hypercorn"), 46 | (b"Referer", b"hypercorn"), 47 | ], 48 | "client": ("127.0.0.1", 80), 49 | "server": None, 50 | "subprotocols": [], 51 | "state": {}, # type: ignore[typeddict-item] 52 | "extensions": {}, 53 | } 54 | -------------------------------------------------------------------------------- /tests/test_background_tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import time 5 | 6 | from quart import current_app 7 | from quart import Quart 8 | 9 | 10 | async def test_background_task() -> None: 11 | app = Quart(__name__) 12 | app.config["DATA"] = "data" 13 | 14 | data = None 15 | 16 | async def background() -> None: 17 | nonlocal data 18 | await asyncio.sleep(0.5) 19 | data = current_app.config["DATA"] 20 | 21 | @app.route("/") 22 | async def index() -> str: 23 | app.add_background_task(background) 24 | return "" 25 | 26 | async with app.test_app(): 27 | test_client = app.test_client() 28 | await test_client.get("/") 29 | 30 | assert data == "data" 31 | 32 | 33 | async def test_lifespan_background_task() -> None: 34 | app = Quart(__name__) 35 | app.config["DATA"] = "data" 36 | 37 | data = None 38 | 39 | async def background() -> None: 40 | nonlocal data 41 | await asyncio.sleep(0.5) 42 | data = current_app.config["DATA"] 43 | 44 | @app.before_serving 45 | async def startup() -> None: 46 | app.add_background_task(background) 47 | 48 | async with app.test_app(): 49 | pass 50 | 51 | assert data == "data" 52 | 53 | 54 | async def test_sync_background_task() -> None: 55 | app = Quart(__name__) 56 | app.config["DATA"] = "data" 57 | 58 | data = None 59 | 60 | def background() -> None: 61 | nonlocal data 62 | time.sleep(0.5) 63 | data = current_app.config["DATA"] 64 | 65 | @app.route("/") 66 | async def index() -> str: 67 | app.add_background_task(background) 68 | return "" 69 | 70 | async with app.test_app(): 71 | test_client = app.test_client() 72 | await test_client.get("/") 73 | 74 | assert data == "data" 75 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import tempfile 5 | from collections.abc import Generator 6 | from pathlib import Path 7 | from unittest.mock import Mock 8 | 9 | import pytest 10 | from _pytest.monkeypatch import MonkeyPatch 11 | from click.testing import CliRunner 12 | 13 | import quart.cli 14 | from quart.app import Quart 15 | from quart.cli import AppGroup 16 | from quart.cli import cli 17 | from quart.cli import load_dotenv 18 | from quart.cli import ScriptInfo 19 | 20 | 21 | @pytest.fixture(scope="module") 22 | def reset_env() -> None: 23 | os.environ.pop("QUART_DEBUG", None) 24 | 25 | 26 | @pytest.fixture(name="app") 27 | def loadable_app(monkeypatch: MonkeyPatch) -> Mock: 28 | app = Mock(spec=Quart) 29 | app.cli = AppGroup() 30 | module = Mock() 31 | module.app = app 32 | monkeypatch.setattr(quart.cli, "import_module", lambda _: module) 33 | return app 34 | 35 | 36 | @pytest.fixture(name="dev_app") 37 | def loadable_dev_app(app: Mock) -> Mock: 38 | app.debug = True 39 | return app 40 | 41 | 42 | @pytest.fixture(name="debug_env") 43 | def debug_env_patch(monkeypatch: MonkeyPatch) -> None: 44 | monkeypatch.setenv("QUART_DEBUG", "true") 45 | 46 | 47 | @pytest.fixture(name="no_debug_env") 48 | def no_debug_env_patch(monkeypatch: MonkeyPatch) -> None: 49 | monkeypatch.setenv("QUART_DEBUG", "false") 50 | 51 | 52 | @pytest.fixture(name="empty_cwd") 53 | def empty_cwd() -> Generator[Path, None, None]: 54 | directory = tempfile.TemporaryDirectory() 55 | cwd = os.getcwd() 56 | os.chdir(directory.name) 57 | 58 | yield Path(directory.name) 59 | 60 | os.chdir(cwd) 61 | directory.cleanup() 62 | 63 | 64 | def test_script_info_load_app(app: Mock) -> None: 65 | info = ScriptInfo("module:app") 66 | assert info.load_app() == app 67 | 68 | 69 | def test_version_command() -> None: 70 | runner = CliRunner() 71 | result = runner.invoke(cli, ["--version"]) 72 | assert "Quart" in result.output 73 | 74 | 75 | def test_run_command(app: Mock) -> None: 76 | runner = CliRunner() 77 | runner.invoke(cli, ["--app", "module:app", "run"]) 78 | app.run.assert_called_once_with( 79 | debug=False, 80 | host="127.0.0.1", 81 | port=5000, 82 | certfile=None, 83 | keyfile=None, 84 | use_reloader=False, 85 | ) 86 | 87 | 88 | def test_run_command_development_debug_disabled( 89 | dev_app: Mock, no_debug_env: None 90 | ) -> None: 91 | runner = CliRunner() 92 | runner.invoke(cli, ["--app", "module:app", "run"]) 93 | dev_app.run.assert_called_once_with( 94 | debug=False, 95 | host="127.0.0.1", 96 | port=5000, 97 | certfile=None, 98 | keyfile=None, 99 | use_reloader=False, 100 | ) 101 | 102 | 103 | def test_load_dotenv(empty_cwd: Path) -> None: 104 | value = "dotenv" 105 | with open(empty_cwd / ".env", "w", encoding="utf8") as env: 106 | env.write(f"TEST_ENV_VAR={value}\n") 107 | 108 | load_dotenv() 109 | 110 | assert os.environ.pop("TEST_ENV_VAR", None) == value 111 | 112 | 113 | def test_load_dotquartenv(empty_cwd: Path) -> None: 114 | value = "dotquartenv" 115 | with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env: 116 | env.write(f"TEST_ENV_VAR={value}\n") 117 | 118 | load_dotenv() 119 | 120 | assert os.environ.pop("TEST_ENV_VAR", None) == value 121 | 122 | 123 | def test_load_dotenv_beats_dotquartenv(empty_cwd: Path) -> None: 124 | env_value = "dotenv" 125 | quartenv_value = "dotquartenv" 126 | 127 | with open(empty_cwd / ".env", "w", encoding="utf8") as env: 128 | env.write(f"TEST_ENV_VAR={env_value}\n") 129 | with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env: 130 | env.write(f"TEST_ENV_VAR={quartenv_value}\n") 131 | 132 | load_dotenv() 133 | 134 | assert os.environ.pop("TEST_ENV_VAR", None) == env_value 135 | -------------------------------------------------------------------------------- /tests/test_debug.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from quart import Quart 4 | from quart.debug import traceback_response 5 | 6 | 7 | async def test_debug() -> None: 8 | app = Quart(__name__) 9 | async with app.test_request_context("/"): 10 | response = await traceback_response(Exception("Unique error")) 11 | 12 | assert response.status_code == 500 13 | assert b"Unique error" in (await response.get_data()) 14 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from http import HTTPStatus 4 | 5 | import pytest 6 | from werkzeug.exceptions import abort 7 | from werkzeug.exceptions import HTTPException 8 | 9 | from quart import Response 10 | 11 | 12 | @pytest.mark.parametrize("status", [400, HTTPStatus.BAD_REQUEST]) 13 | def test_abort(status: int | HTTPStatus) -> None: 14 | with pytest.raises(HTTPException) as exc_info: 15 | abort(status) 16 | assert exc_info.value.get_response().status_code == 400 17 | 18 | 19 | def test_abort_with_response() -> None: 20 | with pytest.raises(HTTPException) as exc_info: 21 | abort(Response("Message", 205)) 22 | assert exc_info.value.get_response().status_code == 205 23 | -------------------------------------------------------------------------------- /tests/test_formparser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from werkzeug.exceptions import RequestEntityTooLarge 5 | 6 | from quart.formparser import MultiPartParser 7 | from quart.wrappers.request import Body 8 | 9 | 10 | async def test_multipart_max_form_memory_size() -> None: 11 | """max_form_memory_size is tracked across multiple data events.""" 12 | data = b"--bound\r\nContent-Disposition: form-field; name=a\r\n\r\n" 13 | data += b"a" * 15 + b"\r\n--bound--" 14 | body = Body(None, None) 15 | body.set_result(data) 16 | # The buffer size is less than the max size, so multiple data events will be 17 | # returned. The field size is greater than the max. 18 | parser = MultiPartParser(max_form_memory_size=10, buffer_size=5) 19 | 20 | with pytest.raises(RequestEntityTooLarge): 21 | await parser.parse(body, b"bound", 0) 22 | -------------------------------------------------------------------------------- /tests/test_routing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | import pytest 6 | from hypercorn.typing import HTTPScope 7 | from werkzeug.datastructures import Headers 8 | 9 | from quart.routing import QuartMap 10 | from quart.testing import no_op_push 11 | from quart.wrappers.request import Request 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "server_name, warns", 16 | [("localhost", False), ("quart.com", True)], 17 | ) 18 | async def test_bind_warning( 19 | server_name: str, warns: bool, http_scope: HTTPScope 20 | ) -> None: 21 | map_ = QuartMap(host_matching=False) 22 | request = Request( 23 | "GET", 24 | "http", 25 | "/", 26 | b"", 27 | Headers([("host", "Localhost")]), 28 | "", 29 | "1.1", 30 | http_scope, 31 | send_push_promise=no_op_push, 32 | ) 33 | 34 | if warns: 35 | with pytest.warns(UserWarning): 36 | map_.bind_to_request(request, subdomain=None, server_name=server_name) 37 | else: 38 | with warnings.catch_warnings(): 39 | warnings.simplefilter("error") 40 | map_.bind_to_request(request, subdomain=None, server_name=server_name) 41 | -------------------------------------------------------------------------------- /tests/test_sessions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from http.cookies import SimpleCookie 4 | 5 | from hypercorn.typing import HTTPScope 6 | from werkzeug.datastructures import Headers 7 | 8 | from quart.app import Quart 9 | from quart.sessions import SecureCookieSession 10 | from quart.sessions import SecureCookieSessionInterface 11 | from quart.testing import no_op_push 12 | from quart.wrappers import Request 13 | from quart.wrappers import Response 14 | 15 | 16 | async def test_secure_cookie_session_interface_open_session( 17 | http_scope: HTTPScope, 18 | ) -> None: 19 | session = SecureCookieSession() 20 | session["something"] = "else" 21 | interface = SecureCookieSessionInterface() 22 | app = Quart(__name__) 23 | app.secret_key = "secret" 24 | response = Response("") 25 | await interface.save_session(app, session, response) 26 | request = Request( 27 | "GET", 28 | "http", 29 | "/", 30 | b"", 31 | Headers(), 32 | "", 33 | "1.1", 34 | http_scope, 35 | send_push_promise=no_op_push, 36 | ) 37 | request.headers["Cookie"] = response.headers["Set-Cookie"] 38 | new_session = await interface.open_session(app, request) 39 | assert new_session == session 40 | 41 | 42 | async def test_secure_cookie_session_interface_save_session() -> None: 43 | session = SecureCookieSession() 44 | session["something"] = "else" 45 | interface = SecureCookieSessionInterface() 46 | app = Quart(__name__) 47 | app.secret_key = "secret" 48 | response = Response("") 49 | await interface.save_session(app, session, response) 50 | cookies: SimpleCookie = SimpleCookie() 51 | cookies.load(response.headers["Set-Cookie"]) 52 | cookie = cookies[app.config["SESSION_COOKIE_NAME"]] 53 | assert cookie["path"] == interface.get_cookie_path(app) 54 | assert cookie["httponly"] == "" if not interface.get_cookie_httponly(app) else True 55 | assert cookie["secure"] == "" if not interface.get_cookie_secure(app) else True 56 | assert cookie["samesite"] == (interface.get_cookie_samesite(app) or "") 57 | assert cookie["domain"] == (interface.get_cookie_domain(app) or "") 58 | assert cookie["expires"] == (interface.get_expiration_time(app, session) or "") 59 | assert response.headers["Vary"] == "Cookie" 60 | 61 | 62 | async def _save_session(session: SecureCookieSession) -> Response: 63 | interface = SecureCookieSessionInterface() 64 | app = Quart(__name__) 65 | app.secret_key = "secret" 66 | response = Response("") 67 | await interface.save_session(app, session, response) 68 | return response 69 | 70 | 71 | async def test_secure_cookie_session_interface_save_session_no_modification() -> None: 72 | session = SecureCookieSession() 73 | session["something"] = "else" 74 | session.modified = False 75 | response = await _save_session(session) 76 | assert response.headers.get("Set-Cookie") is None 77 | 78 | 79 | async def test_secure_cookie_session_interface_save_session_no_access() -> None: 80 | session = SecureCookieSession() 81 | session["something"] = "else" 82 | session.accessed = False 83 | session.modified = False 84 | response = await _save_session(session) 85 | assert response.headers.get("Set-Cookie") is None 86 | assert response.headers.get("Vary") is None 87 | -------------------------------------------------------------------------------- /tests/test_static_hosting.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from quart.app import Quart 6 | 7 | 8 | async def test_host_matching() -> None: 9 | app = Quart(__name__, static_folder="./assets", static_url_path="/static") 10 | 11 | test_client = app.test_client() 12 | 13 | response = await test_client.get("/static/config.cfg") 14 | assert response.status_code == 200 15 | data = await response.get_data(as_text=False) 16 | expected_data = (Path(__file__).parent / "assets/config.cfg").read_bytes() 17 | assert data == expected_data 18 | 19 | response = await test_client.get("/static/foo") 20 | assert response.status_code == 404 21 | 22 | # Should not be able to escape ! 23 | response = await test_client.get("/static/../foo") 24 | assert response.status_code == 404 25 | 26 | response = await test_client.get("/static/../assets/config.cfg") 27 | assert response.status_code == 404 28 | 29 | # Non-escaping path with .. 30 | response = await test_client.get("/static/foo/../config.cfg") 31 | assert response.status_code == 200 32 | data = await response.get_data(as_text=False) 33 | assert data == expected_data 34 | -------------------------------------------------------------------------------- /tests/test_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import threading 4 | from collections.abc import Generator 5 | 6 | import pytest 7 | 8 | from quart import Quart 9 | from quart import request 10 | from quart import ResponseReturnValue 11 | 12 | 13 | @pytest.fixture(name="app") 14 | def _app() -> Quart: 15 | app = Quart(__name__) 16 | 17 | @app.route("/", methods=["GET", "POST"]) 18 | def index() -> ResponseReturnValue: 19 | return request.method 20 | 21 | @app.route("/gen") 22 | def gen() -> ResponseReturnValue: 23 | def _gen() -> Generator[bytes, None, None]: 24 | yield b"%d" % threading.current_thread().ident 25 | for _ in range(2): 26 | yield b"b" 27 | 28 | return _gen(), 200 29 | 30 | return app 31 | 32 | 33 | async def test_sync_request_context(app: Quart) -> None: 34 | test_client = app.test_client() 35 | response = await test_client.get("/") 36 | assert b"GET" in (await response.get_data()) 37 | response = await test_client.post("/") 38 | assert b"POST" in (await response.get_data()) 39 | 40 | 41 | async def test_sync_generator(app: Quart) -> None: 42 | test_client = app.test_client() 43 | response = await test_client.get("/gen") 44 | result = await response.get_data() 45 | assert result[-2:] == b"bb" 46 | assert int(result[:-2]) != threading.current_thread().ident 47 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from werkzeug.datastructures import Headers 4 | 5 | from quart.utils import decode_headers 6 | from quart.utils import encode_headers 7 | 8 | 9 | def test_encode_headers() -> None: 10 | assert encode_headers(Headers({"Foo": "Bar"})) == [(b"foo", b"Bar")] 11 | 12 | 13 | def test_decode_headers() -> None: 14 | assert decode_headers([(b"foo", b"Bar")]) == Headers({"Foo": "Bar"}) 15 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Callable 5 | 6 | import pytest 7 | 8 | from quart import Quart 9 | from quart import request 10 | from quart import ResponseReturnValue 11 | from quart.views import MethodView 12 | from quart.views import View 13 | 14 | 15 | @pytest.fixture 16 | def app() -> Quart: 17 | app = Quart(__name__) 18 | return app 19 | 20 | 21 | async def test_view(app: Quart) -> None: 22 | class Views(View): 23 | methods = ["GET", "POST"] 24 | 25 | async def dispatch_request( 26 | self, *args: Any, **kwargs: Any 27 | ) -> ResponseReturnValue: 28 | return request.method 29 | 30 | app.add_url_rule("/", view_func=Views.as_view("simple")) 31 | 32 | test_client = app.test_client() 33 | response = await test_client.get("/") 34 | assert "GET" == (await response.get_data(as_text=True)) 35 | response = await test_client.put("/") 36 | assert response.status_code == 405 37 | 38 | 39 | async def test_method_view(app: Quart) -> None: 40 | class Views(MethodView): 41 | async def get(self) -> ResponseReturnValue: 42 | return "GET" 43 | 44 | async def post(self) -> ResponseReturnValue: 45 | return "POST" 46 | 47 | app.add_url_rule("/", view_func=Views.as_view("simple")) 48 | 49 | test_client = app.test_client() 50 | response = await test_client.get("/") 51 | assert "GET" == (await response.get_data(as_text=True)) 52 | response = await test_client.post("/") 53 | assert "POST" == (await response.get_data(as_text=True)) 54 | 55 | 56 | async def test_view_decorators(app: Quart) -> None: 57 | def decorate_status_code(func: Callable) -> Callable: 58 | async def wrapper(*args: Any, **kwargs: Any) -> ResponseReturnValue: 59 | response = await func(*args, **kwargs) 60 | return response, 201 61 | 62 | return wrapper 63 | 64 | class Views(View): 65 | decorators = [decorate_status_code] 66 | methods = ["GET"] 67 | 68 | async def dispatch_request( 69 | self, *args: Any, **kwargs: Any 70 | ) -> ResponseReturnValue: 71 | return request.method 72 | 73 | app.add_url_rule("/", view_func=Views.as_view("simple")) 74 | 75 | test_client = app.test_client() 76 | response = await test_client.get("/") 77 | assert response.status_code == 201 78 | -------------------------------------------------------------------------------- /tests/wrappers/test_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from base64 import b64encode 4 | 5 | import pytest 6 | from hypercorn.typing import HTTPScope 7 | from werkzeug.datastructures import Headers 8 | 9 | from quart.wrappers.base import BaseRequestWebsocket 10 | 11 | 12 | def test_basic_authorization(http_scope: HTTPScope) -> None: 13 | headers = Headers() 14 | headers["Authorization"] = "Basic {}".format( 15 | b64encode(b"identity:secret").decode("ascii") 16 | ) 17 | request = BaseRequestWebsocket( 18 | "GET", "http", "/", b"", headers, "", "1.1", http_scope 19 | ) 20 | auth = request.authorization 21 | assert auth.username == "identity" 22 | assert auth.password == "secret" 23 | 24 | 25 | def test_digest_authorization(http_scope: HTTPScope) -> None: 26 | headers = Headers() 27 | headers["Authorization"] = ( 28 | "Digest " 29 | 'username="identity", ' 30 | 'realm="realm@rea.lm", ' 31 | 'nonce="abcd1234", ' 32 | 'uri="/path", ' 33 | 'response="abcd1235", ' 34 | 'opaque="abcd1236"' 35 | ) 36 | request = BaseRequestWebsocket( 37 | "GET", "http", "/", b"", headers, "", "1.1", http_scope 38 | ) 39 | auth = request.authorization 40 | assert auth.username == "identity" 41 | assert auth.realm == "realm@rea.lm" 42 | assert auth.nonce == "abcd1234" 43 | assert auth.uri == "/path" 44 | assert auth.response == "abcd1235" 45 | assert auth.opaque == "abcd1236" 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "method, scheme, host, path, query_string," 50 | "expected_path, expected_full_path, expected_url, expected_base_url," 51 | "expected_url_root, expected_host_url", 52 | [ 53 | ( 54 | "GET", 55 | "http", 56 | "quart.com", 57 | "/", 58 | b"", 59 | "/", 60 | "/?", 61 | "http://quart.com/", 62 | "http://quart.com/", 63 | "http://quart.com/", 64 | "http://quart.com/", 65 | ), 66 | ( 67 | "GET", 68 | "http", 69 | "quart.com", 70 | "/", 71 | b"a=b", 72 | "/", 73 | "/?a=b", 74 | "http://quart.com/?a=b", 75 | "http://quart.com/", 76 | "http://quart.com/", 77 | "http://quart.com/", 78 | ), 79 | ( 80 | "GET", 81 | "https", 82 | "quart.com", 83 | "/branch/leaf", 84 | b"a=b", 85 | "/branch/leaf", 86 | "/branch/leaf?a=b", 87 | "https://quart.com/branch/leaf?a=b", 88 | "https://quart.com/branch/leaf", 89 | "https://quart.com/", 90 | "https://quart.com/", 91 | ), 92 | ], 93 | ) 94 | def test_url_structure( 95 | method: str, 96 | scheme: str, 97 | host: str, 98 | path: str, 99 | query_string: bytes, 100 | expected_path: str, 101 | expected_full_path: str, 102 | expected_url: str, 103 | expected_base_url: str, 104 | expected_url_root: str, 105 | expected_host_url: str, 106 | http_scope: HTTPScope, 107 | ) -> None: 108 | base_request_websocket = BaseRequestWebsocket( 109 | method, 110 | scheme, 111 | path, 112 | query_string, 113 | Headers({"host": host}), 114 | "", 115 | "1.1", 116 | http_scope, 117 | ) 118 | 119 | assert base_request_websocket.path == expected_path 120 | assert base_request_websocket.query_string == query_string 121 | assert base_request_websocket.full_path == expected_full_path 122 | assert base_request_websocket.url == expected_url 123 | assert base_request_websocket.base_url == expected_base_url 124 | assert base_request_websocket.url_root == expected_url_root 125 | assert base_request_websocket.host_url == expected_host_url 126 | assert base_request_websocket.host == host 127 | assert base_request_websocket.method == method 128 | assert base_request_websocket.scheme == scheme 129 | assert base_request_websocket.is_secure == scheme.endswith("s") 130 | 131 | 132 | def test_query_string(http_scope: HTTPScope) -> None: 133 | base_request_websocket = BaseRequestWebsocket( 134 | "GET", 135 | "http", 136 | "/", 137 | b"a=b&a=c&f", 138 | Headers({"host": "localhost"}), 139 | "", 140 | "1.1", 141 | http_scope, 142 | ) 143 | assert base_request_websocket.query_string == b"a=b&a=c&f" 144 | assert base_request_websocket.args.getlist("a") == ["b", "c"] 145 | assert base_request_websocket.args["f"] == "" 146 | -------------------------------------------------------------------------------- /tests/wrappers/test_request.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from urllib.parse import urlencode 5 | 6 | import pytest 7 | from hypercorn.typing import HTTPScope 8 | from werkzeug.datastructures import Headers 9 | from werkzeug.exceptions import RequestEntityTooLarge 10 | from werkzeug.exceptions import RequestTimeout 11 | 12 | from quart.testing import no_op_push 13 | from quart.wrappers.request import Body 14 | from quart.wrappers.request import Request 15 | 16 | 17 | async def _fill_body(body: Body, semaphore: asyncio.Semaphore, limit: int) -> None: 18 | for number in range(limit): 19 | body.append(b"%d" % number) 20 | await semaphore.acquire() 21 | body.set_complete() 22 | 23 | 24 | async def test_full_body() -> None: 25 | body = Body(None, None) 26 | limit = 3 27 | semaphore = asyncio.Semaphore(limit) 28 | asyncio.ensure_future(_fill_body(body, semaphore, limit)) 29 | assert b"012" == await body 30 | 31 | 32 | async def test_body_streaming() -> None: 33 | body = Body(None, None) 34 | limit = 3 35 | semaphore = asyncio.Semaphore(0) 36 | asyncio.ensure_future(_fill_body(body, semaphore, limit)) 37 | index = 0 38 | async for data in body: 39 | semaphore.release() 40 | assert data == b"%d" % index 41 | index += 1 42 | assert b"" == await body 43 | 44 | 45 | async def test_body_stream_single_chunk() -> None: 46 | body = Body(None, None) 47 | body.append(b"data") 48 | body.set_complete() 49 | 50 | async def _check_data() -> None: 51 | async for data in body: 52 | assert data == b"data" 53 | 54 | await asyncio.wait_for(_check_data(), 1) 55 | 56 | 57 | async def test_body_streaming_no_data() -> None: 58 | body = Body(None, None) 59 | semaphore = asyncio.Semaphore(0) 60 | asyncio.ensure_future(_fill_body(body, semaphore, 0)) 61 | async for _ in body: # noqa: F841 62 | raise AssertionError("Should not reach this line") 63 | assert b"" == await body 64 | 65 | 66 | async def test_body_exceeds_max_content_length() -> None: 67 | max_content_length = 5 68 | body = Body(None, max_content_length) 69 | body.append(b" " * (max_content_length + 1)) 70 | with pytest.raises(RequestEntityTooLarge): 71 | await body 72 | 73 | 74 | async def test_request_exceeds_max_content_length(http_scope: HTTPScope) -> None: 75 | max_content_length = 5 76 | headers = Headers() 77 | headers["Content-Length"] = str(max_content_length + 1) 78 | request = Request( 79 | "POST", 80 | "http", 81 | "/", 82 | b"", 83 | headers, 84 | "", 85 | "1.1", 86 | http_scope, 87 | max_content_length=max_content_length, 88 | send_push_promise=no_op_push, 89 | ) 90 | with pytest.raises(RequestEntityTooLarge): 91 | await request.get_data() 92 | 93 | 94 | async def test_request_get_data_timeout(http_scope: HTTPScope) -> None: 95 | request = Request( 96 | "POST", 97 | "http", 98 | "/", 99 | b"", 100 | Headers(), 101 | "", 102 | "1.1", 103 | http_scope, 104 | body_timeout=1, 105 | send_push_promise=no_op_push, 106 | ) 107 | with pytest.raises(RequestTimeout): 108 | await request.get_data() 109 | 110 | 111 | @pytest.mark.parametrize( 112 | "method, expected", 113 | [("GET", ["b", "c"]), ("POST", ["b", "c", "d"])], 114 | ) 115 | async def test_request_values( 116 | method: str, expected: list[str], http_scope: HTTPScope 117 | ) -> None: 118 | request = Request( 119 | method, 120 | "http", 121 | "/", 122 | b"a=b&a=c", 123 | Headers( 124 | {"host": "quart.com", "Content-Type": "application/x-www-form-urlencoded"} 125 | ), 126 | "", 127 | "1.1", 128 | http_scope, 129 | send_push_promise=no_op_push, 130 | ) 131 | request.body.append(urlencode({"a": "d"}).encode()) 132 | request.body.set_complete() 133 | assert (await request.values).getlist("a") == expected 134 | 135 | 136 | async def test_request_send_push_promise(http_scope: HTTPScope) -> None: 137 | push_promise: tuple[str, Headers] = None 138 | 139 | async def _push(path: str, headers: Headers) -> None: 140 | nonlocal push_promise 141 | push_promise = (path, headers) 142 | 143 | request = Request( 144 | "GET", 145 | "http", 146 | "/", 147 | b"a=b&a=c", 148 | Headers( 149 | { 150 | "host": "quart.com", 151 | "Content-Type": "application/x-www-form-urlencoded", 152 | "Accept": "*/*", 153 | "Accept-Encoding": "gzip", 154 | "User-Agent": "quart", 155 | } 156 | ), 157 | "", 158 | "2", 159 | http_scope, 160 | send_push_promise=_push, 161 | ) 162 | await request.send_push_promise("/") 163 | assert push_promise[0] == "/" 164 | valid_headers = {"Accept": "*/*", "Accept-Encoding": "gzip", "User-Agent": "quart"} 165 | assert len(push_promise[1]) == len(valid_headers) 166 | for name, value in valid_headers.items(): 167 | assert push_promise[1][name] == value 168 | --------------------------------------------------------------------------------