├── .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 ├── artwork └── logo.png ├── docs ├── Makefile ├── _static │ ├── logo.png │ └── logo_short.png ├── 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 ├── requirements ├── build.in ├── build.txt ├── dev.in ├── dev.txt ├── docs.in ├── docs.txt ├── tests.in ├── tests.txt ├── typing.in └── typing.txt ├── 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 └── tox.ini /.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 | 12 | 13 | 19 | 20 | 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 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 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 | concurrency: 14 | group: lock 15 | jobs: 16 | lock: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 20 | with: 21 | issue-inactive-days: 14 22 | pr-inactive-days: 14 23 | -------------------------------------------------------------------------------- /.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: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 12 | with: 13 | python-version: 3.x 14 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 15 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 16 | if: ${{ !cancelled() }} 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | hash: ${{ steps.hash.outputs.hash }} 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 14 | with: 15 | python-version: '3.x' 16 | cache: pip 17 | cache-dependency-path: requirements*/*.txt 18 | - run: pip install -r requirements/build.txt 19 | # Use the commit date instead of the current date during the build. 20 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 21 | - run: python -m build 22 | # Generate hashes used for provenance. 23 | - name: generate hash 24 | id: hash 25 | run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT 26 | - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 27 | with: 28 | path: ./dist 29 | provenance: 30 | needs: [build] 31 | permissions: 32 | actions: read 33 | id-token: write 34 | contents: write 35 | # Can't pin with hash due to how this workflow works. 36 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 37 | with: 38 | base64-subjects: ${{ needs.build.outputs.hash }} 39 | create-release: 40 | # Upload the sdist, wheels, and provenance to a GitHub release. They remain 41 | # available as build artifacts for a while as well. 42 | needs: [provenance] 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | steps: 47 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 48 | - name: create release 49 | run: > 50 | gh release create --draft --repo ${{ github.repository }} 51 | ${{ github.ref_name }} 52 | *.intoto.jsonl/* artifact/* 53 | env: 54 | GH_TOKEN: ${{ github.token }} 55 | publish-pypi: 56 | needs: [provenance] 57 | # Wait for approval before attempting to upload to PyPI. This allows reviewing the 58 | # files in the draft release. 59 | environment: 60 | name: publish 61 | url: https://pypi.org/project/Quart/${{ github.ref_name }} 62 | runs-on: ubuntu-latest 63 | permissions: 64 | id-token: write 65 | steps: 66 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 67 | - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 68 | with: 69 | packages-dir: artifact/ 70 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main, stable] 5 | paths-ignore: ['docs/**', '*.md', '*.rst'] 6 | pull_request: 7 | paths-ignore: [ 'docs/**', '*.md', '*.rst' ] 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: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 26 | with: 27 | python-version: ${{ matrix.python }} 28 | allow-prereleases: true 29 | cache: pip 30 | cache-dependency-path: requirements*/*.txt 31 | - run: pip install tox 32 | - run: 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: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 38 | with: 39 | python-version: '3.x' 40 | cache: pip 41 | cache-dependency-path: requirements*/*.txt 42 | - name: cache mypy 43 | uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 44 | with: 45 | path: ./.mypy_cache 46 | key: mypy|${{ hashFiles('pyproject.toml') }} 47 | - run: pip install tox 48 | - run: tox run -e typing 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .venv*/ 4 | venv*/ 5 | __pycache__/ 6 | dist/ 7 | .coverage* 8 | htmlcov/ 9 | .tox/ 10 | docs/reference/source 11 | docs/_build/ 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.7.3 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v5.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: debug-statements 12 | - id: fix-byte-order-marker 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: '3.12' 6 | python: 7 | install: 8 | - requirements: requirements/docs.txt 9 | - method: pip 10 | path: . 11 | sphinx: 12 | builder: html 13 | fail_on_warning: false 14 | -------------------------------------------------------------------------------- /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 | # Quart 2 | 3 | ![](https://raw.githubusercontent.com/pallets/quart/main/artwork/logo.png) 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 | -------------------------------------------------------------------------------- /artwork/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/artwork/logo.png -------------------------------------------------------------------------------- /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/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/logo_short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/docs/_static/logo_short.png -------------------------------------------------------------------------------- /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 | "external_links": [ 33 | {"name": "Source code", "url": "https://github.com/pallets/quart"}, 34 | {"name": "Issues", "url": "https://github.com/pallets/quart/issues"}, 35 | ], 36 | "icon_links": [ 37 | { 38 | "name": "Github", 39 | "url": "https://github.com/pallets/quart", 40 | "icon": "fab fa-github", 41 | }, 42 | ], 43 | } 44 | html_static_path = ["_static"] 45 | html_logo = "_static/logo_short.png" 46 | 47 | 48 | def run_apidoc(_): 49 | # generate API documentation via sphinx-apidoc 50 | # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html 51 | base_path = os.path.abspath(os.path.dirname(__file__)) 52 | apidoc.main( 53 | [ 54 | "-f", 55 | "-e", 56 | "-o", 57 | f"{base_path}/reference/source", 58 | f"{base_path}/../src/quart", 59 | f"{base_path}/../src/quart/datastructures.py", 60 | ] 61 | ) 62 | 63 | 64 | def setup(app): 65 | app.connect("builder-inited", run_apidoc) 66 | -------------------------------------------------------------------------------- /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 | `_ 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 | `_ 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 | `_ 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 `_. 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 `_ Secure cookie 10 | sessions, allows login, authentication and logout. 11 | - `Quart-Babel `_ Implements i18n and l10n support for Quart. 12 | - `Quart-Bcrypt `_ Provides bcrypt hashing utilities for your application. 13 | - `Quart-compress `_ 14 | compress your application's responses with gzip. 15 | - `Quart-compress2 16 | `_ A package to 17 | compress responses in your Quart app with gzip . 18 | - `Quart-CORS `_ Cross Origin 19 | Resource Sharing (access control) support. 20 | - `Quart-DB `_ Managed 21 | connection(s) to postgresql database(s). 22 | - `Quart-events `_ event 23 | broadcasting via WebSockets or SSE. 24 | - `Quart-Login `_ a 25 | port of Flask-Login to work natively with Quart. 26 | - `Quart-minify `_ minify 27 | quart response for HTML, JS, CSS and less. 28 | - `Quart-Mongo `_ Bridges Quart, Motor, and Odmantic to create a powerful MongoDB 29 | extension. 30 | - `Quart-Motor `_ Motor 31 | (MongoDB) support for Quart applications. 32 | - `Quart-OpenApi `_ RESTful 33 | API building. 34 | - `Quart-Keycloak `_ 35 | Support for Keycloak's OAuth2 OpenID Connect (OIDC). 36 | - `Quart-Rapidoc `_ API 37 | documentation from OpenAPI Specification. 38 | - `Quart-Rate-Limiter 39 | `_ Rate limiting 40 | support. 41 | - `Quart-Redis 42 | `_ Redis connection handling 43 | - `Webargs-Quart `_ Webargs 44 | parsing for Quart. 45 | - `Quart-SqlAlchemy `_ Quart-SQLAlchemy provides a simple wrapper for SQLAlchemy. 46 | - `Quart-WTF `_ Simple integration of Quart 47 | and WTForms. Including CSRF and file uploading. 48 | - `Quart-Schema `_ Schema 49 | validation and auto-generated API documentation. 50 | - `Quart-session `_ server 51 | side session support. 52 | - `Quart-LibreTranslate `_ Simple integration to 53 | use LibreTranslate with your Quart app. 54 | - `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 | `_ 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 ```` 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/') 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 | ````. Adapting the example above to, 48 | 49 | .. code-block:: python 50 | 51 | @app.route('/page/') 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 ``/`` 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/`` 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/') 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 `_ 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 | `_, 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 `_ templating engine, 7 | which is well `documented 8 | `_. 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(" 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 `__ 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 `__ 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 `_, 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 | `_ is the best choice. 80 | -------------------------------------------------------------------------------- /docs/how_to_guides/websockets.rst: -------------------------------------------------------------------------------- 1 | .. _websockets: 2 | 3 | Using websockets 4 | ================ 5 | 6 | To use a websocket declare a websocket function rather than a route 7 | function, like so, 8 | 9 | .. code-block:: python 10 | 11 | @app.websocket('/ws') 12 | async def ws(): 13 | while True: 14 | data = await websocket.receive() 15 | await websocket.send(data) 16 | 17 | ``websocket`` is a global like ``request`` and shares many of the same 18 | attributes such as ``headers``. 19 | 20 | Manually rejecting or accepting websockets 21 | ------------------------------------------ 22 | 23 | A websocket connection is created by accepting a HTTP upgrade request, 24 | however a server can choose to reject a websocket request. To do so 25 | just return from the websocket function as you would with a route function, 26 | 27 | .. code-block:: python 28 | 29 | @app.websocket('/ws') 30 | async def ws(): 31 | if ( 32 | websocket.authorization.username != USERNAME or 33 | websocket.authorization.password != PASSWORD 34 | ): 35 | return 'Invalid password', 403 # or abort(403) 36 | else: 37 | websocket.accept() # Automatically invoked by receive or send 38 | ... 39 | 40 | Sending and receiving independently 41 | ----------------------------------- 42 | 43 | The first example given requires the client to send a message for the 44 | server to respond. To send and receive independently requires 45 | independent tasks, 46 | 47 | .. code-block:: python 48 | 49 | async def sending(): 50 | while True: 51 | await websocket.send(...) 52 | 53 | async def receiving(): 54 | while True: 55 | data = await websocket.receive() 56 | ... 57 | 58 | @app.websocket('/ws') 59 | async def ws(): 60 | producer = asyncio.create_task(sending()) 61 | consumer = asyncio.create_task(receiving()) 62 | await asyncio.gather(producer, consumer) 63 | 64 | The gather line is critical, as without it the websocket function 65 | would return triggering Quart to send a HTTP response. 66 | 67 | Detecting disconnection 68 | ----------------------- 69 | 70 | When a client disconnects a ``CancelledError`` is raised, which can be 71 | caught to handle the disconnect, 72 | 73 | .. code-block:: python 74 | 75 | @app.websocket('/ws') 76 | async def ws(): 77 | try: 78 | while True: 79 | data = await websocket.receive() 80 | await websocket.send(data) 81 | except asyncio.CancelledError: 82 | # Handle disconnection here 83 | raise 84 | 85 | .. warning:: 86 | 87 | The ``CancelledError`` must be re-raised. 88 | 89 | Closing the connection 90 | ---------------------- 91 | 92 | An connection can be closed by awaiting the ``close`` method with the 93 | appropriate Websocket error code, 94 | 95 | .. code-block:: python 96 | 97 | @app.websocket('/ws') 98 | async def ws(): 99 | await websocket.accept() 100 | await websocket.close(1000) 101 | 102 | if the websocket is closed before it is accepted the server will 103 | respond with a 403 HTTP response. 104 | 105 | Testing websockets 106 | ------------------ 107 | 108 | To test a websocket route use the test_client like so, 109 | 110 | .. code-block:: python 111 | 112 | test_client = app.test_client() 113 | async with test_client.websocket('/ws/') as test_websocket: 114 | await test_websocket.send(data) 115 | result = await test_websocket.receive() 116 | 117 | If the websocket route returns a response the test_client will raise a 118 | :class:`~quart.testing.WebsocketResponseError` exception with a 119 | :attr:`~quart.testing.WebsocketResponseError.response` attribute. For 120 | example, 121 | 122 | .. code-block:: python 123 | 124 | test_client = app.test_client() 125 | try: 126 | async with test_client.websocket('/ws/') as test_websocket: 127 | await test_websocket.send(data) 128 | except WebsocketResponseError as error: 129 | assert error.response.status_code == 401 130 | 131 | Sending and receiving Bytes or String 132 | ------------------------------------- 133 | 134 | The WebSocket protocol allows for either bytes or strings to be sent 135 | with a frame marker indicating which. The 136 | :meth:`~quart.wrappers.request.Websocket.receive` method will return 137 | either ``bytes`` or ``str`` depending on what the client sent i.e. if 138 | the client sent a string it will be returned from the method. Equally 139 | you can send bytes or strings. 140 | 141 | Mixing websocket and HTTP routes 142 | -------------------------------- 143 | 144 | Quart allows for a route to be defined both as for websockets and for 145 | http requests. This allows responses to be sent depending upon the 146 | type of request (WebSocket upgrade or not). As so, 147 | 148 | .. code-block:: python 149 | 150 | @app.route("/ws") 151 | async def http(): 152 | return "A HTTP request" 153 | 154 | @app.websocket("/ws") 155 | async def ws(): 156 | ... # Use the WebSocket 157 | 158 | If the http definition is absent Quart will respond with a 400, Bad 159 | Request, response for requests to the missing route (rather than 160 | a 404). 161 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. title:: Quart documentation 4 | 5 | .. image:: _static/logo.png 6 | :width: 300px 7 | :alt: Quart logo 8 | :align: right 9 | 10 | Quart 11 | ===== 12 | 13 | Quart is a Fast Python web microframework. Using Quart you can, 14 | 15 | * write JSON APIs e.g. :ref:`a RESTful API`, 16 | * render and serve HTML e.g. :ref:`a blog`, 17 | * serve WebSockets e.g. :ref:`a simple chat`, 18 | * stream responses e.g. :ref:`serve video`, 19 | * all of the above in a single app, 20 | * or do pretty much anything over the HTTP or WebSocket protocols. 21 | 22 | With all of the above possible using asynchronous (asyncio) 23 | libraries/code or :ref:`synchronous` libraries/code. 24 | 25 | If you are, 26 | 27 | * new to Python then start by reading :ref:`installation` instructions, 28 | * new to Quart then try the :ref:`quickstart`, 29 | * new to asyncio see the :ref:`asyncio` guide, 30 | * migrating from Flask see :ref:`flask_migration`, 31 | * looking for a cheatsheet then look :ref:`here`. 32 | 33 | Quart is an asyncio reimplementation of the popular `Flask 34 | `_ microframework API. This means that if you 35 | understand Flask you understand Quart. See :ref:`flask_evolution` to 36 | learn more about how Quart builds on Flask. 37 | 38 | Like Flask Quart has an ecosystem of 39 | :ref:`extensions` for more specific needs. In 40 | addition a number of the Flask :ref:`extensions` 41 | work with Quart. 42 | 43 | Quart is developed on `Github `_. If 44 | you come across an issue, or have a feature request please open an 45 | `issue `_.If you want to 46 | contribute a fix or the feature-implementation please do (typo fixes 47 | welcome), by proposing a `merge request 48 | `_. If you want to 49 | ask for help try `on discord `_. 50 | 51 | .. note:: 52 | 53 | If you can't find documentation for what you are looking for here, 54 | remember that Quart is an implementation of the Flask API and 55 | hence the `Flask documentation `_ is 56 | a great source of help. Quart is also built on the `Jinja 57 | `_ template engine and the `Werkzeug 58 | `_ toolkit. 59 | 60 | The Flask documentation is so good that you may be better placed 61 | consulting it first then returning here to check how Quart 62 | differs. 63 | 64 | Tutorials 65 | --------- 66 | 67 | .. toctree:: 68 | :maxdepth: 2 69 | 70 | tutorials/index.rst 71 | 72 | How to guides 73 | ------------- 74 | 75 | .. toctree:: 76 | :maxdepth: 2 77 | 78 | how_to_guides/index.rst 79 | 80 | Discussion 81 | ---------- 82 | 83 | .. toctree:: 84 | :maxdepth: 2 85 | 86 | discussion/index.rst 87 | 88 | References 89 | ---------- 90 | 91 | .. toctree:: 92 | :maxdepth: 2 93 | 94 | reference/index 95 | license 96 | changes 97 | -------------------------------------------------------------------------------- /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/") # 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 `_. 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 `_ 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"{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 | >>> ['http://google.com', '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 | `_ 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 `_. 37 | 38 | Alternative ASGI Servers 39 | ------------------------ 40 | 41 | Alongside `Hypercorn `_, `Daphne 42 | `_, and `Uvicorn 43 | `_ 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 `_ 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 `_ 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 `_ 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 "] 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/b5593ca4c8c657564cdf2d35c9f0298fce63636b/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 `_ 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 "] 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 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /examples/blog/src/blog/templates/posts.html: -------------------------------------------------------------------------------- 1 |
2 | {% for post in posts %} 3 |
4 |

{{ post.title }}

5 |

{{ post.text|safe }}

6 |
7 | {% else %} 8 |

No posts available

9 | {% endfor %} 10 |
11 | -------------------------------------------------------------------------------- /examples/blog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/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"

Post

" in text 13 | assert b"

Text

" 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 `_ 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 "] 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 | 19 | 20 |
21 |
    22 | 23 |
    24 | 25 | 26 |
    27 |
    28 | -------------------------------------------------------------------------------- /examples/chat/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/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 `_ 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 "] 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/b5593ca4c8c657564cdf2d35c9f0298fce63636b/examples/video/src/video/static/video.mp4 -------------------------------------------------------------------------------- /examples/video/src/video/templates/index.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/video/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/quart/b5593ca4c8c657564cdf2d35c9f0298fce63636b/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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Quart" 3 | version = "0.20.0" 4 | description = "A Python ASGI web framework with the same API as Flask" 5 | readme = "README.md" 6 | license = {text = "MIT"} 7 | authors = [{name = "pgjones", email = "philip.graham.jones@googlemail.com"}] 8 | maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "Environment :: Web Environment", 12 | "Framework :: Flask", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python", 17 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 18 | "Topic :: Software Development :: Libraries :: Application Frameworks", 19 | "Typing :: Typed", 20 | ] 21 | requires-python = ">=3.9" 22 | dependencies = [ 23 | "aiofiles", 24 | "blinker>=1.6", 25 | "click>=8.0", 26 | "flask>=3.0", 27 | "hypercorn>=0.11.2", 28 | "importlib-metadata; python_version < '3.10'", 29 | "itsdangerous", 30 | "jinja2", 31 | "markupsafe", 32 | "typing-extensions; python_version < '3.10'", 33 | "werkzeug>=3.0", 34 | ] 35 | 36 | [project.urls] 37 | Donate = "https://palletsprojects.com/donate" 38 | Documentation = "https://quart.palletsprojects.com/" 39 | Changes = "https://quart.palletsprojects.com/en/latest/changes.html" 40 | Source = "https://github.com/pallets/quart/" 41 | Chat = "https://discord.gg/pallets" 42 | 43 | [project.optional-dependencies] 44 | dotenv = ["python-dotenv"] 45 | 46 | [project.scripts] 47 | quart = "quart.cli:main" 48 | 49 | [build-system] 50 | requires = ["flit-core<4"] 51 | build-backend = "flit_core.buildapi" 52 | 53 | [tool.flit.module] 54 | name = "quart" 55 | 56 | [tool.pytest.ini_options] 57 | addopts = "--no-cov-on-fail --showlocals --strict-markers" 58 | asyncio_default_fixture_loop_scope = "session" 59 | asyncio_mode = "auto" 60 | testpaths = ["tests"] 61 | 62 | [tool.coverage.run] 63 | branch = true 64 | source = ["quart", "tests"] 65 | 66 | [tool.coverage.paths] 67 | source = ["src", "*/site-packages"] 68 | 69 | [tool.mypy] 70 | python_version = "3.9" 71 | files = ["src/quart", "tests"] 72 | show_error_codes = true 73 | pretty = true 74 | strict = true 75 | # TODO fully satisfy strict mode and remove these customizations 76 | allow_redefinition = true 77 | disallow_any_generics = false 78 | disallow_untyped_calls = false 79 | implicit_reexport = true 80 | no_implicit_optional = true 81 | strict_optional = false 82 | warn_return_any = false 83 | 84 | [tool.pyright] 85 | pythonVersion = "3.9" 86 | include = ["src/quart", "tests"] 87 | typeCheckingMode = "basic" 88 | 89 | [tool.ruff] 90 | src = ["src"] 91 | fix = true 92 | show-fixes = true 93 | output-format = "full" 94 | 95 | [tool.ruff.lint] 96 | select = [ 97 | "B", # flake8-bugbear 98 | "E", # pycodestyle error 99 | "F", # pyflakes 100 | "FA", # flake8-future-annotations 101 | "I", # isort 102 | "N", # pep8-naming 103 | "UP", # pyupgrade 104 | "W", # pycodestyle warning 105 | ] 106 | 107 | [tool.ruff.lint.isort] 108 | force-single-line = true 109 | order-by-type = false 110 | -------------------------------------------------------------------------------- /requirements/build.in: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /requirements/build.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile build.in -o build.txt 3 | build==1.2.2.post1 4 | # via -r build.in 5 | packaging==24.2 6 | # via build 7 | pyproject-hooks==1.2.0 8 | # via build 9 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r docs.txt 2 | -r tests.txt 3 | -r typing.txt 4 | pre-commit 5 | tox 6 | tox-uv 7 | -------------------------------------------------------------------------------- /requirements/docs.in: -------------------------------------------------------------------------------- 1 | myst-parser 2 | pydata-sphinx-theme 3 | sphinx 4 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile docs.in -o docs.txt 3 | accessible-pygments==0.0.5 4 | # via pydata-sphinx-theme 5 | alabaster==1.0.0 6 | # via sphinx 7 | babel==2.16.0 8 | # via 9 | # pydata-sphinx-theme 10 | # sphinx 11 | beautifulsoup4==4.12.3 12 | # via pydata-sphinx-theme 13 | certifi==2024.8.30 14 | # via requests 15 | charset-normalizer==3.4.0 16 | # via requests 17 | docutils==0.21.2 18 | # via 19 | # myst-parser 20 | # pydata-sphinx-theme 21 | # sphinx 22 | idna==3.10 23 | # via requests 24 | imagesize==1.4.1 25 | # via sphinx 26 | jinja2==3.1.4 27 | # via 28 | # myst-parser 29 | # sphinx 30 | markdown-it-py==3.0.0 31 | # via 32 | # mdit-py-plugins 33 | # myst-parser 34 | markupsafe==3.0.2 35 | # via jinja2 36 | mdit-py-plugins==0.4.2 37 | # via myst-parser 38 | mdurl==0.1.2 39 | # via markdown-it-py 40 | myst-parser==4.0.0 41 | # via -r docs.in 42 | packaging==24.2 43 | # via sphinx 44 | pydata-sphinx-theme==0.16.0 45 | # via -r docs.in 46 | pygments==2.18.0 47 | # via 48 | # accessible-pygments 49 | # pydata-sphinx-theme 50 | # sphinx 51 | pyyaml==6.0.2 52 | # via myst-parser 53 | requests==2.32.3 54 | # via sphinx 55 | snowballstemmer==2.2.0 56 | # via sphinx 57 | soupsieve==2.6 58 | # via beautifulsoup4 59 | sphinx==8.1.3 60 | # via 61 | # -r docs.in 62 | # myst-parser 63 | # pydata-sphinx-theme 64 | sphinxcontrib-applehelp==2.0.0 65 | # via sphinx 66 | sphinxcontrib-devhelp==2.0.0 67 | # via sphinx 68 | sphinxcontrib-htmlhelp==2.1.0 69 | # via sphinx 70 | sphinxcontrib-jsmath==1.0.1 71 | # via sphinx 72 | sphinxcontrib-qthelp==2.0.0 73 | # via sphinx 74 | sphinxcontrib-serializinghtml==2.0.0 75 | # via sphinx 76 | typing-extensions==4.12.2 77 | # via pydata-sphinx-theme 78 | urllib3==2.2.3 79 | # via requests 80 | -------------------------------------------------------------------------------- /requirements/tests.in: -------------------------------------------------------------------------------- 1 | hypothesis 2 | pytest 3 | pytest-asyncio 4 | pytest-cov 5 | pytest-sugar 6 | python-dotenv 7 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile tests.in -o tests.txt 3 | attrs==24.2.0 4 | # via hypothesis 5 | coverage==7.6.5 6 | # via pytest-cov 7 | hypothesis==6.118.8 8 | # via -r tests.in 9 | iniconfig==2.0.0 10 | # via pytest 11 | packaging==24.2 12 | # via 13 | # pytest 14 | # pytest-sugar 15 | pluggy==1.5.0 16 | # via pytest 17 | pytest==8.3.3 18 | # via 19 | # -r tests.in 20 | # pytest-asyncio 21 | # pytest-cov 22 | # pytest-sugar 23 | pytest-asyncio==0.24.0 24 | # via -r tests.in 25 | pytest-cov==6.0.0 26 | # via -r tests.in 27 | pytest-sugar==1.0.0 28 | # via -r tests.in 29 | python-dotenv==1.0.1 30 | # via -r tests.in 31 | sortedcontainers==2.4.0 32 | # via hypothesis 33 | termcolor==2.5.0 34 | # via pytest-sugar 35 | -------------------------------------------------------------------------------- /requirements/typing.in: -------------------------------------------------------------------------------- 1 | mypy 2 | hypothesis 3 | pyright 4 | pytest 5 | pytest-asyncio 6 | types-aiofiles 7 | python-dotenv 8 | -------------------------------------------------------------------------------- /requirements/typing.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile typing.in -o typing.txt 3 | attrs==24.2.0 4 | # via hypothesis 5 | hypothesis==6.118.8 6 | # via -r typing.in 7 | iniconfig==2.0.0 8 | # via pytest 9 | mypy==1.13.0 10 | # via -r typing.in 11 | mypy-extensions==1.0.0 12 | # via mypy 13 | nodeenv==1.9.1 14 | # via pyright 15 | packaging==24.2 16 | # via pytest 17 | pluggy==1.5.0 18 | # via pytest 19 | pyright==1.1.389 20 | # via -r typing.in 21 | pytest==8.3.3 22 | # via 23 | # -r typing.in 24 | # pytest-asyncio 25 | pytest-asyncio==0.24.0 26 | # via -r typing.in 27 | python-dotenv==1.0.1 28 | # via -r typing.in 29 | sortedcontainers==2.4.0 30 | # via hypothesis 31 | types-aiofiles==24.1.0.20240626 32 | # via -r typing.in 33 | typing-extensions==4.12.2 34 | # via 35 | # mypy 36 | # pyright 37 | -------------------------------------------------------------------------------- /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 | 58 | 59 |

    {{ name }} {{ value }}

    60 |
      61 | {% for frame in frames %} 62 |
    • 63 |
      64 | File {{ frame.file }}, 65 | line {{ frame.line }}, in 66 |
      67 |
      68 | {% for line in frame.code[0] %} 69 |
      71 |
      {{ loop.index + frame.code[1] }}
      72 |
      {{ line }}
      73 |
      74 | {% endfor %} 75 |
      76 |
      77 | {% for name, repr in frame.locals.items() %} 78 |
      79 |
      {{ name }}
      80 |
      {{ repr }}
      81 |
      82 | {% endfor %} 83 |
      84 |
    • 85 | {% endfor %} 86 |
    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 = "" 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('/', 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('/', 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 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{13,12,11,10,9} 4 | style 5 | typing 6 | docs 7 | 8 | [testenv] 9 | package = wheel 10 | wheel_build_env = .pkg 11 | constrain_package_deps = true 12 | use_frozen_constraints = true 13 | deps = -r requirements/tests.txt 14 | commands = pytest -v --tb=short --basetemp={envtmpdir} --cov=quart {posargs} 15 | 16 | [testenv:style] 17 | deps = pre-commit 18 | skip_install = true 19 | commands = pre-commit run --all-files 20 | 21 | [testenv:typing] 22 | deps = -r requirements/typing.txt 23 | # TODO test with pyright as well 24 | commands = mypy 25 | 26 | [testenv:docs] 27 | deps = -r requirements/docs.txt 28 | # TODO enable -W and fix warnings 29 | commands = sphinx-build -E -b html docs docs/_build/html 30 | 31 | [testenv:update-actions] 32 | labels = update 33 | deps = gha-update 34 | skip_install = true 35 | commands = gha-update 36 | 37 | [testenv:update-pre_commit] 38 | labels = update 39 | deps = pre-commit 40 | skip_install = true 41 | commands = pre-commit autoupdate -j4 42 | 43 | [testenv:update-requirements] 44 | labels = update 45 | deps = uv 46 | skip_install = true 47 | change_dir = requirements 48 | commands = 49 | uv pip compile build.in -o build.txt -q {posargs:-U} 50 | uv pip compile docs.in -o docs.txt -q {posargs:-U} 51 | uv pip compile tests.in -o tests.txt -q {posargs:-U} 52 | uv pip compile typing.in -o typing.txt -q {posargs:-U} 53 | uv pip compile dev.in -o dev.txt -q {posargs:-U} 54 | --------------------------------------------------------------------------------