The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .github
    ├── ISSUE_TEMPLATE
    │   ├── bug-report.md
    │   ├── config.yml
    │   └── feature-request.md
    ├── pull_request_template.md
    └── workflows
    │   ├── lock.yaml
    │   ├── pre-commit.yaml
    │   ├── publish.yaml
    │   └── tests.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CHANGES.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── README.md
├── docs
    ├── Makefile
    ├── _static
    │   ├── quart-icon.svg
    │   ├── quart-logo.svg
    │   ├── quart-name-dark.svg
    │   └── quart-name.svg
    ├── changes.md
    ├── conf.py
    ├── discussion
    │   ├── async_compatibility.rst
    │   ├── background_tasks.rst
    │   ├── contexts.rst
    │   ├── design_choices.rst
    │   ├── dos_mitigations.rst
    │   ├── flask_evolution.rst
    │   ├── globals.rst
    │   ├── index.rst
    │   ├── python_versions.rst
    │   └── websockets_discussion.rst
    ├── how_to_guides
    │   ├── background_tasks.rst
    │   ├── blueprints.rst
    │   ├── command_line.rst
    │   ├── configuration.rst
    │   ├── developing.rst
    │   ├── disconnections.rst
    │   ├── event_loop.rst
    │   ├── flask_extensions.rst
    │   ├── flask_migration.rst
    │   ├── index.rst
    │   ├── json_encoding.rst
    │   ├── logging.rst
    │   ├── middleware.rst
    │   ├── quart_extensions.rst
    │   ├── request_body.rst
    │   ├── routing.rst
    │   ├── server_sent_events.rst
    │   ├── session_storage.rst
    │   ├── startup_shutdown.rst
    │   ├── streaming_response.rst
    │   ├── sync_code.rst
    │   ├── templating.rst
    │   ├── testing.rst
    │   ├── using_http2.rst
    │   └── websockets.rst
    ├── index.rst
    ├── license.md
    ├── make.bat
    ├── reference
    │   ├── api.rst
    │   ├── cheatsheet.rst
    │   ├── index.rst
    │   ├── logo.rst
    │   ├── response_values.rst
    │   └── versioning.rst
    └── tutorials
    │   ├── api_tutorial.rst
    │   ├── asyncio.rst
    │   ├── blog_tutorial.rst
    │   ├── chat_tutorial.rst
    │   ├── deployment.rst
    │   ├── index.rst
    │   ├── installation.rst
    │   ├── quickstart.rst
    │   └── video_tutorial.rst
├── examples
    ├── api
    │   ├── README.rst
    │   ├── pyproject.toml
    │   ├── src
    │   │   └── api
    │   │   │   └── __init__.py
    │   └── tests
    │   │   ├── __init__.py
    │   │   └── test_api.py
    ├── blog
    │   ├── README.rst
    │   ├── pyproject.toml
    │   ├── src
    │   │   └── blog
    │   │   │   ├── __init__.py
    │   │   │   ├── schema.sql
    │   │   │   └── templates
    │   │   │       ├── create.html
    │   │   │       └── posts.html
    │   └── tests
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   └── test_blog.py
    ├── chat
    │   ├── README.rst
    │   ├── pyproject.toml
    │   ├── src
    │   │   └── chat
    │   │   │   ├── __init__.py
    │   │   │   ├── broker.py
    │   │   │   └── templates
    │   │   │       └── index.html
    │   └── tests
    │   │   ├── __init__.py
    │   │   └── test_chat.py
    └── video
    │   ├── README.rst
    │   ├── pyproject.toml
    │   ├── src
    │       └── video
    │       │   ├── __init__.py
    │       │   ├── static
    │       │       └── video.mp4
    │       │   └── templates
    │       │       └── index.html
    │   └── tests
    │       ├── __init__.py
    │       └── test_video.py
├── pyproject.toml
├── src
    └── quart
    │   ├── __init__.py
    │   ├── __main__.py
    │   ├── app.py
    │   ├── asgi.py
    │   ├── blueprints.py
    │   ├── cli.py
    │   ├── config.py
    │   ├── ctx.py
    │   ├── datastructures.py
    │   ├── debug.py
    │   ├── formparser.py
    │   ├── globals.py
    │   ├── helpers.py
    │   ├── json
    │       ├── __init__.py
    │       ├── provider.py
    │       └── tag.py
    │   ├── logging.py
    │   ├── py.typed
    │   ├── routing.py
    │   ├── sessions.py
    │   ├── signals.py
    │   ├── templating.py
    │   ├── testing
    │       ├── __init__.py
    │       ├── app.py
    │       ├── client.py
    │       ├── connections.py
    │       └── utils.py
    │   ├── typing.py
    │   ├── utils.py
    │   ├── views.py
    │   └── wrappers
    │       ├── __init__.py
    │       ├── base.py
    │       ├── request.py
    │       ├── response.py
    │       └── websocket.py
├── tests
    ├── assets
    │   └── config.cfg
    ├── conftest.py
    ├── test_app.py
    ├── test_asgi.py
    ├── test_background_tasks.py
    ├── test_basic.py
    ├── test_blueprints.py
    ├── test_cli.py
    ├── test_ctx.py
    ├── test_debug.py
    ├── test_exceptions.py
    ├── test_formparser.py
    ├── test_helpers.py
    ├── test_routing.py
    ├── test_sessions.py
    ├── test_static_hosting.py
    ├── test_sync.py
    ├── test_templating.py
    ├── test_testing.py
    ├── test_utils.py
    ├── test_views.py
    └── wrappers
    │   ├── test_base.py
    │   ├── test_request.py
    │   └── test_response.py
└── uv.lock


/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | indent_style = space
 5 | indent_size = 4
 6 | insert_final_newline = true
 7 | trim_trailing_whitespace = true
 8 | end_of_line = lf
 9 | charset = utf-8
10 | max_line_length = 88
11 | 
12 | [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
13 | indent_size = 2
14 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Report a bug in Quart (not other projects which depend on Quart)
 4 | ---
 5 | 
 6 | <!--
 7 | This issue tracker is a tool to address bugs in Quart itself. Please use
 8 | GitHub Discussions or the Pallets Discord for questions about your own code.
 9 | 
10 | Replace this comment with a clear outline of what the bug is.
11 | -->
12 | 
13 | <!--
14 | Describe how to replicate the bug.
15 | 
16 | Include a minimal reproducible example that demonstrates the bug.
17 | Include the full traceback if there was an exception.
18 | -->
19 | 
20 | <!--
21 | Describe the expected behavior that should have happened but didn't.
22 | -->
23 | 
24 | Environment:
25 | 
26 | - Python version:
27 | - Quart version:
28 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
 1 | blank_issues_enabled: false
 2 | contact_links:
 3 |   - name: Security issue
 4 |     url: https://github.com/pallets/quart/security/advisories/new
 5 |     about: Do not report security issues publicly. Create a private advisory.
 6 |   - name: Questions
 7 |     url: https://github.com/pallets/quart/discussions/
 8 |     about: Ask questions about your own code on the Discussions tab.
 9 |   - name: Questions on
10 |     url: https://discord.gg/pallets
11 |     about: Ask questions about your own code on our Discord chat.
12 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: Suggest a new feature for Quart
 4 | ---
 5 | 
 6 | <!--
 7 | Replace this comment with a description of what the feature should do.
 8 | Include details such as links to relevant specs or previous discussions.
 9 | -->
10 | 
11 | <!--
12 | Replace this comment with an example of the problem which this feature
13 | would resolve. Is this problem solvable without changes to Quart, such
14 | as by subclassing or using an extension?
15 | -->
16 | 


--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
 1 | <!--
 2 | Before opening a PR, open a ticket describing the issue or feature the
 3 | PR will address. An issue is not required for fixing typos in
 4 | documentation, or other simple non-code changes.
 5 | 
 6 | Replace this comment with a description of the change. Describe how it
 7 | addresses the linked ticket.
 8 | -->
 9 | 
10 | <!--
11 | Link to relevant issues or previous PRs, one per line. Use "fixes" to
12 | automatically close an issue.
13 | 
14 | fixes #<issue number>
15 | -->
16 | 


--------------------------------------------------------------------------------
/.github/workflows/lock.yaml:
--------------------------------------------------------------------------------
 1 | name: Lock inactive closed issues
 2 | # Lock closed issues that have not received any further activity for two weeks.
 3 | # This does not close open issues, only humans may do that. It is easier to
 4 | # respond to new issues with fresh examples rather than continuing discussions
 5 | # on old issues.
 6 | 
 7 | on:
 8 |   schedule:
 9 |     - cron: '0 0 * * *'
10 | permissions:
11 |   issues: write
12 |   pull-requests: write
13 |   discussions: write
14 | concurrency:
15 |   group: lock
16 | jobs:
17 |   lock:
18 |     runs-on: ubuntu-latest
19 |     steps:
20 |       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
21 |         with:
22 |           issue-inactive-days: 14
23 |           pr-inactive-days: 14
24 |           discussion-inactive-days: 14
25 | 


--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yaml:
--------------------------------------------------------------------------------
 1 | name: pre-commit
 2 | on:
 3 |   pull_request:
 4 |   push:
 5 |     branches: [main, stable]
 6 | jobs:
 7 |   main:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
11 |       - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
12 |         with:
13 |           enable-cache: true
14 |           prune-cache: false
15 |       - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
16 |         id: setup-python
17 |         with:
18 |           python-version-file: pyproject.toml
19 |       - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
20 |         with:
21 |           path: ~/.cache/pre-commit
22 |           key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }}
23 |       - run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files
24 |       - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
25 |         if: ${{ !cancelled() }}
26 | 


--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
 1 | name: Publish
 2 | on:
 3 |   push:
 4 |     tags: ['*']
 5 | jobs:
 6 |   build:
 7 |     runs-on: ubuntu-latest
 8 |     steps:
 9 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
10 |       - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
11 |         with:
12 |           enable-cache: true
13 |           prune-cache: false
14 |       - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
15 |         with:
16 |           python-version-file: pyproject.toml
17 |       - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
18 |       - run: uv build
19 |       - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
20 |         with:
21 |           path: ./dist
22 |   create-release:
23 |     needs: [build]
24 |     runs-on: ubuntu-latest
25 |     permissions:
26 |       contents: write
27 |     steps:
28 |       - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
29 |       - name: create release
30 |         run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} artifact/*
31 |         env:
32 |           GH_TOKEN: ${{ github.token }}
33 |   publish-pypi:
34 |     needs: [build]
35 |     environment:
36 |       name: publish
37 |       url: https://pypi.org/project/Quart/${{ github.ref_name }}
38 |     runs-on: ubuntu-latest
39 |     permissions:
40 |       id-token: write
41 |     steps:
42 |       - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
43 |       - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
44 |         with:
45 |           packages-dir: artifact/
46 | 


--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
 1 | name: Tests
 2 | on:
 3 |   pull_request:
 4 |     paths-ignore: ['docs/**', 'README.md']
 5 |   push:
 6 |     branches: [main, stable]
 7 |     paths-ignore: ['docs/**', 'README.md']
 8 | jobs:
 9 |   tests:
10 |     name: ${{ matrix.name || matrix.python }}
11 |     runs-on: ${{ matrix.os || 'ubuntu-latest' }}
12 |     strategy:
13 |       fail-fast: false
14 |       matrix:
15 |         include:
16 |           - {python: '3.13'}
17 |           - {name: Windows, python: '3.13', os: windows-latest}
18 |           - {name: Mac, python: '3.13', os: macos-latest}
19 |           - {python: '3.12'}
20 |           - {python: '3.11'}
21 |           - {python: '3.10'}
22 |           - {python: '3.9'}
23 |     steps:
24 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25 |       - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
26 |         with:
27 |           enable-cache: true
28 |           prune-cache: false
29 |       - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
30 |         with:
31 |           python-version: ${{ matrix.python }}
32 |       - run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
33 |   typing:
34 |     runs-on: ubuntu-latest
35 |     steps:
36 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
37 |       - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
38 |         with:
39 |           enable-cache: true
40 |           prune-cache: false
41 |       - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
42 |         with:
43 |           python-version-file: pyproject.toml
44 |       - name: cache mypy
45 |         uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
46 |         with:
47 |           path: ./.mypy_cache
48 |           key: mypy|${{ hashFiles('pyproject.toml') }}
49 |       - run: uv run --locked tox run -e typing
50 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .idea/
 2 | .vscode/
 3 | __pycache__/
 4 | dist/
 5 | .coverage*
 6 | htmlcov/
 7 | .tox/
 8 | .hypothesis/
 9 | docs/reference/source
10 | docs/_build/
11 | 


--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
 1 | repos:
 2 |   - repo: https://github.com/astral-sh/ruff-pre-commit
 3 |     rev: 9aeda5d1f4bbd212c557da1ea78eca9e8c829e19  # frozen: v0.11.13
 4 |     hooks:
 5 |       - id: ruff
 6 |       - id: ruff-format
 7 |   - repo: https://github.com/astral-sh/uv-pre-commit
 8 |     rev: a621b109bab2e7e832d98c88fd3e83399f4e6657  # frozen: 0.7.12
 9 |     hooks:
10 |       - id: uv-lock
11 |   - repo: https://github.com/pre-commit/pre-commit-hooks
12 |     rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b  # frozen: v5.0.0
13 |     hooks:
14 |       - id: check-merge-conflict
15 |       - id: debug-statements
16 |       - id: fix-byte-order-marker
17 |       - id: trailing-whitespace
18 |       - id: end-of-file-fixer
19 | 


--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
 1 | version: 2
 2 | build:
 3 |   os: ubuntu-24.04
 4 |   tools:
 5 |     python: '3.13'
 6 |   commands:
 7 |     - asdf plugin add uv
 8 |     - asdf install uv latest
 9 |     - asdf global uv latest
10 |     # TODO fix warnings and add -W
11 |     - uv run --group docs sphinx-build -b dirhtml docs $READTHEDOCS_OUTPUT/html
12 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
  1 | # Contributing Quick Reference
  2 | 
  3 | This document assumes you have some familiarity with Git, GitHub, and Python
  4 | virutalenvs. We are working on a more thorough guide about the different ways to
  5 | contribute with in depth explanations, which will be available soon.
  6 | 
  7 | These instructions will work with at least Bash and PowerShell, and should work
  8 | on other shells. On Windows, use PowerShell, not CMD.
  9 | 
 10 | You need Python and Git installed, as well as the [GitHub CLI]. Log in with
 11 | `gh auth login`. Choose and install an editor; we suggest [PyCharm] or
 12 | [VS Code].
 13 | 
 14 | [GitHub CLI]: https://cli.github.com/
 15 | [PyCharm]: https://www.jetbrains.com/pycharm/
 16 | [VS Code]: https://code.visualstudio.com/
 17 | 
 18 | ## Set Up the Repository
 19 | 
 20 | Fork and clone the project's repository ("pallets/flask" for example). To work
 21 | on a bug or documentation fix, switch to the "stable" branch (if the project has
 22 | it), otherwise switch to the "main" branch. To update this target branch, pull
 23 | from "upstream". Create a work branch with a short descriptive name.
 24 | 
 25 | ```
 26 | $ gh repo fork --clone pallets/flask
 27 | $ cd flask
 28 | $ git switch stable
 29 | $ git pull upstream
 30 | $ git switch -c short-descriptive-name
 31 | ```
 32 | 
 33 | ## Install Development Dependencies
 34 | 
 35 | Create a virtualenv and activate it. Install the dev dependencies, and the
 36 | project in editable mode. Install the pre-commit hooks.
 37 | 
 38 | Create a virtualenv (Mac and Linux):
 39 | 
 40 | ```
 41 | $ python3 -m venv .venv
 42 | $ . .venv/bin/activate
 43 | ```
 44 | 
 45 | Create a virtualenv (Windows):
 46 | 
 47 | ```
 48 | > py -m venv .venv
 49 | > .\.venv\Scripts\activate
 50 | ```
 51 | 
 52 | Install (all platforms):
 53 | 
 54 | ```
 55 | $ pip install -r requirements/dev.txt && pip install -e .
 56 | $ pre-commit install --install-hooks
 57 | ```
 58 | 
 59 | Any time you open a new terminal, you need to activate the virtualenv again. If
 60 | you've pulled from upstream recently, you can re-run the `pip` command above to
 61 | get the current dev dependencies.
 62 | 
 63 | ## Run Tests
 64 | 
 65 | These are the essential test commands you can run while developing:
 66 | 
 67 | * `pytest` - Run the unit tests.
 68 | * `mypy` - Run the main type checker.
 69 | * `tox run -e docs` - Build the documentation.
 70 | 
 71 | These are some more specific commands if you need them:
 72 | 
 73 | * `tox parallel` - Run all test environments that will be run in CI, in
 74 |   parallel. Python versions that are not installed are skipped.
 75 | * `pre-commit` - Run the linter and formatter tools. Only runs against changed
 76 |   files that have been staged with `git add -u`. This will run automatically
 77 |   before each commit.
 78 | * `pre-commit run --all-files` - Run the pre-commit hooks against all files,
 79 |   including unchanged and unstaged.
 80 | * `tox run -e py3.11` - Run unit tests with a specific Python version. The
 81 |   version must be installed. `-e pypy` will run against PyPy.
 82 | * `pyright` - A second type checker.
 83 | * `tox run -e typing` - Run all typing checks. This includes `pyright` and its
 84 |   export check as well.
 85 | * `python -m http.server -b 127.0.0.1 -d docs/_build/html` - Serve the
 86 |   documentation.
 87 | 
 88 | ## Create a Pull Request
 89 | 
 90 | Make your changes and commit them. Add tests that demonstrate that your code
 91 | works, and ensure all tests pass. Change documentation if needed to reflect your
 92 | change. Adding a changelog entry is optional, a maintainer will write one if
 93 | you're not sure how to. Add the entry to the end of the relevant section, match
 94 | the writing and formatting style of existing entries. Don't add an entry for
 95 | changes that only affect documentation or project internals.
 96 | 
 97 | Use the GitHub CLI to start creating your pull request. Specify the target
 98 | branch with `-B`. The "stable" branch is the target for bug and documentation
 99 | fixes, otherwise the target is "main".
100 | 
101 | ```
102 | $ gh pr create --web --base stable
103 | ```
104 | 
105 | CI will run after you create the PR. If CI fails, you can click to see the logs
106 | and address those failures, pushing new commits. Once you feel your PR is ready,
107 | click the "Ready for review" button. A maintainer will review and merge the PR
108 | when they are available.
109 | 


--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright 2017 Pallets
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
 6 | this software and associated documentation files (the "Software"), to deal in
 7 | the Software without restriction, including without limitation the rights to
 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | <div align="center"><img src="https://raw.githubusercontent.com/pallets/quart/refs/heads/main/docs/_static/quart-name.svg" alt="" height="150"></div>
 2 | 
 3 | # Quart
 4 | 
 5 | Quart is an async Python web application framework. Using Quart you can,
 6 | 
 7 | - render and serve HTML templates,
 8 | - write (RESTful) JSON APIs,
 9 | - serve WebSockets,
10 | - stream request and response data,
11 | - do pretty much anything over the HTTP or WebSocket protocols.
12 | 
13 | ## Quickstart
14 | 
15 | Install from PyPI using an installer such as pip.
16 | 
17 | ```
18 | $ pip install quart
19 | ```
20 | 
21 | Save the following as `app.py`. This shows off rendering a template, returning
22 | JSON data, and using a WebSocket.
23 | 
24 | ```python
25 | from quart import Quart, render_template, websocket
26 | 
27 | app = Quart(__name__)
28 | 
29 | @app.route("/")
30 | async def hello():
31 |     return await render_template("index.html")
32 | 
33 | @app.route("/api")
34 | async def json():
35 |     return {"hello": "world"}
36 | 
37 | @app.websocket("/ws")
38 | async def ws():
39 |     while True:
40 |         await websocket.send("hello")
41 |         await websocket.send_json({"hello": "world"})
42 | ```
43 | 
44 | ```
45 | $ quart run
46 |  * Running on http://127.0.0.1:5000 (CTRL + C to quit)
47 | ```
48 | 
49 | To deploy this app in a production setting see the [deployment] documentation.
50 | 
51 | [deployment]: https://quart.palletsprojects.com/en/latest/tutorials/deployment.html
52 | 
53 | ## Contributing
54 | 
55 | Quart is developed on [GitHub]. If you come across a bug, or have a feature
56 | request, please open an [issue]. To contribute a fix or implement a feature,
57 | follow our [contributing guide].
58 | 
59 | [GitHub]: https://github.com/pallets/quart
60 | [issue]: https://github.com/pallets/quart/issues
61 | [contributing guide]: https://github.com/pallets/quart/CONTRIBUTING.rst
62 | 
63 | ## Help
64 | 
65 | If you need help with your code, the Quart [documentation] and [cheatsheet] are
66 | the best places to start. You can ask for help on the [Discussions tab] or on
67 | our [Discord chat].
68 | 
69 | [documentation]: https://quart.palletsprojects.com
70 | [cheatsheet]: https://quart.palletsprojects.com/en/latest/reference/cheatsheet.html
71 | [Discussions tab]: https://github.com/pallets/quart/discussions
72 | [Discord chat]: https://discord.gg
73 | 
74 | ## Relationship with Flask
75 | 
76 | Quart is an asyncio reimplementation of the popular [Flask] web application
77 | framework. This means that if you understand Flask you understand Quart.
78 | 
79 | Like Flask, Quart has an ecosystem of extensions for more specific needs. In
80 | addition, a number of the Flask extensions work with Quart.
81 | 
82 | [Flask]: https://flask.palletsprojects.com
83 | 
84 | ### Migrating from Flask
85 | 
86 | It should be possible to migrate to Quart from Flask by a find and replace of
87 | `flask` to `quart` and then adding `async` and `await` keywords. See the
88 | [migration] documentation for more help.
89 | 
90 | [migration]: https://quart.palletsprojects.com/en/latest/how_to_guides/flask_migration.html
91 | 


--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
 1 | # Minimal makefile for Sphinx documentation
 2 | #
 3 | 
 4 | # You can set these variables from the command line.
 5 | SPHINXOPTS    =
 6 | SPHINXBUILD   = python -msphinx
 7 | SPHINXPROJ    = Quart
 8 | SOURCEDIR     = .
 9 | BUILDDIR      = _build
10 | 
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | 	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 | 
15 | .PHONY: help Makefile
16 | 
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | 	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 | 


--------------------------------------------------------------------------------
/docs/_static/quart-icon.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 3 | <svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
 4 |     <rect id="Icon" x="0" y="0" width="500" height="500" style="fill:none;"/>
 5 |     <clipPath id="_clip1">
 6 |         <rect x="0" y="0" width="500" height="500"/>
 7 |     </clipPath>
 8 |     <g clip-path="url(#_clip1)">
 9 |         <g>
10 |             <path d="M415.574,136.117l-50.302,58.634c53.079,70.053 52.154,169.732 -7.407,238.859c-68.509,79.928 -188.864,88.877 -268.484,20.368c-79.928,-68.51 -88.878,-188.557 -20.368,-268.485c54.623,-63.88 142.266,-82.397 216.022,-52.462l54.006,-62.955c1.543,-1.852 4.629,-2.16 6.789,-0.617l69.435,59.56c1.852,2.469 2.161,5.246 0.309,7.098Zm-37.032,181.458c-27.157,-93.506 -122.207,-14.504 -187.013,-54.931c-64.498,-40.427 -33.638,-108.628 -33.638,-108.628c-27.157,9.875 -51.228,26.231 -70.053,48.142c-59.252,69.127 -51.228,173.743 17.899,232.995c69.127,59.252 173.435,51.537 232.995,-17.899c24.997,-29.009 49.685,-65.732 39.81,-99.679Z" style="fill:#2952e1;"/>
11 |             <path d="M422.363,121.613l-71.287,-61.104c-5.863,-4.937 -6.172,-13.578 -1.543,-19.442c4.938,-5.863 13.579,-6.172 19.442,-1.543l71.287,61.104c5.864,4.937 6.172,13.578 1.543,19.442c-4.937,5.863 -13.578,6.48 -19.442,1.543Z" style="fill:#2952e1;fill-rule:nonzero;"/>
12 |             <path d="M446.126,88.283l-62.647,-53.388c-3.394,-2.777 -3.703,-8.023 -0.925,-11.418l17.59,-20.676c2.777,-3.395 8.024,-3.704 11.418,-0.926l62.647,53.388c3.394,2.777 3.703,8.024 0.925,11.418l-17.59,20.677c-3.086,3.394 -8.024,4.012 -11.418,0.925Z" style="fill:#2952e1;fill-rule:nonzero;"/>
13 |         </g>
14 |     </g>
15 | </svg>
16 | 


--------------------------------------------------------------------------------
/docs/_static/quart-logo.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 3 | <svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
 4 |     <rect id="Logo" x="0" y="0" width="500" height="500" style="fill:none;"/>
 5 |     <g>
 6 |         <path id="Box" d="M500,50l0,400c0,27.596 -22.404,50 -50,50l-400,0c-27.596,0 -50,-22.404 -50,-50l0,-400c0,-27.596 22.404,-50 50,-50l400,0c27.596,0 50,22.404 50,50Z" style="fill:url(#_Linear1);"/>
 7 |         <path id="Shadow" d="M500,248.633l0,42.217l-169.912,-169.913l37.587,32.033c2.037,1.852 5,1.481 6.851,-0.555l10.555,-12.406c1.475,-1.804 1.499,-4.478 0.071,-6.224l114.848,114.848Zm0,47.586l0,29.441l-189.354,-189.354l42.772,36.662c3.518,2.962 8.703,2.592 11.665,-0.926c2.492,-3.157 2.599,-7.656 0.053,-10.687l134.864,134.864Zm0,32.033l0,121.748c0,27.596 -22.404,50 -50,50l-168.758,-0l-127.613,-127.613c47.771,41.105 119.984,35.736 161.09,-12.221c35.736,-41.476 36.292,-101.283 4.444,-143.315l30.181,-35.181c1.111,-1.111 0.926,-2.777 -0.185,-4.259l150.841,150.841Z" style="fill:#140b5b;"/>
 8 |         <g id="Icon">
 9 |             <path d="M349.344,181.67l-30.181,35.181c31.848,42.032 31.292,101.839 -4.444,143.315c-41.106,47.957 -113.319,53.326 -161.09,12.221c-47.957,-41.106 -53.327,-113.134 -12.221,-161.091c32.774,-38.328 85.359,-49.438 129.613,-31.477l32.403,-37.773c0.926,-1.111 2.778,-1.297 4.074,-0.371l41.661,35.736c1.111,1.482 1.296,3.148 0.185,4.259Z" style="fill:#fff;"/>
10 |             <path d="M327.125,290.545c-16.294,-56.104 -73.324,-8.702 -112.208,-32.959c-38.699,-24.256 -20.183,-65.176 -20.183,-65.176c-16.294,5.925 -30.736,15.738 -42.031,28.885c-35.551,41.476 -30.737,104.246 10.739,139.797c41.476,35.551 104.061,30.922 139.797,-10.74c14.998,-17.405 29.811,-39.439 23.886,-59.807Z" style="fill:#140b5b;"/>
11 |             <path d="M353.418,172.968l-42.772,-36.662c-3.518,-2.963 -3.704,-8.148 -0.926,-11.666c2.962,-3.518 8.147,-3.703 11.665,-0.925l42.772,36.662c3.518,2.962 3.704,8.147 0.926,11.665c-2.962,3.518 -8.147,3.888 -11.665,0.926Z" style="fill:#fff;fill-rule:nonzero;"/>
12 |             <path d="M367.675,152.97l-37.587,-32.033c-2.037,-1.666 -2.222,-4.814 -0.556,-6.851l10.554,-12.406c1.667,-2.036 4.815,-2.222 6.851,-0.555l37.588,32.033c2.037,1.666 2.222,4.814 0.556,6.851l-10.555,12.406c-1.851,2.036 -4.814,2.407 -6.851,0.555Z" style="fill:#fff;fill-rule:nonzero;"/>
13 |         </g>
14 |     </g>
15 |     <defs>
16 |         <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.06162e-14,500,-500,3.06162e-14,267.59,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient>
17 |     </defs>
18 | </svg>
19 | 


--------------------------------------------------------------------------------
/docs/_static/quart-name-dark.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 3 | <svg width="100%" height="100%" viewBox="0 0 713 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
 4 |     <g>
 5 |         <path id="Name" d="M434.9,147l-0,6.15c-0,17 -4.25,28.75 -12.75,35.25c5.5,-0.3 10.6,-1.15 15.3,-2.55l-0,18.45l-45,0c-6.2,0 -12.15,-1.025 -17.85,-3.075c-5.7,-2.05 -10.625,-5.15 -14.775,-9.3c-4.15,-4.15 -6.825,-9.125 -8.025,-14.925c-1.2,-5.8 -1.8,-13.85 -1.8,-24.15l-0,-5.7c-0,-10.3 0.6,-18.35 1.8,-24.15c1.2,-5.8 3.875,-10.775 8.025,-14.925c4.15,-4.15 9.075,-7.25 14.775,-9.3c5.7,-2.05 11.65,-3.075 17.85,-3.075c6.2,0 12.125,1.025 17.775,3.075c5.65,2.05 10.6,5.2 14.85,9.45c4.25,4.25 6.95,9.125 8.1,14.625c1.15,5.5 1.725,13.55 1.725,24.15Zm-20.85,5.25l-0,-7.2c-0,-12.5 -1.7,-20.7 -5.1,-24.6c-3.4,-4.1 -8.9,-6.15 -16.5,-6.15c-7.6,0 -13.2,2.1 -16.8,6.3c-2.6,3.1 -4.1,8.5 -4.5,16.2c-0.2,2.5 -0.3,5.95 -0.3,10.35l-0,5.55c0.1,10.6 0.9,17.85 2.4,21.75c2.8,7.5 9.2,11.25 19.2,11.25c8.7,0 14.5,-2.5 17.4,-7.5c1.9,-3.6 3.05,-7.1 3.45,-10.5c0.5,-4.3 0.75,-9.45 0.75,-15.45Z" style="fill:#fff;fill-rule:nonzero;"/>
 6 |         <path d="M515.6,203.4l-14.55,-0l-4.5,-10.2l-24,11.1c-6.7,-0 -12.525,-2.475 -17.475,-7.425c-4.95,-4.95 -7.425,-12.175 -7.425,-21.675l-0,-57.45l19.5,-0l-0,53.25c-0,9.5 3.8,14.25 11.4,14.25l17.55,-9.45l-0,-58.05l19.5,-0l-0,85.65Z" style="fill:#fff;fill-rule:nonzero;"/>
 7 |         <path d="M596.45,203.4l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c-0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l-0,86.25Zm-19.5,-27.3l-0,-40.2l-14.85,0c-5.5,0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c-0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill:#fff;fill-rule:nonzero;"/>
 8 |         <path d="M663.05,118.05l-5.4,19.2c-1.5,-0.6 -3.2,-0.9 -5.1,-0.9c-5,-0 -11.15,2.15 -18.45,6.45l-0,60.6l-19.5,0l-0,-85.65l14.55,0l4.5,7.5l20.7,-8.55c1,-0.1 2.35,-0.15 4.05,-0.15c1.7,0 3.25,0.5 4.65,1.5Z" style="fill:#fff;fill-rule:nonzero;"/>
 9 |         <path d="M712.7,203.4l-12.15,-0c-7.4,-0 -13.025,-2.2 -16.875,-6.6c-3.85,-4.4 -5.775,-9.65 -5.775,-15.75l-0,-45l-8.25,-0l-0,-14.85l8.25,-0l-0,-16.8l19.5,-0l-0,16.8l13.8,-0l-0,14.85l-13.8,-0l-0,43.8c-0,4.7 2.25,7.05 6.75,7.05l8.55,-0l-0,16.5Z" style="fill:#fff;fill-rule:nonzero;"/>
10 |         <g id="Logo">
11 |             <path id="Box" d="M300,30l0,240c0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/>
12 |             <path id="Shadow" d="M300,149.18l0,25.33l-101.947,-101.948l22.552,19.22c1.222,1.111 3,0.889 4.111,-0.333l6.332,-7.444c0.886,-1.082 0.9,-2.687 0.043,-3.734l68.909,68.909Zm0,28.552l0,17.664l-113.613,-113.613l25.664,21.998c2.111,1.777 5.221,1.555 6.999,-0.556c1.495,-1.894 1.559,-4.593 0.032,-6.412l80.918,80.919Zm0,19.219l0,73.049c0,16.557 -13.443,30 -30,30l-101.255,-0l-76.568,-76.568c28.663,24.663 71.991,21.442 96.655,-7.332c21.441,-24.886 21.775,-60.77 2.666,-85.989l18.109,-21.109c0.666,-0.666 0.555,-1.666 -0.111,-2.555l90.504,90.504Z" style="fill:#140b5b;"/>
13 |             <g id="Icon">
14 |                 <path d="M209.607,109.002l-18.109,21.109c19.109,25.219 18.775,61.103 -2.666,85.989c-24.664,28.774 -67.992,31.995 -96.655,7.332c-28.774,-24.664 -31.996,-67.88 -7.332,-96.654c19.664,-22.997 51.215,-29.663 77.768,-18.887l19.442,-22.664c0.555,-0.666 1.666,-0.777 2.444,-0.222l24.997,21.442c0.666,0.889 0.777,1.889 0.111,2.555Z" style="fill:#fff;"/>
15 |                 <path d="M196.275,174.327c-9.777,-33.662 -43.994,-5.221 -67.325,-19.775c-23.219,-14.554 -12.109,-39.106 -12.109,-39.106c-9.777,3.555 -18.442,9.443 -25.219,17.331c-21.331,24.886 -18.442,62.547 6.443,83.878c24.886,21.331 62.437,18.553 83.879,-6.444c8.998,-10.443 17.886,-23.663 14.331,-35.884Z" style="fill:#140b5b;"/>
16 |                 <path d="M212.051,103.781l-25.664,-21.998c-2.11,-1.777 -2.222,-4.888 -0.555,-6.999c1.777,-2.111 4.888,-2.222 6.999,-0.555l25.663,21.997c2.111,1.777 2.222,4.888 0.556,6.999c-1.778,2.111 -4.888,2.333 -6.999,0.556Z" style="fill:#fff;fill-rule:nonzero;"/>
17 |                 <path d="M220.605,91.782l-22.552,-19.22c-1.222,-1 -1.334,-2.888 -0.334,-4.11l6.333,-7.444c1,-1.222 2.888,-1.333 4.11,-0.333l22.553,19.22c1.222,1 1.333,2.888 0.333,4.11l-6.332,7.444c-1.111,1.222 -2.889,1.444 -4.111,0.333Z" style="fill:#fff;fill-rule:nonzero;"/>
18 |             </g>
19 |         </g>
20 |     </g>
21 |     <defs>
22 |         <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient>
23 |     </defs>
24 | </svg>
25 | 


--------------------------------------------------------------------------------
/docs/_static/quart-name.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 3 | <svg width="100%" height="100%" viewBox="0 0 713 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
 4 |     <g>
 5 |         <path id="Name" d="M434.9,147l0,6.15c0,17 -4.25,28.75 -12.75,35.25c5.5,-0.3 10.6,-1.15 15.3,-2.55l0,18.45l-45,-0c-6.2,-0 -12.15,-1.025 -17.85,-3.075c-5.7,-2.05 -10.625,-5.15 -14.775,-9.3c-4.15,-4.15 -6.825,-9.125 -8.025,-14.925c-1.2,-5.8 -1.8,-13.85 -1.8,-24.15l0,-5.7c0,-10.3 0.6,-18.35 1.8,-24.15c1.2,-5.8 3.875,-10.775 8.025,-14.925c4.15,-4.15 9.075,-7.25 14.775,-9.3c5.7,-2.05 11.65,-3.075 17.85,-3.075c6.2,-0 12.125,1.025 17.775,3.075c5.65,2.05 10.6,5.2 14.85,9.45c4.25,4.25 6.95,9.125 8.1,14.625c1.15,5.5 1.725,13.55 1.725,24.15Zm-20.85,5.25l0,-7.2c0,-12.5 -1.7,-20.7 -5.1,-24.6c-3.4,-4.1 -8.9,-6.15 -16.5,-6.15c-7.6,-0 -13.2,2.1 -16.8,6.3c-2.6,3.1 -4.1,8.5 -4.5,16.2c-0.2,2.5 -0.3,5.95 -0.3,10.35l0,5.55c0.1,10.6 0.9,17.85 2.4,21.75c2.8,7.5 9.2,11.25 19.2,11.25c8.7,-0 14.5,-2.5 17.4,-7.5c1.9,-3.6 3.05,-7.1 3.45,-10.5c0.5,-4.3 0.75,-9.45 0.75,-15.45Z" style="fill-rule:nonzero;"/>
 6 |         <path d="M515.6,203.4l-14.55,-0l-4.5,-10.2l-24,11.1c-6.7,-0 -12.525,-2.475 -17.475,-7.425c-4.95,-4.95 -7.425,-12.175 -7.425,-21.675l0,-57.45l19.5,-0l0,53.25c0,9.5 3.8,14.25 11.4,14.25l17.55,-9.45l0,-58.05l19.5,-0l0,85.65Z" style="fill-rule:nonzero;"/>
 7 |         <path d="M596.45,203.4l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l0,86.25Zm-19.5,-27.3l0,-40.2l-14.85,-0c-5.5,-0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill-rule:nonzero;"/>
 8 |         <path d="M663.05,118.05l-5.4,19.2c-1.5,-0.6 -3.2,-0.9 -5.1,-0.9c-5,-0 -11.15,2.15 -18.45,6.45l0,60.6l-19.5,-0l0,-85.65l14.55,-0l4.5,7.5l20.7,-8.55c1,-0.1 2.35,-0.15 4.05,-0.15c1.7,-0 3.25,0.5 4.65,1.5Z" style="fill-rule:nonzero;"/>
 9 |         <path d="M712.7,203.4l-12.15,-0c-7.4,-0 -13.025,-2.2 -16.875,-6.6c-3.85,-4.4 -5.775,-9.65 -5.775,-15.75l0,-45l-8.25,-0l0,-14.85l8.25,-0l0,-16.8l19.5,-0l0,16.8l13.8,-0l0,14.85l-13.8,-0l0,43.8c0,4.7 2.25,7.05 6.75,7.05l8.55,-0l0,16.5Z" style="fill-rule:nonzero;"/>
10 |         <g id="Logo">
11 |             <path id="Box" d="M300,30l0,240c0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/>
12 |             <path id="Shadow" d="M300,149.18l0,25.33l-101.947,-101.948l22.552,19.22c1.222,1.111 3,0.889 4.111,-0.333l6.332,-7.444c0.886,-1.082 0.9,-2.687 0.043,-3.734l68.909,68.909Zm0,28.552l0,17.664l-113.613,-113.613l25.664,21.998c2.111,1.777 5.221,1.555 6.999,-0.556c1.495,-1.894 1.559,-4.593 0.032,-6.412l80.918,80.919Zm0,19.219l0,73.049c0,16.557 -13.443,30 -30,30l-101.255,-0l-76.568,-76.568c28.663,24.663 71.991,21.442 96.655,-7.332c21.441,-24.886 21.775,-60.77 2.666,-85.989l18.109,-21.109c0.666,-0.666 0.555,-1.666 -0.111,-2.555l90.504,90.504Z" style="fill:#140b5b;"/>
13 |             <g id="Icon">
14 |                 <path d="M209.607,109.002l-18.109,21.109c19.109,25.219 18.775,61.103 -2.666,85.989c-24.664,28.774 -67.992,31.995 -96.655,7.332c-28.774,-24.664 -31.996,-67.88 -7.332,-96.654c19.664,-22.997 51.215,-29.663 77.768,-18.887l19.442,-22.664c0.555,-0.666 1.666,-0.777 2.444,-0.222l24.997,21.442c0.666,0.889 0.777,1.889 0.111,2.555Z" style="fill:#fff;"/>
15 |                 <path d="M196.275,174.327c-9.777,-33.662 -43.994,-5.221 -67.325,-19.775c-23.219,-14.554 -12.109,-39.106 -12.109,-39.106c-9.777,3.555 -18.442,9.443 -25.219,17.331c-21.331,24.886 -18.442,62.547 6.443,83.878c24.886,21.331 62.437,18.553 83.879,-6.444c8.998,-10.443 17.886,-23.663 14.331,-35.884Z" style="fill:#140b5b;"/>
16 |                 <path d="M212.051,103.781l-25.664,-21.998c-2.11,-1.777 -2.222,-4.888 -0.555,-6.999c1.777,-2.111 4.888,-2.222 6.999,-0.555l25.663,21.997c2.111,1.777 2.222,4.888 0.556,6.999c-1.778,2.111 -4.888,2.333 -6.999,0.556Z" style="fill:#fff;fill-rule:nonzero;"/>
17 |                 <path d="M220.605,91.782l-22.552,-19.22c-1.222,-1 -1.334,-2.888 -0.334,-4.11l6.333,-7.444c1,-1.222 2.888,-1.333 4.11,-0.333l22.553,19.22c1.222,1 1.333,2.888 0.333,4.11l-6.332,7.444c-1.111,1.222 -2.889,1.444 -4.111,0.333Z" style="fill:#fff;fill-rule:nonzero;"/>
18 |             </g>
19 |         </g>
20 |     </g>
21 |     <defs>
22 |         <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#bacfe5;stop-opacity:1"/><stop offset="1" style="stop-color:#1b2a8f;stop-opacity:1"/></linearGradient>
23 |     </defs>
24 | </svg>
25 | 


--------------------------------------------------------------------------------
/docs/changes.md:
--------------------------------------------------------------------------------
1 | # Changes
2 | 
3 | ```{include} ../CHANGES.md
4 | ```
5 | 


--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
 1 | import importlib.metadata
 2 | import os
 3 | 
 4 | from sphinx.ext import apidoc
 5 | 
 6 | # Project --------------------------------------------------------------
 7 | 
 8 | project = "Quart"
 9 | copyright = "2017 Pallets"
10 | version = release = importlib.metadata.version("quart").partition(".dev")[0]
11 | 
12 | # General --------------------------------------------------------------
13 | 
14 | default_role = "code"
15 | extensions = [
16 |     "sphinx.ext.autodoc",
17 |     "sphinx.ext.napoleon",
18 |     "myst_parser",
19 | ]
20 | autodoc_member_order = "bysource"
21 | autodoc_typehints = "description"
22 | autodoc_preserve_defaults = True
23 | myst_enable_extensions = [
24 |     "fieldlist",
25 | ]
26 | myst_heading_anchors = 2
27 | 
28 | # HTML -----------------------------------------------------------------
29 | 
30 | html_theme = "pydata_sphinx_theme"
31 | html_theme_options = {
32 |     "logo": {"text": "Quart"},
33 |     "external_links": [
34 |         {"name": "Source code", "url": "https://github.com/pallets/quart"},
35 |         {"name": "Issues", "url": "https://github.com/pallets/quart/issues"},
36 |     ],
37 |     "icon_links": [
38 |         {
39 |             "name": "Github",
40 |             "url": "https://github.com/pallets/quart",
41 |             "icon": "fab fa-github",
42 |         },
43 |     ],
44 | }
45 | html_static_path = ["_static"]
46 | html_favicon = "_static/quart-icon.svg"
47 | html_logo = "_static/quart-logo.svg"
48 | 
49 | 
50 | def run_apidoc(_):
51 |     # generate API documentation via sphinx-apidoc
52 |     # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html
53 |     base_path = os.path.abspath(os.path.dirname(__file__))
54 |     apidoc.main(
55 |         [
56 |             "-f",
57 |             "-e",
58 |             "-o",
59 |             f"{base_path}/reference/source",
60 |             f"{base_path}/../src/quart",
61 |             f"{base_path}/../src/quart/datastructures.py",
62 |         ]
63 |     )
64 | 
65 | 
66 | def setup(app):
67 |     app.connect("builder-inited", run_apidoc)
68 | 


--------------------------------------------------------------------------------
/docs/discussion/async_compatibility.rst:
--------------------------------------------------------------------------------
 1 | .. _async_compatibility:
 2 | 
 3 | Async compatibility
 4 | ===================
 5 | 
 6 | Synchronous and asynchronous code are not directly compatible in that
 7 | the functions must be called differently depending on the type. This
 8 | limits what can be done, for example in how Quart interacts with Flask
 9 | extensions and any effort to make Flask directly asynchronous.
10 | 
11 | In my opinion it is much easier to start with an asynchronous codebase
12 | that calls synchronous code than vice versa in Python. I will try and
13 | reason why below.
14 | 
15 | Calling sync code from async functions
16 | --------------------------------------
17 | 
18 | This is mostly easy in that you can either call, or via a simple wrapper
19 | await a synchronous function,
20 | 
21 | .. code-block:: python
22 | 
23 |     async def example():
24 |         sync_call()
25 |         await asyncio.coroutine(sync_call)()
26 | 
27 | whilst this doesn't actually change the nature, the call is
28 | synchronous, it does work.
29 | 
30 | Calling async code from sync functions
31 | --------------------------------------
32 | 
33 | This is where things get difficult, as it is only possible to create a
34 | single event loop. Hence this can only be used once,
35 | 
36 | .. code-block:: python
37 | 
38 |     def example():
39 |         loop = asyncio.get_event_loop()
40 |         loop.run_until_complete(async_call())
41 | 
42 | therefore if you are not at the very outer scope it isn't really
43 | possible to call asynchronous code from a synchronous function.
44 | 
45 | This is problematic when dealing with Flask extensions as for example the
46 | extension may have something like,
47 | 
48 | .. code-block:: python
49 | 
50 |     @app.route('/')
51 |     def route():
52 |         data = request.form
53 |         return render_template_string("{{ name }}", name=data['name'])
54 | 
55 | whilst the route function can be wrapped with the
56 | ``asyncio.coroutine`` function and hence awaited, there is no (easy?)
57 | way to insert the ``await`` before the ``request.form`` and
58 | ``render_template`` calls.
59 | 
60 | It is for this reason that Quart-Flask-Patch creates sync wrapped
61 | versions for the Flask extensions. The former adding synchronous
62 | request methods and the other providing synchronous functions.
63 | 
64 | Quart monkey patches a ``sync_wait`` method onto the base event loop
65 | allowing for definitions such as,
66 | 
67 | .. code-block:: python
68 | 
69 |     from quart.templating import render_template as quart_render_template
70 | 
71 |     def render_template(*args):
72 |         return asyncio.sync_wait(quart_render_template(*args))
73 | 


--------------------------------------------------------------------------------
/docs/discussion/background_tasks.rst:
--------------------------------------------------------------------------------
 1 | .. _background_task_discussion:
 2 | 
 3 | Background tasks
 4 | ================
 5 | 
 6 | The API for background tasks follows Sanic and Starlette by taking a
 7 | callable and arguments. However, Quart will ensure that the tasks
 8 | finish during the shutdown (unless the server times out and
 9 | cancels). This is as you'd hope in a production environment.
10 | 
11 | Errors raised in a background task are logged but otherwise ignored
12 | allowing the app to continue - much like with request/websocket
13 | handling errors.
14 | 


--------------------------------------------------------------------------------
/docs/discussion/contexts.rst:
--------------------------------------------------------------------------------
 1 | .. _contexts:
 2 | 
 3 | Contexts
 4 | ========
 5 | 
 6 | Quart, like Flask, has two contexts the *application context* and the
 7 | *request context*. Both of these contexts exist per request and allow
 8 | the global proxies ``current_app``, ``request``, etc... to be resolved.
 9 | Note that these contexts are task local, and hence will not exist if a
10 | task is spawned by ``ensure_future`` or ``create_task``.
11 | 
12 | The design principle of these contexts is that they are likely needed
13 | in all routes, and hence rather than pass these objects around they
14 | are made available via global proxies. This has its downsides, notably
15 | all the arguments relating to global variables. Therefore, it is
16 | recommended that these proxies are only used within routes so as to
17 | isolate the scope.
18 | 
19 | Application Context
20 | -------------------
21 | 
22 | The application context is a reference point for any information that
23 | isn't specifically related to a request. This includes the app itself,
24 | the ``g`` global object and a ``url_adapter`` bound only to the app. The
25 | context is created and destroyed implicitly by the request context.
26 | 
27 | Request Context
28 | ---------------
29 | 
30 | The request context is a reference point for any information that is
31 | related to a request. This includes the request itself, a ``url_adapter``
32 | bound to the request and the session. It is created and destroyed by
33 | the :func:`~quart.Quart.handle_request` method per request.
34 | 
35 | Websocket Context
36 | -----------------
37 | 
38 | The websocket context is analogous to the request context, but is
39 | related only to websocket requests. It is created and destroyed by the
40 | :func:`~quart.Quart.handle_websocket_request` method per websocket
41 | connection.
42 | 
43 | Tasks and contexts
44 | ------------------
45 | 
46 | Context is bound to a ContextVar and will be copied to tasks created
47 | from an existing task. To explicitly copy a context Quart provides the
48 | decorators :func:`~quart.ctx.copy_current_request_context` and
49 | :func:`copy_current_websocket_context` which can be used as so,
50 | 
51 | .. code-block:: python
52 | 
53 |     @app.route('/')
54 |     async def index():
55 | 
56 |         @copy_current_request_context
57 |         async def background_task():
58 |             method = request.method
59 |             ...
60 | 
61 |         asyncio.ensure_future(background_task())
62 |         ...
63 | 
64 | If you need to provide the ``request`` context in an asynchronous
65 | generator, use the :func:`quart.helpers.stream_with_context` decorator
66 | as discussed in :ref:`streaming_response`:
67 | 
68 | .. code-block:: python
69 | 
70 |     @app.route('/')
71 |     async def index():
72 | 
73 |         @stream_with_context
74 |         async def async_generator():
75 |             async for data in request.body:
76 |                 yield data
77 | 
78 |         await consume_data(async_generator())
79 |         ...
80 | 
81 | .. note:: These decorators must be used within an existing context, hence
82 |           the background task is defined as a nested function.
83 | 


--------------------------------------------------------------------------------
/docs/discussion/design_choices.rst:
--------------------------------------------------------------------------------
 1 | .. _design_choices:
 2 | 
 3 | Design Choices
 4 | ==============
 5 | 
 6 | Coroutines or functions
 7 | -----------------------
 8 | 
 9 | It is quite easy to call sync and trigger async execution from a
10 | coroutine and hard to trigger async execution from a function, see
11 | :ref:`async_compatibility`. For this reason coroutines are preferred even in
12 | cases where IO seems unlikely.
13 | 


--------------------------------------------------------------------------------
/docs/discussion/flask_evolution.rst:
--------------------------------------------------------------------------------
 1 | .. _flask_evolution:
 2 | 
 3 | Flask evolution
 4 | ===============
 5 | 
 6 | (The author) sees Quart as an evolution of Flask, primarily to support
 7 | asyncio and secondarily to support websockets and HTTP/2. These
 8 | additions are designed following (the author's interpretation) of
 9 | Flask's design choices. It is for this reason that the websocket
10 | context and global exist, rather than as an argument to the route
11 | handler.
12 | 
13 | Omissions from the Flask API
14 | ----------------------------
15 | 
16 | There are parts of the Flask API that I've decided to either not
17 | implement, these are,
18 | 
19 | request.stream
20 | ^^^^^^^^^^^^^^
21 | 
22 | The ``stream`` method present on Flask request instances allows the
23 | request body to be 'streamed' via a file like interface. In Quart
24 | :ref:`request_body` is done differently in order to make use of the
25 | ``async`` keyword.
26 | 


--------------------------------------------------------------------------------
/docs/discussion/globals.rst:
--------------------------------------------------------------------------------
 1 | .. _globals:
 2 | 
 3 | Globals
 4 | =======
 5 | 
 6 | As Quart follows the Flask API, it also has globals, specifically
 7 | ``current_app, g, request, session``. These are globals as they are
 8 | likely needed in all request handlers.
 9 | 
10 | Locals
11 | ------
12 | 
13 | As is rather confusing from a naming point of view, the globals are
14 | local to the task being executed. This is detailed from a contextual
15 | view in :ref:`contexts`. This is necessary to ensure that Quart can
16 | asynchronously handle many requests with a single global object.
17 | 


--------------------------------------------------------------------------------
/docs/discussion/index.rst:
--------------------------------------------------------------------------------
 1 | ===========
 2 | Discussions
 3 | ===========
 4 | 
 5 | .. toctree::
 6 |    :maxdepth: 1
 7 | 
 8 |    async_compatibility.rst
 9 |    background_tasks.rst
10 |    contexts.rst
11 |    design_choices.rst
12 |    dos_mitigations.rst
13 |    flask_evolution.rst
14 |    globals.rst
15 |    python_versions.rst
16 |    websockets_discussion.rst
17 | 


--------------------------------------------------------------------------------
/docs/discussion/python_versions.rst:
--------------------------------------------------------------------------------
1 | .. _python_versions:
2 | 
3 | Python version support
4 | ======================
5 | 
6 | The main branch and releases >= 0.20.0 onwards only support Python
7 | 3.9.0 or greater.
8 | 


--------------------------------------------------------------------------------
/docs/discussion/websockets_discussion.rst:
--------------------------------------------------------------------------------
 1 | .. _websockets_discussion:
 2 | 
 3 | Websockets
 4 | ==========
 5 | 
 6 | Websockets start as a GET request that can either be upgraded to a
 7 | websocket via a 101, switching protocols, response or any other
 8 | response. The choice of what to do, or how to respond, is often not
 9 | possible in other frameworks and is one of the motivating aims in
10 | Quart. In addition as websockets are very similar to requests, Quart
11 | aims to have analogues functionality between websockets and requests.
12 | 
13 | Request analogues
14 | -----------------
15 | 
16 | Websockets are very similar to GET requests, to the extent that is was
17 | tempting to simply extend the Flask request API to include websocket
18 | functionality. This would likely cause surprise to users of
19 | Flask-Sockets or Flask-SocketIO which set the de-facto Flask
20 | standard. Therefore I decided to introduce the websocket functionality
21 | alongside the existing request functionality.
22 | 
23 | As websockets are so similar to GET requests it makes sense to produce
24 | an analogue for all the functionality available for requests. For
25 | example. :meth:`~quart.app.Quart.before_request` and
26 | :meth:`~quart.app.Quart.before_websocket` and there is a *websocket
27 | context* alongside the *request context*.
28 | 
29 | Response or Upgrade
30 | -------------------
31 | 
32 | The utility of being able to choose how to respond, or whether to
33 | upgrade, is best shown when considering authentication. In the example
34 | below a typical login_required decorator can be used to prevent
35 | unauthorised usage of the websocket.
36 | 
37 | .. code-block:: python
38 | 
39 |     def login_required(func):
40 |         @wraps(func)
41 |         async def wrapper(*args, **kwargs):
42 |             if websocket.authentication == (...):
43 |                 return await func(*args, **kwargs)
44 |             else:
45 |                 abort(401)
46 |         return wrapper
47 | 
48 |     @app.websocket('/ws')
49 |     @login_required
50 |     async def ws():
51 |         while True:
52 |             await websocket.receive()
53 |             ...
54 | 
55 | Quart also allows for the acceptance response (101) to be manually
56 | sent via :meth:`~quart.wrappers.Websocket.accept` as this gives the
57 | framework user full control.
58 | 
59 | .. note::
60 | 
61 |     This functionality is only useable with ASGI servers that
62 |     implement the ``Websocket Denial Response`` extension. If the
63 |     server does not support this extension Quart will instruct the
64 |     server to close the connection without a response. Hypercorn, the
65 |     recommended ASGI server, supports this extension.
66 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/background_tasks.rst:
--------------------------------------------------------------------------------
 1 | .. _background_tasks:
 2 | 
 3 | Background tasks
 4 | ================
 5 | 
 6 | If you have a task to perform where the outcome or result isn't
 7 | required you can utilise a background task to run it. Background tasks
 8 | run concurrently with the route handlers etc, i.e. in the
 9 | background. Background tasks are very useful when they contain actions
10 | that take a lot of time to complete, as they allow a response to be
11 | sent to the client whilst the task itself is carried out. Equally some
12 | tasks just don't need to be completed before the response is sent and
13 | instead can be done in the background.
14 | 
15 | Background tasks in Quart are created via the ``add_background_task``
16 | method:
17 | 
18 | .. code-block:: python
19 | 
20 |     async def background_task():
21 |         ...
22 | 
23 |     @app.route('/jobs/', methods=['POST'])
24 |     async def create_job():
25 |         app.add_background_task(background_task)
26 |         return 'Success'
27 | 
28 |     @app.before_serving
29 |     async def startup():
30 |         app.add_background_task(background_task)
31 | 
32 | 
33 | The background tasks will have access to the app context. The tasks
34 | will be awaited during shutdown to ensure they complete before the app
35 | shuts down. If your task does not complete within the config
36 | ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` it will be cancelled.
37 | 
38 | Note ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` should ideally be less than
39 | any server shutdown timeout.
40 | 
41 | Synchronous background tasks are supported and will run in a separate
42 | thread.
43 | 
44 | .. warning::
45 | 
46 |     As Quart is based on asyncio it will run on a single execution and
47 |     switch between tasks as they become blocked on waiting on IO, if a
48 |     task does not need to wait on IO it will instead block the event
49 |     loop and Quart could become unresponsive. Additionally the task
50 |     will consume the same CPU resources as the server and hence could
51 |     slow the server.
52 | 
53 | 
54 | Testing background tasks
55 | ------------------------
56 | 
57 | To ensure that background tasks complete in tests utilise the
58 | ``test_app`` context manager. This will wait for any background
59 | tasks to complete before allowing the test to continue:
60 | 
61 | .. code-block:: python
62 | 
63 |     async def test_tasks_complete():
64 |         async with app.test_app():
65 |             app.add_background_task(...)
66 |         # Background task has completed here
67 |         assert task_has_done_something
68 | 
69 | Note when testing an app the ``test_client`` usage should be within
70 | the ``test_app`` context block.
71 | 
72 | The background task coroutine function can be tested by creating an
73 | app context and await the function,
74 | 
75 | .. code-block:: python
76 | 
77 |     async def test_background_task():
78 |         async with app.app_context():
79 |             await background_task()
80 |         assert something_to_test
81 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/blueprints.rst:
--------------------------------------------------------------------------------
 1 | .. _blueprints:
 2 | 
 3 | Blueprints
 4 | ==========
 5 | 
 6 | Blueprints allow for modular code, they should be used whenever the
 7 | routes start to span multiple modules. Blueprints, like the app can have
 8 | template and static files, therefore a typical folder structure for a
 9 | blueprint termed ``store`` would be,
10 | 
11 | ::
12 | 
13 |     blueprints/
14 |     blueprints/store/__init__.py
15 |     blueprints/store/templates/
16 |     blueprints/store/templates/index.html
17 |     blueprints/store/static/
18 | 
19 | the ``__init__.py`` file should contain something like,
20 | 
21 | .. code-block:: python
22 | 
23 |     from quart import Blueprint
24 | 
25 |     blueprint = Blueprint('store', __name__)
26 | 
27 |     @blueprint.route('/')
28 |     async def index():
29 |         return await render_template('index.html')
30 | 
31 | 
32 | the endpoint is then identified as ``store.index`` for example when
33 | using ``url_for('store.index')``.
34 | 
35 | Nested Blueprints
36 | -----------------
37 | 
38 | It is possible to register a blueprint on another blueprint.
39 | 
40 | .. code-block:: python
41 | 
42 |     parent = Blueprint("parent", __name__, url_prefix="/parent")
43 |     child = Blueprint("child", __name__, url_prefix="/child")
44 |     parent.register_blueprint(child)
45 |     app.register_blueprint(parent)
46 | 
47 | The child blueprint will gain the parent's name as a prefix to its
48 | name, and child URLs will be prefixed with the parent's URL prefix.
49 | 
50 | .. code-block:: python
51 | 
52 |     url_for('parent.child.create')
53 |     /parent/child/create
54 | 
55 | Blueprint-specific before request functions, etc. registered with the
56 | parent will trigger for the child. If a child does not have an error
57 | handler that can handle a given exception, the parent's will be tried.
58 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/command_line.rst:
--------------------------------------------------------------------------------
 1 | .. _command_line:
 2 | 
 3 | Custom Command Line Commands
 4 | ============================
 5 | 
 6 | The ``quart`` command can be customised by the app to add additional
 7 | functionality. A very typical use case is to add a database
 8 | initialisation command,
 9 | 
10 | .. code-block:: python
11 | 
12 |     import click
13 | 
14 |     @app.cli.command()
15 |     def initdb():
16 |         click.echo('Database is migrating')
17 |         ...
18 | 
19 | which will then work as,
20 | 
21 | .. code-block:: console
22 | 
23 |     $ quart initdb
24 |     Database is migrating
25 | 
26 | .. note::
27 | 
28 |    Unlike Flask the Quart commands do not run within an app context,
29 |    as click commands are synchronous rather than asynchronous.
30 | 
31 | Asynchronous usage
32 | ------------------
33 | 
34 | The best way to use some asynchronous code in a custom command is to
35 | create an event loop and run it manually, for example,
36 | 
37 | .. code-block:: python
38 | 
39 |     import asyncio
40 | 
41 |     @app.cli.command()
42 |     def fetch_db_data():
43 |         result = asyncio.get_event_loop().run_until_complete(_fetch())
44 | 
45 | 
46 |     async def _fetch():
47 |         return await db.execute(...)
48 | 
49 | Including a CLI Command in an extension or another module
50 | ---------------------------------------------------------
51 | 
52 | To include CLI commands in a Quart extension or blueprint, register the methods in the "run" factory function
53 | 
54 | .. code-block:: python
55 | 
56 |     from quart import Quart
57 |     from my_extension import my_cli
58 | 
59 |     def create_app():
60 |         app = Quart(__name__)
61 |         app = my_cli(app)
62 |         return app
63 | 
64 | And in your module or extension:
65 | 
66 | .. code-block:: python
67 | 
68 |     import click
69 | 
70 |     def my_cli(app):
71 |         # @click.option("--my-option")
72 | 	@app.cli.command("mycli")
73 | 	def my_cli_command():
74 |             print("quart ran this command")
75 | 
76 |         return app
77 | 
78 | This can be run with:
79 | 
80 | .. code-block:: console
81 | 
82 |     $ quart mycli
83 |     $ quart ran this command
84 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/configuration.rst:
--------------------------------------------------------------------------------
 1 | .. _configuration:
 2 | 
 3 | Configuration
 4 | =============
 5 | 
 6 | A common pattern is to store configuration values in the
 7 | environment. Quart supports this via
 8 | :meth:`~quart.Config.from_prefixed_env` which can be used to load
 9 | environment variables into the configuration. Only environment
10 | variables starting with the prefix, default ``QUART_`` will be
11 | loaded. For example if the environment variable ``QUART_TESTING=true``
12 | is set then,
13 | 
14 | .. code-block:: python
15 | 
16 |     app = Quart(__name__)
17 |     app.config.from_prefixed_env()
18 |     assert app.config["TESTING"] is True
19 | 
20 | Another common pattern for configuration loading is to use class
21 | inheritance to define common settings with production and development
22 | overrides, for example,
23 | 
24 | .. code-block:: python
25 | 
26 |     class Config:
27 |         DEBUG = False
28 |         TESTING = False
29 |         SECRET_KEY = 'secret'
30 | 
31 |     class Development(Config):
32 |         DEBUG = True
33 | 
34 |     class Production(Config):
35 |         SECRET_KEY = 'an actually secret key'
36 | 
37 | This can then be loaded in say a ``create_app`` function, for example:
38 | 
39 | .. code-block:: python
40 | 
41 |     def create_app(mode='Development'):
42 |         """In production create as app = create_app('Production')"""
43 |         app = Quart(__name__)
44 |         app.config.from_object(f"config.{mode}")
45 |         return app
46 | 
47 | Custom configuration class
48 | --------------------------
49 | 
50 | The :attr:`~quart.Quart.config_class` can be set to a custom class,
51 | however it must be changed before the app is initialised as the
52 | :meth:`~quart.Quart.make_config` is called on construction.
53 | 
54 | Instance folders
55 | ----------------
56 | 
57 | An instance folder is a deployment specific location to store files
58 | and configuration settings. As opposed to loading files relative to
59 | the app root path :meth:`~quart.Quart.open_resource` you can load
60 | files relative to an instance path
61 | :meth:`~quart.Quart.open_instance_resource` including the
62 | configuration. To load the configuration from this folder, instead of
63 | relative to the app root path simply provide the
64 | ``instance_relative_config`` argument as ``True`` when initialising
65 | the app ``app = Quart(__name__, instance_relative_config=True)``.
66 | 
67 | The instance path can be specified when initialising the app, or found
68 | automatically if it exists. The search locations are::
69 | 
70 |     /app.py
71 |     /instance/
72 | 
73 | or if the app has been installed::
74 | 
75 |     $PREFIX/var/app-instance/
76 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/developing.rst:
--------------------------------------------------------------------------------
  1 | .. _developing:
  2 | 
  3 | Developing with Quart
  4 | =====================
  5 | 
  6 | When developing it is best to have your Quart app running so you can
  7 | test any changes you make directly. This is made easier by the
  8 | reloader which reloads your app whenever a file is changed. The
  9 | reloader is active if you use ``app.run()``, ``quart run`` command or
 10 | ``run_task`` method.
 11 | 
 12 | 
 13 | Quart run
 14 | ---------
 15 | 
 16 | The ``quart run`` command is the recommended way to develop with Quart
 17 | and will run whichever app is specified by the ``QUART_APP``
 18 | environment variable. For example,
 19 | 
 20 | .. code-block:: python
 21 |     :caption: run.py
 22 | 
 23 |     from quart import Quart
 24 | 
 25 |     app = Quart(__name__)
 26 | 
 27 |     ...
 28 | 
 29 | .. code-block:: console
 30 | 
 31 |     $ QUART_APP=run:app quart run
 32 | 
 33 | The ``quart run`` command comes with ``--host``, and ``--port`` to
 34 | specify where the app is served, and ``--certfile`` and ``--keyfile``
 35 | to specify the SSL certificates to use.
 36 | 
 37 | app.run()
 38 | ---------
 39 | 
 40 | The Quart class, instances typically named ``app``, has a
 41 | :meth:`~quart.Quart.run` method. This method runs a development server,
 42 | automatically turning on debug mode and code reloading. This can be
 43 | used to run the app via this snippet,
 44 | 
 45 | .. code-block:: python
 46 |     :caption: run.py
 47 | 
 48 |     from quart import Quart
 49 | 
 50 |     app = Quart(__name__)
 51 | 
 52 |     ...
 53 | 
 54 |     if __name__ == "__main__":
 55 |         app.run()
 56 | 
 57 | with the ``if`` ensuring that this code only runs if the file is run
 58 | directly, i.e.
 59 | 
 60 | .. code-block:: console
 61 | 
 62 |     $ python run.py
 63 | 
 64 | which ensures that it doesn't run in production.
 65 | 
 66 | The :meth:`~quart.Quart.run` method has options to set the ``host``,
 67 | and ``port`` the app will be served over, to turn off the reloader via
 68 | ``use_reloader=False``, and to add specify SSL certificates via the
 69 | ``certfile`` and ``keyfile`` options.
 70 | 
 71 | .. note::
 72 | 
 73 |    The :meth:`~quart.Quart.run` method will create a new event loop,
 74 |    use ``run_task`` instead if you wish to control the event loop.
 75 | 
 76 | app.run_task
 77 | ------------
 78 | 
 79 | The Quart class also has a :meth:`~quart.Quart.run_task` method with
 80 | the same options as the :meth:`~quart.Quart.run` method. The
 81 | ``run_task`` returns an asyncio task that when awaited will run the
 82 | app. This is as useful as it makes no alterations to the event
 83 | loop. The ``run_task`` can be used as so,
 84 | 
 85 | .. code-block:: python
 86 |     :caption: run.py
 87 | 
 88 |     import asyncio
 89 | 
 90 |     from quart import Quart
 91 | 
 92 |     app = Quart(__name__)
 93 | 
 94 |     ...
 95 | 
 96 |     if __name__ == "__main__":
 97 |         asyncio.run(app.run_task())
 98 | 
 99 | with the ``if`` ensuring that this code only runs if the file is run
100 | directly, i.e.
101 | 
102 | .. code-block:: console
103 | 
104 |     $ python run.py
105 | 
106 | which ensures that it doesn't run in production.
107 | 
108 | 
109 | Curl
110 | ----
111 | 
112 | To test the app locally I like to use a web browser, and the curl
113 | command line tool. I'd recommend reading the curl `documentation
114 | <https://curl.se/docs/>`_ and always using the ``-v``, ``--verbose``
115 | option. For example,
116 | 
117 | .. code-block:: console
118 | 
119 |     $ curl -v localhost:5000/
120 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/disconnections.rst:
--------------------------------------------------------------------------------
 1 | .. _detecting_disconnection:
 2 | 
 3 | Detecting disconnection
 4 | =======================
 5 | 
 6 | If in your route or websocket handler (or code called from within it)
 7 | you are awaiting and the client disconnects the await will raise a
 8 | ``CancelledError``. This can be used to detect when a client
 9 | disconnects, to allow for cleanup, for example the sse handler from
10 | the :ref:`broadcast_tutorial` uses this to remove clients on
11 | disconnect,
12 | 
13 | .. code-block:: python
14 | 
15 |     @app.route('/sse')
16 |     async def sse():
17 |         queue = asyncio.Queue()
18 |         app.clients.add(queue)
19 |         async def send_events():
20 |             while True:
21 |                 try:
22 |                     data = await queue.get()
23 |                     event = ServerSentEvent(data)
24 |                     yield event.encode()
25 |                 except asyncio.CancelledError:
26 |                     app.clients.remove(queue)
27 | 
28 | or with only the relevant parts,
29 | 
30 | .. code-block:: python
31 | 
32 |     @app.route('/sse')
33 |     async def sse():
34 |         try:
35 |             await ...
36 |         except asyncio.CancelledError:
37 |             # Has disconnected
38 | 
39 | The same applies for WebSockets, streaming the request, etc...
40 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/event_loop.rst:
--------------------------------------------------------------------------------
 1 | .. _event_loop:
 2 | 
 3 | Customise the Event Loop
 4 | ========================
 5 | 
 6 | Customising the event loop is often desired in order to use Quart with
 7 | another library whilst ensuring both use the same loop.  The best practice is
 8 | to create/initialise the third party within the loop created by Quart,
 9 | by using :ref:`startup_shutdown` ``before_serving`` functions as so,
10 | 
11 | .. code-block:: python
12 | 
13 |     @app.before_serving
14 |     async def startup():
15 |         loop = asyncio.get_event_loop()
16 |         app.smtp_server = loop.create_server(aiosmtpd.smtp.SMTP, port=1025)
17 |         loop.create_task(app.smtp_server)
18 | 
19 |     @app.after_serving
20 |     async def shutdown():
21 |         app.smtp_server.close()
22 | 
23 | Do not follow this pattern, typically seen in examples, because this creates a
24 | new loop separate from the Quart loop for ThirdParty,
25 | 
26 | .. code-block:: python
27 | 
28 |     loop = asyncio.get_event_loop()
29 |     third_party = ThirdParty(loop)
30 |     app.run()  # A new loop is created by default
31 | 
32 | Controlling the event loop
33 | --------------------------
34 | 
35 | It is the ASGI server running running Quart that owns the event loop
36 | that Quart runs within, by default the server is Hypercorn. Both Quart
37 | and Hypercorn allow the loop to be specified, the Quart shortcut in
38 | development is to pass the loop to the ``app.run`` method,
39 | 
40 | .. code-block:: python
41 | 
42 |     loop = asyncio.get_event_loop()
43 |     third_party = ThirdParty(loop)
44 |     app.run(loop=loop)
45 | 
46 | or to use the ``app.run_task`` method,
47 | 
48 | .. code-block:: python
49 | 
50 |     loop = asyncio.get_event_loop()
51 |     third_party = ThirdParty(loop)
52 |     loop.run_until_complete(app.run_task())
53 | 
54 | the Hypercorn (production) solution is to utilise the `Hypercorn API
55 | <https://hypercorn.readthedocs.io/en/latest/how_to_guides/api_usage.html>`_ to do the
56 | following,
57 | 
58 | .. code-block:: python
59 | 
60 |     from hypercorn.asyncio import serve
61 |     from hypercorn.config import Config
62 |     ...
63 |     loop = asyncio.get_event_loop()
64 |     third_party = ThirdParty(loop)
65 |     loop.run_until_complete(serve(app, Config()))
66 |     # or even
67 |     await serve(app, config)
68 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/flask_extensions.rst:
--------------------------------------------------------------------------------
 1 | .. _flask_extensions:
 2 | 
 3 | Using Flask Extensions
 4 | ======================
 5 | 
 6 | Some Flask extensions can be used with Quart by patching Quart to act
 7 | as Flask, to patch Quart see the `Quart-Flask-Patch
 8 | <https://github.com/pgjones/quart-flask-patch>`_ extension. This was
 9 | part of Quart until release 0.19.0.
10 | 
11 | Reference
12 | ---------
13 | 
14 | More information about Flask extensions can be found
15 | `here <https://flask.palletsprojects.com/extensions>`_.
16 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/flask_migration.rst:
--------------------------------------------------------------------------------
 1 | .. _flask_migration:
 2 | 
 3 | Migration from Flask
 4 | ====================
 5 | 
 6 | As Quart is compatible with the Flask public API it should be
 7 | relatively straight forward to migrate to Quart from Flask. This
 8 | migration basically consists of two steps, firstly replacing Flask
 9 | imports with Quart imports and secondly inserting the relevant
10 | ``async`` and ``await`` keywords.
11 | 
12 | Import changes
13 | --------------
14 | 
15 | Any import of a module from the flask package can be changed to be an
16 | import from the same module in the quart package. For example the
17 | following in Flask,
18 | 
19 | .. code-block:: python
20 | 
21 |     from flask import Flask, g, request
22 |     from flask.helpers import make_response
23 | 
24 | becomes in Quart,
25 | 
26 | .. code-block:: python
27 | 
28 |     from quart import Quart, g, request
29 |     from quart.helpers import make_response
30 | 
31 | noting that the imported objects have the same name in both packages
32 | except for the ``Quart`` and ``Flask`` classes themselves.
33 | 
34 | This can largely be automated via the use of find and replace.
35 | 
36 | Async and Await
37 | ---------------
38 | 
39 | As Quart is an asynchronous framework based on asyncio, it is
40 | necessary to explicitly add ``async`` and ``await`` keywords. The most
41 | notable place in which to do this is route functions, for example the
42 | following in Flask,
43 | 
44 | .. code-block:: python
45 | 
46 |     @app.route('/')
47 |     def route():
48 |         data = request.get_json()
49 |         return render_template_string("Hello {{name}}", name=data['name'])
50 | 
51 | becomes in Quart,
52 | 
53 | .. code-block:: python
54 | 
55 |     @app.route('/')
56 |     async def route():
57 |         data = await request.get_json()
58 |         return await render_template_string("Hello {{name}}", name=data['name'])
59 | 
60 | If you have sufficient test coverage it is possible to search for
61 | awaitables by searching for ``RuntimeWarning: coroutine 'XX' was never
62 | awaited``.
63 | 
64 | The following common lines require awaiting, note that these must be
65 | awaited in functions/methods that are async. Awaiting in a non-async
66 | function/method is a syntax error.
67 | 
68 | .. code-block:: python
69 | 
70 |     await request.data
71 |     await request.get_data()
72 |     await request.json
73 |     await request.get_json()
74 |     await request.form
75 |     await request.files
76 |     await render_template()
77 |     await render_template_string()
78 | 
79 | Testing
80 | -------
81 | 
82 | The test client also requires the usage of async and await keywords,
83 | mostly to await test requests i.e.
84 | 
85 | .. code-block:: python
86 | 
87 |     await test_client.get('/')
88 |     await test_client.post('/')
89 |     await test_client.open('/', 'PUT')
90 | 
91 | Extensions
92 | ----------
93 | 
94 | To use a Flask extension with Quart see the :ref:`flask_extensions`
95 | documentation.
96 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/index.rst:
--------------------------------------------------------------------------------
 1 | =============
 2 | How to guides
 3 | =============
 4 | 
 5 | .. toctree::
 6 |    :maxdepth: 1
 7 | 
 8 | 
 9 |    background_tasks.rst
10 |    blueprints.rst
11 |    command_line.rst
12 |    configuration.rst
13 |    developing.rst
14 |    disconnections.rst
15 |    event_loop.rst
16 |    flask_extensions.rst
17 |    flask_migration.rst
18 |    json_encoding.rst
19 |    logging.rst
20 |    middleware.rst
21 |    request_body.rst
22 |    routing.rst
23 |    server_sent_events.rst
24 |    session_storage.rst
25 |    startup_shutdown.rst
26 |    streaming_response.rst
27 |    sync_code.rst
28 |    templating.rst
29 |    testing.rst
30 |    quart_extensions.rst
31 |    using_http2.rst
32 |    websockets.rst
33 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/json_encoding.rst:
--------------------------------------------------------------------------------
 1 | .. _json_encoding:
 2 | 
 3 | JSON Encoding
 4 | =============
 5 | 
 6 | It is often useful to be able to control how objects are encoded to
 7 | and decoded from JSON. Quart makes this possible via a JSONProvider
 8 | :class:`~quart.json.provider.JSONProvider`.
 9 | 
10 | Money example
11 | -------------
12 | 
13 | As an example lets consider a Money object,
14 | 
15 | .. code-block:: python
16 | 
17 |     class Money:
18 | 
19 |         def __init__(self, amount: Decimal, currency: str) -> None:
20 |             self.amount = amount
21 |             self.currency = currency
22 | 
23 | which we desire to translate to JSON as,
24 | 
25 | .. code-block:: json
26 | 
27 |     {
28 |       "amount": "10.00",
29 |       "currency": "GBP"
30 |     }
31 | 
32 | using encoders and decoders as so,
33 | 
34 | .. code-block:: python
35 | 
36 |     from quart.json.provider import _default, DefaultJSONProvider
37 | 
38 | 
39 |     class MoneyJSONProvider(DefaultJSONProvider):
40 | 
41 |         @staticmethod
42 |         def default(object_):
43 |             if isinstance(object_, date):
44 |                 return http_date(object_)
45 |             if isinstance(object_, (Decimal, UUID)):
46 |                 return str(object_)
47 |             if is_dataclass(object_):
48 |                 return asdict(object_)
49 |             if hasattr(object_, "__html__"):
50 |                 return str(object_.__html__())
51 |             if isinstance(object_, Money):
52 |                 return {'amount': object_.amount, 'currency': object_.currency}
53 | 
54 |             raise TypeError(f"Object of type {type(object_).__name__} is not JSON serializable")
55 | 
56 |         @staticmethod
57 |         def dict_to_object(dict_):
58 |             if 'amount' in dict_ and 'currency' in dict_:
59 |                 return Money(Decimal(dict_['amount']), dict_['currency'])
60 |             else:
61 |                 return dict_
62 | 
63 |         def loads(self, object_, **kwargs):
64 |             return super().loads(object_, object_hook=self.dict_to_object, **kwargs)
65 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/logging.rst:
--------------------------------------------------------------------------------
 1 | .. _how_to_log:
 2 | 
 3 | Logging
 4 | =======
 5 | 
 6 | Quart has a standard Python logger sharing the same name as the
 7 | ``app.name``. To use it, simply make use of
 8 | :attr:`~quart.app.Quart.logger`, for example:
 9 | 
10 | .. code-block:: python
11 | 
12 |     app.logger.info('Interesting')
13 |     app.logger.warning('Easy Now')
14 | 
15 | Configuration
16 | -------------
17 | 
18 | The Quart logger is not created until its first usage, which may occur
19 | as the app is created. These loggers on creation respect any existing
20 | configuration. This allows the loggers to be configured like any other
21 | python logger, for example
22 | 
23 | .. code-block:: python
24 | 
25 |     from logging.config import dictConfig
26 | 
27 |     dictConfig({
28 |         'version': 1,
29 |         'loggers': {
30 |             'quart.app': {
31 |                 'level': 'ERROR',
32 |             },
33 |         },
34 |     })
35 | 
36 | Disabling/removing handlers
37 | ---------------------------
38 | 
39 | The handler :attr:`~quart.logging.default_handler` attached to the
40 | quart logger can be removed like so,
41 | 
42 | .. code-block:: python
43 | 
44 |     from logging import getLogger
45 |     from quart.logging import default_handler
46 | 
47 |     getLogger(app.name).removeHandler(default_handler)
48 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/middleware.rst:
--------------------------------------------------------------------------------
 1 | .. _middleware:
 2 | 
 3 | Middleware
 4 | ==========
 5 | 
 6 | Middleware can be used to wrap a Quart app instance and alter the ASGI
 7 | process. A very simple example would be to reject requests based on
 8 | the presence of a header,
 9 | 
10 | .. code-block:: python
11 | 
12 |     class RejectMiddleware:
13 | 
14 |         def __init__(self, app):
15 |             self.app = app
16 | 
17 |         async def __call__(self, scope, receive, send):
18 |             if "headers" not in scope:
19 |                 return await self.app(scope, receive, send)
20 | 
21 |             for header, value in scope['headers']:
22 |                 if header.lower() == b'x-secret' and value == b'very-secret':
23 |                     return await self.app(scope, receive, send)
24 | 
25 |             return await self.error_response(receive, send)
26 | 
27 |         async def error_response(self, receive, send):
28 |             await send({
29 |                 'type': 'http.response.start',
30 |                 'status': 401,
31 |                 'headers': [(b'content-length', b'0')],
32 |             })
33 |             await send({
34 |                 'type': 'http.response.body',
35 |                 'body': b'',
36 |                 'more_body': False,
37 |             })
38 | 
39 | Whilst middleware can always be used as a wrapper around the app
40 | instance, it is best to assign to and wrap the ``asgi_app`` attribute,
41 | 
42 | .. code-block:: python
43 | 
44 |     quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app)
45 | 
46 | as this ensures that the middleware is applied in any test code.
47 | 
48 | You can combine multiple middleware wrappers,
49 | 
50 | .. code-block:: python
51 | 
52 |     quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app)
53 |     quart_app.asgi_app = AdditionalMiddleware(quart_app.asgi_app)
54 | 
55 | and use any ASGI middleware.
56 | 
57 | .. warning::
58 | 
59 |     Middleware runs before any Quart code, which means that if the
60 |     middleware returns a response no Quart functionality nor any Quart
61 |     extensions will run.
62 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/quart_extensions.rst:
--------------------------------------------------------------------------------
 1 | .. _quart_extensions:
 2 | 
 3 | Using Quart Extensions
 4 | ======================
 5 | 
 6 | There are a number of extensions for Quart, some of which are listed
 7 | here,
 8 | 
 9 | - `Quart-Auth <https://github.com/pgjones/quart-auth>`_ Secure cookie
10 |   sessions, allows login, authentication and logout.
11 | - `Quart-Babel <https://github.com/Quart-Addons/quart-babel>`_ Implements i18n and l10n support for Quart.
12 | - `Quart-Bcrypt <https://github.com/Quart-Addons/quart-bcrypt>`_ Provides bcrypt hashing utilities for your application.
13 | - `Quart-compress <https://github.com/AceFire6/quart-compress>`_
14 |   compress your application's responses with gzip.
15 | - `Quart-compress2
16 |   <https://github.com/DahlitzFlorian/quart-compress>`_ A package to
17 |   compress responses in your Quart app with gzip .
18 | - `Quart-CORS <https://github.com/pgjones/quart-cors>`_ Cross Origin
19 |   Resource Sharing (access control) support.
20 | - `Quart-DB <https://github.com/pgjones/quart-db>`_ Managed
21 |   connection(s) to postgresql database(s).
22 | - `Quart-events <https://github.com/smithk86/quart-events>`_ event
23 |   broadcasting via WebSockets or SSE.
24 | - `Quart-Login <https://github.com/0000matteo0000/quart-login>`_ a
25 |   port of Flask-Login to work natively with Quart.
26 | - `Quart-minify <https://github.com/AceFire6/quart_minify/>`_ minify
27 |   quart response for HTML, JS, CSS and less.
28 | - `Quart-Mongo <https://github.com/Quart-Addons/quart-mongo>`_  Bridges Quart, Motor, and Odmantic to create a powerful MongoDB
29 |   extension.
30 | - `Quart-Motor <https://github.com/marirs/quart-motor>`_ Motor
31 |   (MongoDB) support for Quart applications.
32 | - `Quart-OpenApi <https://github.com/factset/quart-openapi/>`_ RESTful
33 |   API building.
34 | - `Quart-Keycloak <https://github.com/kroketio/quart-keycloak>`_
35 |   Support for Keycloak's OAuth2 OpenID Connect (OIDC).
36 | - `Quart-Rapidoc <https://github.com/marirs/quart-rapidoc>`_ API
37 |   documentation from OpenAPI Specification.
38 | - `Quart-Rate-Limiter
39 |   <https://github.com/pgjones/quart-rate-limiter>`_ Rate limiting
40 |   support.
41 | - `Quart-Redis
42 |   <https://github.com/enchant97/quart-redis>`_ Redis connection handling
43 | - `Webargs-Quart <https://github.com/esfoobar/webargs-quart>`_ Webargs
44 |   parsing for Quart.
45 | - `Quart-SqlAlchemy <https://github.com/joeblackwaslike/quart-sqlalchemy>`_ Quart-SQLAlchemy provides a simple wrapper for SQLAlchemy.
46 | - `Quart-WTF <https://github.com/Quart-Addons/quart-wtf>`_ Simple integration of Quart
47 |   and WTForms. Including CSRF and file uploading.
48 | - `Quart-Schema <https://github.com/pgjones/quart-schema>`_ Schema
49 |   validation and auto-generated API documentation.
50 | - `Quart-session <https://github.com/sanderfoobar/quart-session>`_ server
51 |   side session support.
52 | - `Quart-LibreTranslate <https://github.com/Quart-Addons/quart-libretranslate>`_ Simple integration to
53 |   use LibreTranslate with your Quart app.
54 | - `Quart-Uploads <https://github.com/Quart-Addons/quart-uploads>`_ File upload handling for Quart.
55 | 
56 | Supporting sync code in a Quart Extension
57 | -----------------------------------------
58 | 
59 | Extension authors can support sync functions by utilising the
60 | :meth:`quart.Quart.ensure_async` method. For example, if the extension
61 | provides a view function decorator add ``ensure_async`` before calling
62 | the decorated function,
63 | 
64 | .. code-block:: python
65 | 
66 |     def extension(func):
67 |         @wraps(func)
68 |         async def wrapper(*args, **kwargs):
69 |             ...  # Extension logic
70 |             return await current_app.ensure_async(func)(*args, **kwargs)
71 |         return wrapper
72 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/request_body.rst:
--------------------------------------------------------------------------------
 1 | .. _request_body:
 2 | 
 3 | Consuming the request body
 4 | ==========================
 5 | 
 6 | Requests can come with a body, for example for a POST request the body
 7 | can include form encoded data from a webpage or JSON encoded data from
 8 | a client. The body is sent after the request line and headers for both
 9 | HTTP/1 and HTTP/2. This allows Quart to trigger the app's request
10 | handling code before the full body has been received. Additionally the
11 | requester can choose to stream the request body, especially if the
12 | body is large as is often the case when sending files.
13 | 
14 | Quart follows Flask and provides methods to await the entire body
15 | before continuing,
16 | 
17 | .. code-block:: python
18 | 
19 |     @app.route('/', methods=['POST'])
20 |     async def index():
21 |         await request.get_data()
22 | 
23 | Advanced usage
24 | --------------
25 | 
26 | You may wish to completely control how the request body is consumed,
27 | most likely to consume the data as it is received. To do this quart
28 | provides methods to iterate over the body,
29 | 
30 | .. code-block:: python
31 | 
32 |     from async_timeout import timeout
33 | 
34 |     @app.route('/', methods=['POST'])
35 |     async def index():
36 |         async with timeout(app.config['BODY_TIMEOUT']):
37 |             async for data in request.body:
38 |                 ...
39 | 
40 | .. note::
41 | 
42 |    The above snippet uses `Async-Timeout
43 |    <https://github.com/aio-libs/async-timeout>`_ to ensure the body is
44 |    received within the timeout specified.
45 | 
46 | .. warning::
47 | 
48 |    Whilst the other request methods and attributes for accessing the
49 |    body will timeout if the client takes to long send the
50 |    request. Usage of :attr:`~quart.wrappers.request.Request.body` will
51 |    not and it is up to you to wrap usage in a timeout.
52 | 
53 | .. warning::
54 | 
55 |     Iterating over the body consumes the data, so any further usage of
56 |     the data is not possible unless it is saved during the iteration.
57 | 
58 | Testing
59 | -------
60 | 
61 | To test that a route consumes the body iteratively you will need to use
62 | the :meth:`~quart.testing.client.QuartClient.request` method,
63 | 
64 | .. code-block:: python
65 | 
66 |     async def test_stream() -> None:
67 |         test_client = app.test_client()
68 |         async with test_client.request(...) as connection:
69 |             await connection.send(b"data")
70 |             await connection.send_complete()
71 |         response = await connection.as_response()
72 |         assert response ...
73 | 
74 | it also makes sense to check the code cleans up as you expect if the
75 | client disconnects whilst the request body is being sent,
76 | 
77 | .. code-block:: python
78 | 
79 |     async def test_stream_closed() -> None:
80 |         test_client = app.test_client()
81 |         async with test_client.request(...) as connection:
82 |             await connection.send(b"partial")
83 |             await connection.disconnect()
84 |         # Check cleanup markers....
85 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/routing.rst:
--------------------------------------------------------------------------------
  1 | .. _routing:
  2 | 
  3 | Routing
  4 | =======
  5 | 
  6 | Quart allows for multiple and complex routes to be defined, allowing a
  7 | client to trigger specific code depending on the method and path
  8 | requested.
  9 | 
 10 | The simplest routing is simple static rules, such as the following,
 11 | 
 12 | .. code-block:: python
 13 | 
 14 |     @app.route('/')
 15 |     async def index():
 16 |         ...
 17 | 
 18 |     @app.route('/about')
 19 |     async def about():
 20 |         ...
 21 | 
 22 | which is often sufficient for mostly static websites.
 23 | 
 24 | Dynamic routing
 25 | ---------------
 26 | 
 27 | Dynamic routing can be achieved by using ``<variable>`` markers which
 28 | specify that part of the route can be matched rather than
 29 | pre-defined. For example,
 30 | 
 31 | .. code-block:: python
 32 | 
 33 |     @app.route('/page/<page_no>')
 34 |     async def page(page_no):
 35 |         ...
 36 | 
 37 | will match paths ``/page/1``, ``/page/2``, and ``/page/jeff`` with the
 38 | ``page_no`` argument set to ``'1'``, ``'2'``, and ``'jeff'``
 39 | respectively.
 40 | 
 41 | Converters
 42 | ^^^^^^^^^^
 43 | 
 44 | It is often necessary and useful to specify how the variable should
 45 | convert and by implication match paths. This works by adding the
 46 | converter name before the variable name separated by a colon,
 47 | ``<converter:variable>``. Adapting the example above to,
 48 | 
 49 | .. code-block:: python
 50 | 
 51 |     @app.route('/page/<int:page_no>')
 52 |     async def page(page_no):
 53 |         ...
 54 | 
 55 | will match paths ``/page/1``, and ``/page/2`` with the ``page_no``
 56 | argument set to ``1``, and ``2`` (note types) but will no longer match
 57 | ``/page/jeff`` as ``jeff`` cannot be converted to an int.
 58 | 
 59 | The available converters are,
 60 | 
 61 | ========== ==========================================
 62 | ``float``  positive floating point numbers
 63 | ``int``    positive integers
 64 | ``path``   like ``string`` with slashes
 65 | ``string`` (default) any text without a slash
 66 | ``uuid``   UUID strings
 67 | ========== ==========================================
 68 | 
 69 | note that additional converters can be added to the
 70 | :attr:`~quart.app.Quart.url_map` :attr:`~quart.routing.Map.converters`
 71 | dictionary.
 72 | 
 73 | 
 74 | Catch all route
 75 | ^^^^^^^^^^^^^^^
 76 | 
 77 | A ``/<path:path>`` route definition will catch all requests that do
 78 | not match any other routes.
 79 | 
 80 | 
 81 | Default values
 82 | --------------
 83 | 
 84 | Variable usage can sometimes prove annoying to users, for example
 85 | ``/page/<int:page_no>`` will not match ``/page`` forcing the user to
 86 | specify ``/page/1``. This can be solved by specifying a default value,
 87 | 
 88 | .. code-block:: python
 89 | 
 90 |     @app.route('/page', defaults={'page_no': 1})
 91 |     @app.route('/page/<int:page_no>')
 92 |     async def page(page_no):
 93 |         ...
 94 | 
 95 | which allows ``/page`` to match with ``page_no`` set to ``1``.
 96 | 
 97 | 
 98 | Host matching, host and subdomain
 99 | ---------------------------------
100 | 
101 | Routes can be added to the app with an explicit ``host`` or
102 | ``subdomain`` to match if the app has host matching enabled. This
103 | results in the routes only matching if the host header matches, for
104 | example ``host='quart.com'`` will allow the route to match any request
105 | with a host header of ``quart.com`` and otherwise 404.
106 | 
107 | The ``subdomain`` option can only be used if the app config
108 | ``SERVER_NAME`` is set, as the host will be built up as
109 | ``{subdomain}.{SERVER_NAME}``.
110 | 
111 | Note that the variable converters can be used in the host or subdomain
112 | options.
113 | 
114 | See also
115 | --------
116 | 
117 | Quart uses `Werkzeug's router <https://werkzeug.palletsprojects.com/en/2.1.x/routing/>`_
118 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/server_sent_events.rst:
--------------------------------------------------------------------------------
 1 | .. _server_sent_events:
 2 | 
 3 | Server Sent Events
 4 | ==================
 5 | 
 6 | Quart supports streaming `Server Sent Events
 7 | <https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events>`_,
 8 | which you may decide to use as an alternative to WebSockets -
 9 | especially if the communication is one way.
10 | 
11 | Server sent events must be encoded in a specific way, as shown by this
12 | helper class:
13 | 
14 | .. code-block:: python
15 | 
16 |     from dataclasses import dataclass
17 | 
18 |     @dataclass
19 |     class ServerSentEvent:
20 |         data: str
21 |         event: str | None = None
22 |         id: int | None = None
23 |         retry: int | None
24 | 
25 |         def encode(self) -> bytes:
26 |             message = f"data: {self.data}"
27 |             if self.event is not None:
28 |                 message = f"{message}\nevent: {self.event}"
29 |             if self.id is not None:
30 |                 message = f"{message}\nid: {self.id}"
31 |             if self.retry is not None:
32 |                 message = f"{message}\nretry: {self.retry}"
33 |             message = f"{message}\n\n"
34 |             return message.encode('utf-8')
35 | 
36 | To use a GET route that returns a streaming generator is
37 | required. This generator, ``send_events`` in the code below, must
38 | yield the encoded Server Sent Event. The route itself also needs to
39 | check the client will accept ``text/event-stream`` responses and set
40 | the response headers appropriately:
41 | 
42 | .. code-block:: python
43 | 
44 |     from quart import abort, make_response
45 | 
46 |     @app.get("/sse")
47 |     async def sse():
48 |         if "text/event-stream" not in request.accept_mimetypes:
49 |             abort(400)
50 | 
51 |         async def send_events():
52 |             while True:
53 |                 data = ...  # Up to you where the events are from
54 |                 event = ServerSentEvent(data)
55 |                 yield event.encode()
56 | 
57 |         response = await make_response(
58 |             send_events(),
59 |             {
60 |                 'Content-Type': 'text/event-stream',
61 |                 'Cache-Control': 'no-cache',
62 |                 'Transfer-Encoding': 'chunked',
63 |             },
64 |         )
65 |         response.timeout = None
66 |         return response
67 | 
68 | Quart by default will timeout long responses to protect against
69 | possible denial of service attacks, see :ref:`dos_mitigations`. For
70 | this reason the timeout is disabled. This can be done globally,
71 | however that could make other routes DOS vulnerable, therefore the
72 | recommendation is to set the timeout attribute on the specific
73 | response to ``None``.
74 | 
75 | See also
76 | --------
77 | 
78 | :ref:`streaming_response`
79 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/session_storage.rst:
--------------------------------------------------------------------------------
 1 | .. _session_storage:
 2 | 
 3 | Session Storage
 4 | ===============
 5 | 
 6 | It is often useful to store information relevant to a user of the app
 7 | for the duration of that usage session. For example the user may
 8 | choose to want to save a option or be remembered as logged in. This
 9 | information can either be stored client side or server side and Quart
10 | provides a system to store the information client side via Secure
11 | Cookie Sessions.
12 | 
13 | Secure Cookie Sessions
14 | ----------------------
15 | 
16 | Secure Cookie Sessions store the session information on the Cookie in
17 | plain text with a signature to ensure that the information is not
18 | altered by the client. They can be used in Quart so long as the
19 | :attr:`~quart.app.Quart.secret_key` is set to a **secret**
20 | value.
21 | 
22 | An example usage to store a users colour preference would be,
23 | 
24 | .. code-block:: python
25 | 
26 |     from quart import session
27 |     ...
28 | 
29 | 
30 |     @app.route('/')
31 |     async def index():
32 |         return await render_template(
33 |             'index.html',
34 |             colour=session.get('colour', 'black'),
35 |         )
36 | 
37 |     @app.route('/colour/', methods=['POST'])
38 |     async def set_colour():
39 |         ...
40 |         session['colour'] = colour
41 |         return redirect(url_for('index'))
42 | 
43 | Permanent Sessions
44 | ------------------
45 | 
46 | The cookies used by default are not set to be permanent (deleted when
47 | the browser's session ends) to have permanent cookies
48 | ``session.permanent`` must be ``True`` when the session is
49 | modified. To set this as the default use this snippet,
50 | 
51 | .. code-block:: python
52 | 
53 |     @app.before_request
54 |     def make_session_permanent():
55 |         session.permanent = True
56 | 
57 | WebSockets
58 | ----------
59 | 
60 | Sessions can be used with WebSockets with an important caveat about
61 | cookies. A cookie can only be set on a HTTP response, and an accepted
62 | WebSocket connection cannot return a HTTP response. Therefore the
63 | default implementation, being based on cookies, will lose any
64 | modifications made during an accepted WebSocket connection.
65 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/startup_shutdown.rst:
--------------------------------------------------------------------------------
 1 | .. _startup_shutdown:
 2 | 
 3 | Startup and Shutdown
 4 | ====================
 5 | 
 6 | The `ASGI lifespan specification`_ includes the ability for awaiting
 7 | coroutines before the first byte is received and after the final byte
 8 | is sent, through the ``startup`` and ``shutdown`` lifespan events.
 9 | This is particularly useful for creating and destroying connection
10 | pools.  Quart supports this via the decorators
11 | :func:`~quart.app.Quart.before_serving`,
12 | :func:`~quart.app.Quart.after_serving`, and
13 | :func:`~quart.app.Quart.while_serving` which expects a function that
14 | returns a generator.
15 | 
16 | .. _ASGI lifespan specification: https://github.com/django/asgiref/blob/master/specs/lifespan.rst
17 | 
18 | The decorated functions are all called within the app context,
19 | allowing ``current_app`` and ``g`` to be used.
20 | 
21 | .. warning::
22 | 
23 |     Use ``g`` with caution, as it will reset after startup, i.e. after
24 |     all the ``before_serving`` functions complete and after the
25 |     initial yield in a while serving generator. It can still be used
26 |     within this context. If you want to create something used in
27 |     routes, try storing it on the app instead.
28 | 
29 | To use this functionality simply do the following:
30 | 
31 | .. code-block:: python
32 | 
33 |     @app.before_serving
34 |     async def create_db_pool():
35 |         app.db_pool = await ...
36 |         g.something = something
37 | 
38 |     @app.before_serving
39 |     async def use_g():
40 |         g.something.do_something()
41 | 
42 |     @app.while_serving
43 |     async def lifespan():
44 |         ...  # startup
45 |         yield
46 |         ...  # shutdown
47 | 
48 |     @app.route("/")
49 |     async def index():
50 |         app.db_pool.execute(...)
51 |         # g.something is not available here
52 | 
53 |     @app.after_serving
54 |     async def create_db_pool():
55 |         await app.db_pool.close()
56 | 
57 | Testing
58 | -------
59 | 
60 | Quart's test client works on a request lifespan and hence does not
61 | call ``before_serving``, or ``after_serving`` functions, nor advance
62 | the ``while_serving`` generator. Instead Quart's test app can be used,
63 | for example
64 | 
65 | .. code-block:: python
66 | 
67 |     @pytest.fixture(name="app", scope="function")
68 |     async def _app():
69 |         app = create_app()  # Initialize app
70 |         async with app.test_app() as test_app:
71 |             yield test_app
72 | 
73 | The app fixture can then be used as normal, knowing that the
74 | ``before_serving``, and ``after_serving`` functions have been called,
75 | and the ``while_serving`` generator has been advanced,
76 | 
77 | .. code-block:: python
78 | 
79 |     async def test_index(app):
80 |         test_client = app.test_client()
81 |         await test_client.get("/")
82 |         ...
83 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/streaming_response.rst:
--------------------------------------------------------------------------------
 1 | .. _streaming_response:
 2 | 
 3 | Streaming responses
 4 | ===================
 5 | 
 6 | Quart supports responses that are meant to be streamed to the client,
 7 | rather than received in one block. If you are interested in streaming
 8 | the request data see :ref:`request_body` or for duplex streaming see
 9 | :ref:`websockets`.
10 | 
11 | To stream a response the view-function should return an asynchronous
12 | generator that yields bytes. This generator can be returned with a
13 | status code and headers as normal. For example to stream the time
14 | every second,
15 | 
16 | .. code-block:: python
17 | 
18 |     @app.route('/')
19 |     async def stream_time():
20 |         async def async_generator():
21 |             time = datetime.isoformat()
22 |             yield time.encode()
23 |         return async_generator(), 200, {'X-Something': 'value'}
24 | 
25 | With context
26 | ------------
27 | 
28 | If you want to make use of the ``request`` context whilst streaming
29 | you will need to use the :func:`quart.helpers.stream_with_context`
30 | decorator,
31 | 
32 | .. code-block:: python
33 | 
34 |     @app.route('/')
35 |     async def stream_time():
36 |         @stream_with_context
37 |         async def async_generator():
38 |             time = datetime.isoformat()
39 |             yield time.encode()
40 |         return async_generator(), 200, {'X-Something': 'value'}
41 | 
42 | Timeout
43 | -------
44 | 
45 | Quart by default will timeout long responses to protect against
46 | possible denial of service attacks, see :ref:`dos_mitigations`. This
47 | may be undesired for streaming responses, e.g. an indefinite
48 | stream. The timeout can be disabled globally, however this could make
49 | other routes DOS vulnerable, therefore the recommendation is to set
50 | the timeout attribute on a specific response to ``None``,
51 | 
52 | .. code-block:: python
53 | 
54 |     from quart import make_response
55 | 
56 |     @app.route('/sse')
57 |     async def stream_time():
58 |         ...
59 |         response = await make_response(async_generator())
60 |         response.timeout = None  # No timeout for this route
61 |         return response
62 | 
63 | Testing
64 | -------
65 | 
66 | The test client :meth:`~quart.testing.client.QuartClient.get` and
67 | associated methods will collate the entire streamed response. If you
68 | want to test that the route actually streams the response, or to test
69 | routes that stream until the client disconnects you will need to use
70 | the :meth:`~quart.testing.client.QuartClient.request` method,
71 | 
72 | .. code-block:: python
73 | 
74 |     async def test_stream() -> None:
75 |         test_client = app.test_client()
76 |         async with test_client.request(..) as connection:
77 |             data = await connection.receive()
78 |             assert data ...
79 |             assert connection.status_code == 200
80 |             ...
81 |             await connection.disconnect()  # For infinite streams
82 | 
83 | See also
84 | --------
85 | 
86 | :ref:`server_sent_events`
87 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/sync_code.rst:
--------------------------------------------------------------------------------
 1 | .. _sync_code:
 2 | 
 3 | Run synchronous code
 4 | ====================
 5 | 
 6 | Synchronous code will block the event loop and degrade the performance
 7 | of the Quart application it is run in. This is because the synchronous
 8 | code will block the task it is run in and in addition block the event
 9 | loop. It is for this reason that synchronous code is best avoided,
10 | with asynchronous versions used in preference.
11 | 
12 | It is likely though that you will need to use a third party library
13 | that is synchronous as there is no asynchronous version to use in its
14 | place. In this situation it is common to run the synchronous code in a
15 | thread pool executor so that it doesn't block the event loop and hence
16 | degrade the performance of the Quart application. This can be a bit
17 | tricky to do, so Quart provides some helpers to do so. Firstly any
18 | synchronous route will be run in an executor, i.e.
19 | 
20 | .. code-block:: python
21 | 
22 |     @app.route("/")
23 |     def sync():
24 |         method = request.method
25 |         ...
26 | 
27 | will result in the sync function being run in a thread. Note that you
28 | are still within the :ref:`contexts`, and hence you can still access
29 | the ``request``, ``current_app`` and other globals.
30 | 
31 | The following functionality accepts synchronous functions and will run
32 | them in a thread,
33 | 
34 | - Route handlers
35 | - Endpoint handlers
36 | - Error handlers
37 | - Context processors
38 | - Before request
39 | - Before websocket
40 | - Before first request
41 | - Before serving
42 | - After request
43 | - After websocket
44 | - After serving
45 | - Teardown request
46 | - Teardown websocket
47 | - Teardown app context
48 | - Open session
49 | - Make null session
50 | - Save session
51 | 
52 | Context usage
53 | -------------
54 | 
55 | Whilst you can access the ``request`` and other globals in synchronous
56 | routes you will be unable to await coroutine functions. To work around
57 | this Quart provides :meth:`~quart.app.Quart.run_sync` which can be
58 | used as so,
59 | 
60 | .. code-block:: python
61 | 
62 |     @app.route("/")
63 |     async def sync_within():
64 |         data = await request.get_json()
65 | 
66 |         def sync_processor():
67 |              # does something with data
68 |              ...
69 | 
70 |         result = await run_sync(sync_processor)()
71 |         return result
72 | 
73 | this is similar to utilising the asyncio run_in_executor function,
74 | 
75 | .. code-block:: python
76 | 
77 |     @app.route("/")
78 |     async def sync_within():
79 |         data = await request.get_json()
80 | 
81 |         def sync_processor():
82 |              # does something with data
83 |              ...
84 | 
85 |         result = await asyncio.get_running_loop().run_in_executor(
86 |             None, sync_processor
87 |         )
88 |         return result
89 | 
90 | .. note::
91 | 
92 |    The run_in_executor function does not copy the current context,
93 |    whereas the run_sync method does. It is for this reason that the
94 |    latter is recommended. Without the copied context the ``request``
95 |    and other globals will not be accessible.
96 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/templating.rst:
--------------------------------------------------------------------------------
 1 | .. _templating:
 2 | 
 3 | Templates
 4 | =========
 5 | 
 6 | Quart uses the `Jinja <https://jinja.palletsprojects.com>`_ templating engine,
 7 | which is well `documented
 8 | <https://jinja.palletsprojects.com/templates/>`_. Quart adds a standard
 9 | context, and some standard filters to the Jinja defaults. Quart also
10 | adds the ability to define custom filters, tests and contexts at an
11 | app and blueprint level.
12 | 
13 | There are two functions to use when templating,
14 | :func:`~quart.templating.render_template` and
15 | :func:`~quart.templating.render_template_string`, both must be
16 | awaited. The return value from either function is a string and can
17 | form a route response directly or be otherwise combined. Both
18 | functions take an variable number of additional keyword arguments to
19 | pass to the template as context, for example,
20 | 
21 | .. code-block:: python
22 | 
23 |     @app.route('/')
24 |     async def index():
25 |         return await render_template('index.html', hello='world')
26 | 
27 | Quart standard extras
28 | ---------------------
29 | 
30 | The standard context includes the ``config``, ``request``,
31 | ``session``, and ``g`` with these objects referencing the
32 | ``current_app.config`` and those defined in :mod:`~quart.globals`
33 | respectively. The can be accessed as expected,
34 | 
35 | .. code-block:: python
36 | 
37 |     @app.route('/')
38 |     async def index():
39 |         return await render_template_string("{{ request.endpoint }}")
40 | 
41 | The standard global functions are :func:`~quart.helpers.url_for` and
42 | :func:`~quart.helpers.get_flashed_messages`. These can be used as expected,
43 | 
44 | .. code-block:: python
45 | 
46 |     @app.route('/')
47 |     async def index():
48 |         return await render_template_string("<a href="{{ url_for('index') }}>index</a>")
49 | 
50 | Adding filters, tests, globals and context
51 | ------------------------------------------
52 | 
53 | To add a filter for usage in templates, make use of
54 | :meth:`~quart.app.Quart.template_filter` or
55 | :meth:`~quart.blueprints.Blueprint.app_template_filter` as decorators,
56 | or :meth:`~quart.app.Quart.add_template_filter` or
57 | :meth:`~quart.blueprints.Blueprint.add_app_template_filter` as
58 | functions. These expect the filter to take in Any value and return a
59 | str, e.g.
60 | 
61 | .. code-block:: python
62 | 
63 |     @app.template_filter(name='upper')
64 |     def upper_case(value):
65 |         return value.upper()
66 | 
67 |     @app.route('/')
68 |     async def index():
69 |         return await render_template_string("{{ lower | upper }}")
70 | 
71 | tests and globals work in a very similar way only with the test and
72 | global methods rather than filter.
73 | 
74 | The context processors however have an additional feature, in that
75 | they can be specified on a per blueprint basis. This allows contextual
76 | information to be present only for requests that are routed to the
77 | blueprint. By default
78 | :meth:`~quart.blueprints.Blueprint.context_processor` adds contextual
79 | information to blueprint routed requests whereas
80 | :meth:`~quart.blueprints.Blueprint.app_context_processor` adds the
81 | information to all requests to the app. An example,
82 | 
83 | .. code-block:: python
84 | 
85 |     @blueprint.context_processor
86 |     async def blueprint_only():
87 |         return {'context': 'value'}
88 | 
89 |     @blueprint.app_context_processor
90 |     async def app_wide():
91 |         return {'context': 'value'}
92 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/testing.rst:
--------------------------------------------------------------------------------
  1 | .. _testing:
  2 | 
  3 | Testing
  4 | =======
  5 | 
  6 | Quart's usage of global variables (``request`` etc) makes testing any
  7 | code that uses these variables more difficult. To combat this it is
  8 | best practice to only use these variables in the code directly called
  9 | by Quart e.g. route functions or before request functions. Thereafter
 10 | Quart provides a testing framework to control these globals.
 11 | 
 12 | Primarily testing should be done using a test client bound to the
 13 | Quart app being tested. As this is so common there is a helper method
 14 | :meth:`~quart.app.Quart.test_client` which returns a bound client,
 15 | e.g.
 16 | 
 17 | .. code-block:: python
 18 | 
 19 |     async def test_app(app):
 20 |         client = app.test_client()
 21 |         response = await client.get('/')
 22 |         assert response.status_code == 200
 23 | 
 24 | Event loops
 25 | -----------
 26 | 
 27 | To test with quart you will need to have an event loop in order to
 28 | call the async functions. This is possible to do manually, for example
 29 | 
 30 | .. code-block:: python
 31 | 
 32 |     def aiotest(func):
 33 |         loop = asyncio.get_event_loop()
 34 |         loop.run_until_complete(func())
 35 | 
 36 |     @aiotest
 37 |     async def test_app(app)
 38 |         ...
 39 | 
 40 | However it is much easier to use ``pytest-asyncio`` and the to do this
 41 | for you. Note that ``pytest`` is the recommended test runner and the
 42 | examples throughout assume ``pytest`` is used with ``pytest-asyncio``.
 43 | 
 44 | Calling routes
 45 | --------------
 46 | 
 47 | The test client has helper methods for all the HTTP verbs
 48 | e.g. :meth:`~quart.testing.QuartClient.post`. These are helper methods
 49 | for :meth:`~quart.testing.QuartClient.open`, as such all the methods at
 50 | a minimum expect a path and optionally can have query parameters, json
 51 | or form data. A standard :class:`~quart.wrappers.Response` class is
 52 | returned. An example:
 53 | 
 54 | .. code-block:: python
 55 | 
 56 |     async def test_create(app):
 57 |         test_client = app.test_client()
 58 |         data = {'name': 'foo'}
 59 |         response = await test_client.post('/resource/', json=data)
 60 |         assert response.status_code == 201
 61 |         result = await response.get_json()
 62 |         assert result == data
 63 | 
 64 | To test test routes which stream requests or responses, use the
 65 | :meth:`~quart.testing.client.QuartClient.request` method:
 66 | 
 67 | .. code-block:: python
 68 | 
 69 |     async def test_stream() -> None:
 70 |         test_client = app.test_client()
 71 |         async with test_client.request(...) as connection:
 72 |             await connection.send(b"data")
 73 |             await connection.send_complete()
 74 |             ...
 75 |             # receive a chunk of the response
 76 |             data = await connection.receive()
 77 |             ...
 78 |         # assemble the rest of the response without the first bit
 79 |         response = await connection.as_response()
 80 | 
 81 | To learn more about streaming requests and responses, read :ref:`request_body`
 82 | and :ref:`streaming_response`.
 83 | 
 84 | Context testing
 85 | ---------------
 86 | 
 87 | It is often necessary to test something within the app or request
 88 | contexts.  This is simple enough for the app context,
 89 | 
 90 | .. code-block:: python
 91 | 
 92 |     async def test_app_context(app):
 93 |         async with app.app_context():
 94 |             current_app.[use]
 95 | 
 96 | for the request context however the request context has to be faked,
 97 | at a minimum this means the method and path must be supplied, e.g.
 98 | 
 99 | .. code-block:: python
100 | 
101 |     async def test_app_context(app):
102 |         async with app.test_request_context("/", method="GET"):
103 |             request.[use]
104 | 
105 | .. note::
106 | 
107 |     Any ``before_request`` or ``after_request`` functions are not
108 |     called when using the ``test_request_context``. You can add
109 |     ``await app.preprocess_request()`` to ensure the
110 |     ``before_request`` functions are called.
111 | 
112 | .. code-block:: python
113 | 
114 |     async def test_app_context(app):
115 |         async with app.test_request_context("/", method="GET"):
116 |             await app.preprocess_request()
117 |             # The before_request functions have now been called
118 |             request.[use]
119 | 


--------------------------------------------------------------------------------
/docs/how_to_guides/using_http2.rst:
--------------------------------------------------------------------------------
 1 | .. _using_http2:
 2 | 
 3 | Using HTTP/2
 4 | ============
 5 | 
 6 | `HTTP/2 <https://http2.github.io/>`__ is the second major version of
 7 | the Hyper Text Transfer Protocol used to transfer web data.
 8 | 
 9 | .. note::
10 | 
11 |     Not all ASGI Servers support HTTP/2. The recommended ASGI server,
12 |     Hypercorn, does.
13 | 
14 | To use HTTP/2 in development you will need to create some SSL
15 | certificates and run Quart with SSL.
16 | 
17 | Server push or push promises
18 | ----------------------------
19 | 
20 | With `HTTP/2 <http://httpwg.org/specs/rfc7540.html#PushResources>`__
21 | the server can choose to pre-emptively push additional responses to
22 | the client, this is termed a server push and the response itself is
23 | called a push promise. Server push is very useful when the server
24 | knows the client will likely initiate a request, say for the css or js
25 | referenced in a html response.
26 | 
27 | .. note::
28 | 
29 |    Browsers are deprecating support for server push, and usage is not
30 |    recommended. This section is kept for reference.
31 | 
32 | In Quart server push can be initiated during a request via the
33 | function :func:`~quart.helpers.make_push_promise`, for example,
34 | 
35 | .. code-block:: python
36 | 
37 |     async def index():
38 |         await make_push_promise(url_for('static', filename='css/minimal.css'))
39 |         return await render_template('index.html')
40 | 
41 | The push promise will include (copy) header values present in the
42 | request that triggers the push promise. These are to ensure that the
43 | push promise is responded too as if the request had made it. A good
44 | example is the ``Accept`` header. The full set of copied headers are
45 | ``SERVER_PUSH_HEADERS_TO_COPY`` in the request module.
46 | 
47 | .. note::
48 | 
49 |     This functionality is only useable with ASGI servers that
50 |     implement the ``HTTP/2 Server Push`` extension. If the server does
51 |     not support this extension Quart will ignore the push promises (as
52 |     with HTTP/1 connections). Hypercorn, the recommended ASGI server,
53 |     supports this extension.
54 | 
55 | When testing server push,the :class:`~quart.testing.QuartClient`
56 | ``push_promises`` list will contain every push promise as a tuple of
57 | the path and headers, for example,
58 | 
59 | .. code-block:: python
60 | 
61 |     async def test_push_promise():
62 |         test_client = app.test_client()
63 |         await test_client.get("/push")
64 |         assert test_client.push_promises[0] == ("/", {})
65 | 
66 | HTTP/2 clients
67 | --------------
68 | 
69 | At the time of writing there aren't that many HTTP/2 clients. The best
70 | option is to use a browser and inspect the network connections (turn
71 | on the protocol information). Otherwise curl can be used, if HTTP/2
72 | support is `installed <https://curl.haxx.se/docs/http2.html>`_, as so,
73 | 
74 | .. code-block:: console
75 | 
76 |     $ curl --http2 ...
77 | 
78 | If you wish to communicate via HTTP/2 in python `httpx
79 | <https://github.com/encode/httpx>`_ is the best choice.
80 | 


--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
  1 | :orphan:
  2 | 
  3 | .. title:: Quart documentation
  4 | 
  5 | .. image:: _static/quart-name.svg
  6 |     :class: only-light
  7 |     :height: 200px
  8 |     :align: center
  9 | 
 10 | .. image:: _static/quart-name-dark.svg
 11 |     :class: only-dark
 12 |     :height: 200px
 13 |     :align: center
 14 | 
 15 | Quart
 16 | =====
 17 | 
 18 | Quart is a Fast Python web microframework. Using Quart you can,
 19 | 
 20 |  * write JSON APIs e.g. :ref:`a RESTful API<api_tutorial>`,
 21 |  * render and serve HTML e.g. :ref:`a blog<blog_tutorial>`,
 22 |  * serve WebSockets e.g. :ref:`a simple chat<chat_tutorial>`,
 23 |  * stream responses e.g. :ref:`serve video<video_tutorial>`,
 24 |  * all of the above in a single app,
 25 |  * or do pretty much anything over the HTTP or WebSocket protocols.
 26 | 
 27 | With all of the above possible using asynchronous (asyncio)
 28 | libraries/code or :ref:`synchronous<sync_code>` libraries/code.
 29 | 
 30 | If you are,
 31 | 
 32 |  * new to Python then start by reading :ref:`installation` instructions,
 33 |  * new to Quart then try the :ref:`quickstart`,
 34 |  * new to asyncio see the :ref:`asyncio` guide,
 35 |  * migrating from Flask see :ref:`flask_migration`,
 36 |  * looking for a cheatsheet then look :ref:`here<cheatsheet>`.
 37 | 
 38 | Quart is an asyncio reimplementation of the popular `Flask
 39 | <https://flask.palletsprojects.com>`_ microframework API. This means that if you
 40 | understand Flask you understand Quart. See :ref:`flask_evolution` to
 41 | learn more about how Quart builds on Flask.
 42 | 
 43 | Like Flask Quart has an ecosystem of
 44 | :ref:`extensions<quart_extensions>` for more specific needs. In
 45 | addition a number of the Flask :ref:`extensions<flask_extensions>`
 46 | work with Quart.
 47 | 
 48 | Quart is developed on `Github <https://github.com/pallets/quart>`_. If
 49 | you come across an issue, or have a feature request please open an
 50 | `issue <https://github.com/pallets/quart/issues>`_.If you want to
 51 | contribute a fix or the feature-implementation please do (typo fixes
 52 | welcome), by proposing a `merge request
 53 | <https://github.com/pallets/quart/merge_requests>`_. If you want to
 54 | ask for help try `on discord <https://discord.gg/pallets>`_.
 55 | 
 56 | .. note::
 57 | 
 58 |     If you can't find documentation for what you are looking for here,
 59 |     remember that Quart is an implementation of the Flask API and
 60 |     hence the `Flask documentation <https://flask.palletsprojects.com>`_ is
 61 |     a great source of help. Quart is also built on the `Jinja
 62 |     <https://flask.palletsprojects.com>`_ template engine and the `Werkzeug
 63 |     <https://werkzeug.palletsprojects.com>`_ toolkit.
 64 | 
 65 |     The Flask documentation is so good that you may be better placed
 66 |     consulting it first then returning here to check how Quart
 67 |     differs.
 68 | 
 69 | Tutorials
 70 | ---------
 71 | 
 72 | .. toctree::
 73 |    :maxdepth: 2
 74 | 
 75 |    tutorials/index.rst
 76 | 
 77 | How to guides
 78 | -------------
 79 | 
 80 | .. toctree::
 81 |    :maxdepth: 2
 82 | 
 83 |    how_to_guides/index.rst
 84 | 
 85 | Discussion
 86 | ----------
 87 | 
 88 | .. toctree::
 89 |    :maxdepth: 2
 90 | 
 91 |    discussion/index.rst
 92 | 
 93 | References
 94 | ----------
 95 | 
 96 | .. toctree::
 97 |     :maxdepth: 2
 98 | 
 99 |     reference/index
100 |     license
101 |     changes
102 | 


--------------------------------------------------------------------------------
/docs/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 | ===========
3 | 
4 | ```{literalinclude} ../LICENSE.txt
5 | :language: text
6 | ```
7 | 


--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
 1 | @ECHO OFF
 2 | 
 3 | pushd %~dp0
 4 | 
 5 | REM Command file for Sphinx documentation
 6 | 
 7 | if "%SPHINXBUILD%" == "" (
 8 | 	set SPHINXBUILD=python -msphinx
 9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=Quart
13 | 
14 | if "%1" == "" goto help
15 | 
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | 	echo.
19 | 	echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | 	echo.then set the SPHINXBUILD environment variable to point to the full
21 | 	echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | 	echo.Sphinx directory to PATH.
23 | 	echo.
24 | 	echo.If you don't have Sphinx installed, grab it from
25 | 	echo.http://sphinx-doc.org/
26 | 	exit /b 1
27 | )
28 | 
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 | 
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 | 
35 | :end
36 | popd
37 | 


--------------------------------------------------------------------------------
/docs/reference/api.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =============
3 | 
4 | .. toctree::
5 |    :maxdepth: 2
6 |    :caption: Contents:
7 | 
8 |    source/modules.rst
9 | 


--------------------------------------------------------------------------------
/docs/reference/cheatsheet.rst:
--------------------------------------------------------------------------------
  1 | .. _cheatsheet:
  2 | 
  3 | Cheatsheet
  4 | ==========
  5 | 
  6 | Basic App
  7 | ---------
  8 | 
  9 | .. code-block:: python
 10 | 
 11 |     from quart import Quart
 12 | 
 13 |     app = Quart(__name__)
 14 | 
 15 |     @app.route("/hello")
 16 |     async def hello():
 17 |         return "Hello, World!"
 18 | 
 19 |     if __name__ == "__main__":
 20 |         app.run(debug=True)
 21 | 
 22 | Routing
 23 | -------
 24 | 
 25 | .. code-block:: python
 26 | 
 27 |     @app.route("/hello/<string:name>")  # example.com/hello/quart
 28 |     async def hello(name):
 29 |         return f"Hello, {name}!"
 30 | 
 31 | Request Methods
 32 | ---------------
 33 | 
 34 | .. code-block:: python
 35 | 
 36 |     @app.route("/get")  # GET Only by default
 37 |     @app.route("/get", methods=["GET", "POST"])  # GET and POST
 38 |     @app.route("/get", methods=["DELETE"])  # Just DELETE
 39 | 
 40 | JSON Responses
 41 | --------------
 42 | 
 43 | .. code-block:: python
 44 | 
 45 |     @app.route("/hello")
 46 |     async def hello():
 47 |         return {"Hello": "World!"}
 48 | 
 49 | Template Rendering
 50 | ------------------
 51 | 
 52 | .. code-block:: python
 53 | 
 54 |     from quart import render_template
 55 | 
 56 |     @app.route("/hello")
 57 |     async def hello():
 58 |         return await render_template("index.html")  # Required to be in templates/
 59 | 
 60 | Configuration
 61 | -------------
 62 | 
 63 | .. code-block:: python
 64 | 
 65 |     import json
 66 |     import tomllib
 67 | 
 68 |     app.config["VALUE"] = "something"
 69 | 
 70 |     app.config.from_file("filename.toml", tomllib.load)
 71 |     app.config.from_file("filename.json", json.load)
 72 | 
 73 | Request
 74 | -------
 75 | 
 76 | .. code-block:: python
 77 | 
 78 |     from quart import request
 79 | 
 80 |     @app.route("/hello")
 81 |     async def hello():
 82 |         request.method
 83 |         request.url
 84 |         request.headers["X-Bob"]
 85 |         request.args.get("a")  # Query string e.g. example.com/hello?a=2
 86 |         await request.get_data()  # Full raw body
 87 |         (await request.form)["name"]
 88 |         (await request.get_json())["key"]
 89 |         request.cookies.get("name")
 90 | 
 91 | WebSocket
 92 | ---------
 93 | 
 94 | .. code-block:: python
 95 | 
 96 |     from quart import websocket
 97 | 
 98 |     @app.websocket("/ws")
 99 |     async def ws():
100 |         websocket.headers
101 |         while True:
102 |             try:
103 |                 data = await websocket.receive()
104 |                 await websocket.send(f"Echo {data}")
105 |             except asyncio.CancelledError:
106 |                 # Handle disconnect
107 |                 raise
108 | 
109 | Cookies
110 | -------
111 | 
112 | .. code-block:: python
113 | 
114 |     from quart import make_response
115 | 
116 |     @app.route("/hello")
117 |     async def hello():
118 |         response = await make_response("Hello")
119 |         response.set_cookie("name", "value")
120 |         return response
121 | 
122 | Abort
123 | -----
124 | 
125 | .. code-block:: python
126 | 
127 |     from quart import abort
128 | 
129 |     @app.route("/hello")
130 |     async def hello():
131 |         abort(409)
132 | 
133 | 
134 | HTTP/2 & HTTP/3 Server Push
135 | ---------------------------
136 | 
137 | .. code-block:: python
138 | 
139 |     from quart import make_push_promise, url_for
140 | 
141 |     @app.route("/hello")
142 |     async def hello():
143 |         await make_push_promise(url_for('static', filename='css/minimal.css'))
144 |         ...
145 | 


--------------------------------------------------------------------------------
/docs/reference/index.rst:
--------------------------------------------------------------------------------
 1 | =========
 2 | Reference
 3 | =========
 4 | 
 5 | .. toctree::
 6 |    :maxdepth: 1
 7 | 
 8 |    api.rst
 9 |    cheatsheet.rst
10 |    logo.rst
11 |    response_values.rst
12 |    versioning.rst
13 | 


--------------------------------------------------------------------------------
/docs/reference/logo.rst:
--------------------------------------------------------------------------------
 1 | Logo
 2 | ====
 3 | 
 4 | The Quart logo has been kindly provided by Vic Shóstak, @koddr,
 5 | `Quart-Logo <https://github.com/koddr/quart-logo>`_.
 6 | 
 7 | The logo itself has a lot of meaning, as expressed by @koddr,
 8 | 
 9 |     This is looks like Flask fonts and give us to understand what's
10 |     the quart means?, because 1 quart equal 0.9 litre (and we're
11 |     assuming: full bottle on logo hold one litre, but it's not full —
12 |     because hold a quart now).
13 | 
14 |     The bung on top of bottle makes it clear — this is production
15 |     ready Python framework with speed more than Flask, but Flask-like
16 |     — all in one pack!. Get, open (equal install) and use it!
17 | 
18 |     Unstable liquid's state in bottle give us to understand — this
19 |     liquid is reactivity, like new async features on Python and
20 |     HTTP/2. And some aggressive gradient's color of liquid say to us
21 |     hey, I'm alive and can help you to improve yourself... just use
22 |     me!.
23 | 
24 | 
25 | .. image:: ../_static/logo.png
26 |    :alt: Quart logo
27 | 


--------------------------------------------------------------------------------
/docs/reference/response_values.rst:
--------------------------------------------------------------------------------
  1 | .. _response_values:
  2 | 
  3 | Response Return Values
  4 | ======================
  5 | 
  6 | Response functions can return a number of different types as the
  7 | response value which will trigger different responses to the
  8 | client. The possible direct returns are,
  9 | 
 10 | Response Values
 11 | ---------------
 12 | 
 13 | str
 14 | ^^^
 15 | 
 16 | .. code-block:: python
 17 | 
 18 |     return "Hello"
 19 |     return await render_template("index.html")
 20 | 
 21 | A solitary string return indicates that you intend to return a string
 22 | mimetype ``text/html``. The string will be encoded using the default
 23 | :attr:`~quart.wrappers._BaseRequestResponse.charset`.
 24 | 
 25 | dict
 26 | ^^^^
 27 | 
 28 | .. code-block:: python
 29 | 
 30 |     return {"a": "b"}
 31 | 
 32 | A solitary dict return indicates that you intend to return json,
 33 | ``application/json``. The jsonify function will be used to encode the
 34 | dictionary.
 35 | 
 36 | list
 37 | ^^^^
 38 | 
 39 | .. code-block:: python
 40 | 
 41 |     return ["a", "b"]
 42 | 
 43 | A solitary list return indicates that you intend to return json,
 44 | ``application/json``. The jsonify function will be used to encode the
 45 | list.
 46 | 
 47 | Response
 48 | ^^^^^^^^
 49 | 
 50 | .. code-block:: python
 51 | 
 52 |     @app.route('/')
 53 |     async def route_func():
 54 |         return Response("Hello")
 55 | 
 56 | Returning a Response instance indicates that you know exactly what you
 57 | wish to return.
 58 | 
 59 | AsyncGenerator[bytes, None]
 60 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 61 | 
 62 | .. code-block:: python
 63 | 
 64 |     @app.route('/')
 65 |     async def route_func():
 66 | 
 67 |         async def agen():
 68 |             data = await something
 69 |             yield data
 70 | 
 71 |         return agen()
 72 | 
 73 | Returning an async generator allows for the response to be streamed to
 74 | the client, thereby lowing the peak memory usage, if combined with a
 75 | ``Transfer-Encoding`` header with value ``chunked``.
 76 | 
 77 | Generator[bytes, None, None]
 78 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 79 | 
 80 | .. code-block:: python
 81 | 
 82 |     @app.route('/')
 83 |     async def route_func():
 84 | 
 85 |         def gen():
 86 |             yield data
 87 | 
 88 |         return gen()
 89 | 
 90 | Returning an generator allows for the response to be streamed to the
 91 | client, thereby lowing the peak memory usage, if combined with a
 92 | ``Transfer-Encoding`` header with value ``chunked``.
 93 | 
 94 | Combinations
 95 | ------------
 96 | 
 97 | Any of the above Response Values can be combined, as described,
 98 | 
 99 | Tuple[ResponseValue, int]
100 | ^^^^^^^^^^^^^^^^^^^^^^^^^
101 | 
102 | .. code-block:: python
103 | 
104 |     @app.route('/')
105 |     async def route_func():
106 |         return "Hello", 200
107 | 
108 | A tuple of a Response Value and a integer indicates that you intend to
109 | specify the status code.
110 | 
111 | Tuple[str, int, Dict[str, str]]
112 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113 | 
114 | .. code-block:: python
115 | 
116 |     @app.route('/')
117 |     async def route_func():
118 |         return "Hello", 200, {'X-Header': 'Value'}
119 | 
120 | A tuple of a Response Value, integer and dictionary indicates that you intend
121 | to specify additional headers.
122 | 


--------------------------------------------------------------------------------
/docs/reference/versioning.rst:
--------------------------------------------------------------------------------
 1 | Versioning
 2 | ==========
 3 | 
 4 | Quart uses `Semantic Versioning <https://semver.org/>`_ with the
 5 | caveat that Quart has yet to reach it's first major release. This
 6 | means there may be changes between minor versions that would be
 7 | considered major version changes - these are avoided and indicated in
 8 | the changelog.
 9 | 
10 | Long term support
11 | -----------------
12 | 
13 | Only the latest release is supported and hence likely to receive
14 | updates. No previous releases are supported. Please always upgrade to
15 | the latest Quart release where possible.
16 | 
17 | Python versions
18 | ---------------
19 | 
20 | Quart will support Python versions until they reach end of life. This
21 | may, is rare circumstances be achieved by bug fixes to a specific
22 | release branch i.e. no new features. This happended with Python 3.6
23 | and the Quart 0.5.X releases.
24 | 


--------------------------------------------------------------------------------
/docs/tutorials/asyncio.rst:
--------------------------------------------------------------------------------
 1 | .. _asyncio:
 2 | 
 3 | Introduction to asyncio
 4 | =======================
 5 | 
 6 | Asyncio is the part of the Python standard library that provides an
 7 | event loop with IO (input/output) operations. It exists to allow
 8 | concurrent programming in Python, whereby the event loop switches to
 9 | another task whilst the previous task waits on IO. This concurrency
10 | allows for greater CPU utilisation and hence greater throughput
11 | performance.
12 | 
13 | The easiest way to understand this is to consider something concrete,
14 | namely a demonstrative simulation In the following we fetch a url with
15 | a simulated IO delay,
16 | 
17 | .. code-block:: python
18 | 
19 |     import asyncio
20 | 
21 | 
22 |     async def simulated_fetch(url, delay):
23 |         await asyncio.sleep(delay)
24 |         print(f"Fetched {url} after {delay}")
25 |         return f"<html>{url}"
26 | 
27 | 
28 |     def main():
29 |         loop = asyncio.get_event_loop()
30 |         results = loop.run_until_complete(asyncio.gather(
31 |             simulated_fetch('http://google.com', 2),
32 |             simulated_fetch('http://bbc.co.uk', 1),
33 |         ))
34 |         print(results)
35 | 
36 | you should see the following output,
37 | 
38 | >>> Fetched http://bbc.co.uk after 1
39 | >>> Fetched http://google.com after 2
40 | >>> ['<html>http://google.com', '<html>http://bbc.co.uk']
41 | 
42 | which indicates that despite calling the ``google.com`` fetch first,
43 | the ``bbc.co.uk`` actually completed first i.e. the code ran
44 | concurrently. Additionally the code runs in a little over 2 seconds
45 | rather than over 3 as expected with synchronous code.
46 | 
47 | Relevance to web servers
48 | ------------------------
49 | 
50 | Web servers by definition do IO, in that they receive and respond to
51 | requests from the network. This means that asyncio is a very good fit
52 | even if the code within the framework does no IO itself. Yet in
53 | practice IO is present, for example when loading a template from a
54 | file, or contacting a database or another server.
55 | 
56 | Common pitfalls
57 | ---------------
58 | 
59 | It is very easy to await the wrong thing, for example,
60 | 
61 | .. code-block:: python
62 | 
63 |     await awaitable.attribute
64 | 
65 | will not await the ``awaitable`` object as you might expect, but
66 | rather attempt to resolve the attribute and then await it. This is
67 | quite commonly seen as,
68 | 
69 | .. code-block:: python
70 | 
71 |     await request.form.get('key')
72 | 
73 | which fails with an error that the coroutine wrapper has no get
74 | attribute. To work around this simply use brackets to indicate what
75 | must be awaited first,
76 | 
77 | .. code-block:: python
78 | 
79 |     (await awaitable).attribute
80 |     (await request.form).get('key')
81 | 


--------------------------------------------------------------------------------
/docs/tutorials/deployment.rst:
--------------------------------------------------------------------------------
 1 | .. _deployment:
 2 | 
 3 | Deploying Quart
 4 | ===============
 5 | 
 6 | It is not recommended to run Quart directly (via
 7 | :meth:`~quart.app.Quart.run`) in production. Instead it is recommended
 8 | that Quart be run using `Hypercorn
 9 | <https://github.com/pgjones/hypercorn>`_ or an alternative ASGI
10 | server. This is because the :meth:`~quart.app.Quart.run` enables
11 | features that help development yet slow production
12 | performance. Hypercorn is installed with Quart and will be used to
13 | serve requests in development mode by default (e.g. with
14 | :meth:`~quart.app.Quart.run`).
15 | 
16 | To use Quart with an ASGI server simply point the server at the Quart
17 | application, for example,
18 | 
19 | .. code-block:: python
20 |    :caption: example.py
21 | 
22 |     from quart import Quart
23 | 
24 |     app = Quart(__name__)
25 | 
26 |     @app.route('/')
27 |     async def hello():
28 |         return 'Hello World'
29 | 
30 | you can run with Hypercorn using,
31 | 
32 | .. code-block:: bash
33 | 
34 |     hypercorn example:app
35 | 
36 | See the `Hypercorn docs <https://hypercorn.readthedocs.io/>`_.
37 | 
38 | Alternative ASGI Servers
39 | ------------------------
40 | 
41 | Alongside `Hypercorn <https://github.com/pgjones/hypercorn>`_, `Daphne
42 | <https://github.com/django/daphne>`_, and `Uvicorn
43 | <https://github.com/encode/uvicorn>`_ are available ASGI servers that
44 | work with Quart.
45 | 
46 | Serverless deployment
47 | ---------------------
48 | 
49 | To deploy Quart in an AWS Lambda & API Gateway setting you will need to use a specialised
50 | ASGI function adapter. `Mangum <https://github.com/erm/mangum>`_ is
51 | recommended for this and can be as simple as,
52 | 
53 | .. code-block:: python
54 | 
55 |     from mangum import Mangum
56 |     from quart import Quart
57 | 
58 |     app = Quart(__name__)
59 | 
60 |     @app.route("/")
61 |     async def index():
62 |         return "Hello, world!"
63 | 
64 |     handler = Mangum(app)  # optionally set debug=True
65 | 


--------------------------------------------------------------------------------
/docs/tutorials/index.rst:
--------------------------------------------------------------------------------
 1 | =========
 2 | Tutorials
 3 | =========
 4 | 
 5 | .. toctree::
 6 |    :maxdepth: 1
 7 | 
 8 |    installation.rst
 9 |    quickstart.rst
10 |    asyncio.rst
11 |    api_tutorial.rst
12 |    blog_tutorial.rst
13 |    chat_tutorial.rst
14 |    video_tutorial.rst
15 |    deployment.rst
16 | 


--------------------------------------------------------------------------------
/docs/tutorials/installation.rst:
--------------------------------------------------------------------------------
 1 | .. _installation:
 2 | 
 3 | Installation
 4 | ============
 5 | 
 6 | Quart is only compatible with Python 3.9 or higher and can be installed
 7 | using pip or your favorite python package manager:
 8 | 
 9 | .. code-block:: console
10 | 
11 |     pip install quart
12 | 
13 | Dependencies
14 | ------------
15 | 
16 | Quart dependends on the following packages, which will automatically
17 | be installed with Quart:
18 | 
19 | - aiofiles, to load files in an asyncio compatible manner,
20 | - blinker, to manage signals,
21 | - click, to manage command line arguments
22 | - hypercorn, an ASGI server for development,
23 | - importlib_metadata only for Python 3.9,
24 | - itsdangerous, for signing secure cookies,
25 | - jinja2, for template rendering,
26 | - markupsafe, for markup rendering,
27 | - typing_extensions only for Python 3.9,
28 | - werkzeug, as the basis of many Quart classes.
29 | 
30 | You can choose to install with the dotenv extra:
31 | 
32 | .. code-block:: console
33 | 
34 |     pip install quart[dotenv]
35 | 
36 | Which will install the ``python-dotenv`` package which enables support
37 | for automatically loading environment variables when running ``quart``
38 | commands.
39 | 
40 | See also
41 | --------
42 | 
43 | `Poetry <https://python-poetry.org>`_ for project management.
44 | 


--------------------------------------------------------------------------------
/docs/tutorials/quickstart.rst:
--------------------------------------------------------------------------------
 1 | .. _quickstart:
 2 | 
 3 | Quickstart
 4 | ==========
 5 | 
 6 | Hello World
 7 | -----------
 8 | 
 9 | A very simple app that simply returns a response containing ``hello``
10 | is, (file ``hello-world.py``)
11 | 
12 | .. code-block:: python
13 | 
14 |     from quart import Quart
15 | 
16 |     app = Quart(__name__)
17 | 
18 |     @app.route('/')
19 |     async def hello():
20 |         return 'hello'
21 | 
22 |     app.run()
23 | 
24 | and is simply run via
25 | 
26 | .. code-block:: console
27 | 
28 |     python hello-world.py
29 | 
30 | or alternatively
31 | 
32 | .. code-block:: console
33 | 
34 |     $ export QUART_APP=hello-world:app
35 |     $ quart run
36 | 
37 | and tested by
38 | 
39 | .. code-block:: sh
40 | 
41 |     curl localhost:5000
42 | 
43 | See also
44 | --------
45 | 
46 | :ref:`cheatsheet`
47 | 


--------------------------------------------------------------------------------
/examples/api/README.rst:
--------------------------------------------------------------------------------
 1 | Tutorial: Building a RESTful API
 2 | ================================
 3 | 
 4 | This is the example code for the api tutorial. It is managed using
 5 | `Poetry <https://python-poetry.org>`_ with the following commands,
 6 | 
 7 | .. code-block:: console
 8 | 
 9 |     poetry install
10 |     poetry run start
11 |     poetry run pytest -c pyproject.toml tests/
12 | 


--------------------------------------------------------------------------------
/examples/api/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [tool.poetry]
 2 | name = "api"
 3 | version = "0.1.0"
 4 | description = ""
 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"]
 6 | 
 7 | [tool.poetry.dependencies]
 8 | python = "^3.10"
 9 | quart = "*"
10 | quart-schema = "*"
11 | 
12 | [tool.poetry.dev-dependencies]
13 | pytest = "*"
14 | pytest-asyncio = "^0.18.3"
15 | 
16 | [tool.poetry.scripts]
17 | start = "api:run"
18 | 
19 | [tool.pytest.ini_options]
20 | asyncio_mode = "auto"
21 | 
22 | [build-system]
23 | requires = ["poetry-core>=1.0.0"]
24 | build-backend = "poetry.core.masonry.api"
25 | 


--------------------------------------------------------------------------------
/examples/api/src/api/__init__.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from dataclasses import dataclass
 4 | from datetime import datetime
 5 | 
 6 | from quart_schema import QuartSchema
 7 | from quart_schema import validate_request
 8 | from quart_schema import validate_response
 9 | 
10 | from quart import Quart
11 | from quart import request
12 | 
13 | app = Quart(__name__)
14 | QuartSchema(app)
15 | 
16 | 
17 | @app.post("/echo")
18 | async def echo():
19 |     print(request.is_json, request.mimetype)
20 |     data = await request.get_json()
21 |     return {"input": data, "extra": True}
22 | 
23 | 
24 | @dataclass
25 | class TodoIn:
26 |     task: str
27 |     due: datetime | None
28 | 
29 | 
30 | @dataclass
31 | class Todo(TodoIn):
32 |     id: int
33 | 
34 | 
35 | @app.post("/todos/")
36 | @validate_request(TodoIn)
37 | @validate_response(Todo)
38 | async def create_todo(data: Todo) -> Todo:
39 |     return Todo(id=1, task=data.task, due=data.due)
40 | 
41 | 
42 | def run() -> None:
43 |     app.run()
44 | 


--------------------------------------------------------------------------------
/examples/api/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/api/tests/__init__.py


--------------------------------------------------------------------------------
/examples/api/tests/test_api.py:
--------------------------------------------------------------------------------
 1 | from api import app
 2 | from api import TodoIn
 3 | 
 4 | 
 5 | async def test_echo() -> None:
 6 |     test_client = app.test_client()
 7 |     response = await test_client.post("/echo", json={"a": "b"})
 8 |     data = await response.get_json()
 9 |     assert data == {"extra": True, "input": {"a": "b"}}
10 | 
11 | 
12 | async def test_create_todo() -> None:
13 |     test_client = app.test_client()
14 |     response = await test_client.post("/todos/", json=TodoIn(task="Abc", due=None))
15 |     data = await response.get_json()
16 |     assert data == {"id": 1, "task": "Abc", "due": None}
17 | 


--------------------------------------------------------------------------------
/examples/blog/README.rst:
--------------------------------------------------------------------------------
 1 | Tutorial: Building a simple blog
 2 | ================================
 3 | 
 4 | This is the example code for the blog tutorial. It is managed using
 5 | `Poetry <https://python-poetry.org>`_ with the following commands,
 6 | 
 7 | .. code-block:: console
 8 | 
 9 |     poetry install
10 |     poetry run start
11 |     poetry run pytest -c pyproject.toml tests/
12 | 


--------------------------------------------------------------------------------
/examples/blog/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [tool.poetry]
 2 | name = "blog"
 3 | version = "0.1.0"
 4 | description = ""
 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"]
 6 | 
 7 | [tool.poetry.dependencies]
 8 | python = "^3.10"
 9 | quart = "*"
10 | 
11 | [tool.poetry.dev-dependencies]
12 | pytest = "*"
13 | pytest-asyncio = "*"
14 | 
15 | [tool.poetry.scripts]
16 | init_db = "blog:init_db"
17 | start = "blog:run"
18 | 
19 | [tool.pytest.ini_options]
20 | asyncio_mode = "auto"
21 | 
22 | [build-system]
23 | requires = ["poetry-core>=1.0.0"]
24 | build-backend = "poetry.core.masonry.api"
25 | 


--------------------------------------------------------------------------------
/examples/blog/src/blog/__init__.py:
--------------------------------------------------------------------------------
 1 | from sqlite3 import dbapi2 as sqlite3
 2 | 
 3 | from quart import g
 4 | from quart import Quart
 5 | from quart import redirect
 6 | from quart import render_template
 7 | from quart import request
 8 | from quart import url_for
 9 | 
10 | app = Quart(__name__)
11 | 
12 | app.config.update(
13 |     {
14 |         "DATABASE": app.root_path / "blog.db",
15 |     }
16 | )
17 | 
18 | 
19 | def _connect_db():
20 |     engine = sqlite3.connect(app.config["DATABASE"])
21 |     engine.row_factory = sqlite3.Row
22 |     return engine
23 | 
24 | 
25 | def _get_db():
26 |     if not hasattr(g, "sqlite_db"):
27 |         g.sqlite_db = _connect_db()
28 |     return g.sqlite_db
29 | 
30 | 
31 | @app.get("/")
32 | async def posts():
33 |     db = _get_db()
34 |     cur = db.execute(
35 |         """SELECT title, text
36 |              FROM post
37 |          ORDER BY id DESC""",
38 |     )
39 |     posts = cur.fetchall()
40 |     return await render_template("posts.html", posts=posts)
41 | 
42 | 
43 | @app.route("/create/", methods=["GET", "POST"])
44 | async def create():
45 |     if request.method == "POST":
46 |         db = _get_db()
47 |         form = await request.form
48 |         db.execute(
49 |             "INSERT INTO post (title, text) VALUES (?, ?)",
50 |             [form["title"], form["text"]],
51 |         )
52 |         db.commit()
53 |         return redirect(url_for("posts"))
54 |     else:
55 |         return await render_template("create.html")
56 | 
57 | 
58 | def init_db():
59 |     db = _connect_db()
60 |     with open(app.root_path / "schema.sql") as file_:
61 |         db.cursor().executescript(file_.read())
62 |     db.commit()
63 | 
64 | 
65 | def run() -> None:
66 |     app.run()
67 | 


--------------------------------------------------------------------------------
/examples/blog/src/blog/schema.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS post;
2 | CREATE TABLE post (
3 |        id INTEGER PRIMARY KEY AUTOINCREMENT,
4 |        title TEXT NOT NULL,
5 |        'text' TEXT NOT NULL
6 | );
7 | 


--------------------------------------------------------------------------------
/examples/blog/src/blog/templates/create.html:
--------------------------------------------------------------------------------
1 | <form method="POST" style="display: flex; flex-direction: column; gap: 8px">
2 |   <label>Title: <input type="text" size="30" name="title" /></label>
3 |   <label>Text: <textarea name="text" rows="5" cols="40"></textarea></label>
4 |   <button type="submit">Create</button>
5 | </form>
6 | 


--------------------------------------------------------------------------------
/examples/blog/src/blog/templates/posts.html:
--------------------------------------------------------------------------------
 1 | <main>
 2 |   {% for post in posts %}
 3 |     <article>
 4 |       <h2>{{ post.title }}</h2>
 5 |       <p>{{ post.text|safe }}</p>
 6 |     </article>
 7 |   {% else %}
 8 |     <p>No posts available</p>
 9 |   {% endfor %}
10 | </main>
11 | 


--------------------------------------------------------------------------------
/examples/blog/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/blog/tests/__init__.py


--------------------------------------------------------------------------------
/examples/blog/tests/conftest.py:
--------------------------------------------------------------------------------
 1 | import pytest
 2 | from blog import app
 3 | from blog import init_db
 4 | 
 5 | 
 6 | @pytest.fixture(autouse=True)
 7 | def configure_db(tmpdir):
 8 |     app.config["DATABASE"] = str(tmpdir.join("blog.db"))
 9 |     init_db()
10 | 


--------------------------------------------------------------------------------
/examples/blog/tests/test_blog.py:
--------------------------------------------------------------------------------
 1 | from blog import app
 2 | 
 3 | 
 4 | async def test_create_post():
 5 |     test_client = app.test_client()
 6 |     response = await test_client.post(
 7 |         "/create/", form={"title": "Post", "text": "Text"}
 8 |     )
 9 |     assert response.status_code == 302
10 |     response = await test_client.get("/")
11 |     text = await response.get_data()
12 |     assert b"<h2>Post</h2>" in text
13 |     assert b"<p>Text</p>" in text
14 | 


--------------------------------------------------------------------------------
/examples/chat/README.rst:
--------------------------------------------------------------------------------
 1 | Tutorial: Building a chat server
 2 | ================================
 3 | 
 4 | This is the example code for the chat server tutorial. It is managed
 5 | using `Poetry <https://python-poetry.org>`_ with the following
 6 | commands,
 7 | 
 8 | .. code-block:: console
 9 | 
10 |     poetry install
11 |     poetry run start
12 |     poetry run pytest -c pyproject.toml tests/
13 | 


--------------------------------------------------------------------------------
/examples/chat/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [tool.poetry]
 2 | name = "chat"
 3 | version = "0.1.0"
 4 | description = ""
 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"]
 6 | 
 7 | [tool.poetry.dependencies]
 8 | python = "^3.10"
 9 | quart = "*"
10 | 
11 | [tool.poetry.dev-dependencies]
12 | pytest = "*"
13 | pytest-asyncio = "*"
14 | 
15 | [tool.poetry.scripts]
16 | start = "chat:run"
17 | 
18 | [tool.pytest.ini_options]
19 | asyncio_mode = "auto"
20 | 
21 | [build-system]
22 | requires = ["poetry-core>=1.0.0"]
23 | build-backend = "poetry.core.masonry.api"
24 | 


--------------------------------------------------------------------------------
/examples/chat/src/chat/__init__.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | 
 3 | from chat.broker import Broker
 4 | from quart import Quart
 5 | from quart import render_template
 6 | from quart import websocket
 7 | 
 8 | app = Quart(__name__)
 9 | broker = Broker()
10 | 
11 | 
12 | @app.get("/")
13 | async def index():
14 |     return await render_template("index.html")
15 | 
16 | 
17 | async def _receive() -> None:
18 |     while True:
19 |         message = await websocket.receive()
20 |         await broker.publish(message)
21 | 
22 | 
23 | @app.websocket("/ws")
24 | async def ws() -> None:
25 |     try:
26 |         task = asyncio.ensure_future(_receive())
27 |         async for message in broker.subscribe():
28 |             await websocket.send(message)
29 |     finally:
30 |         task.cancel()
31 |         await task
32 | 
33 | 
34 | def run():
35 |     app.run(debug=True)
36 | 


--------------------------------------------------------------------------------
/examples/chat/src/chat/broker.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | from collections.abc import AsyncGenerator
 3 | 
 4 | 
 5 | class Broker:
 6 |     def __init__(self) -> None:
 7 |         self.connections = set()
 8 | 
 9 |     async def publish(self, message: str) -> None:
10 |         for connection in self.connections:
11 |             await connection.put(message)
12 | 
13 |     async def subscribe(self) -> AsyncGenerator[str, None]:
14 |         connection = asyncio.Queue()
15 |         self.connections.add(connection)
16 |         try:
17 |             while True:
18 |                 yield await connection.get()
19 |         finally:
20 |             self.connections.remove(connection)
21 | 


--------------------------------------------------------------------------------
/examples/chat/src/chat/templates/index.html:
--------------------------------------------------------------------------------
 1 | <script type="text/javascript">
 2 |   const ws = new WebSocket(`ws://${location.host}/ws`);
 3 | 
 4 |   ws.addEventListener('message', function (event) {
 5 |     const li = document.createElement("li");
 6 |     li.appendChild(document.createTextNode(event.data));
 7 |     document.getElementById("messages").appendChild(li);
 8 |   });
 9 | 
10 |   function send(event) {
11 |     const message = (new FormData(event.target)).get("message");
12 |     if (message) {
13 |       ws.send(message);
14 |     }
15 |     event.target.reset();
16 |     return false;
17 |   }
18 | </script>
19 | 
20 | <div style="display: flex; height: 100%; flex-direction: column">
21 |   <ul id="messages" style="flex-grow: 1; list-style-type: none"></ul>
22 | 
23 |   <form onsubmit="return send(event)">
24 |     <input type="text" name="message" minlength="1" />
25 |     <button type="submit">Send</button>
26 |   </form>
27 | </div>
28 | 


--------------------------------------------------------------------------------
/examples/chat/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/chat/tests/__init__.py


--------------------------------------------------------------------------------
/examples/chat/tests/test_chat.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | 
 3 | from chat import app
 4 | 
 5 | from quart.testing.connections import (
 6 |     TestWebsocketConnection as _TestWebsocketConnection,
 7 | )
 8 | 
 9 | 
10 | async def _receive(test_websocket: _TestWebsocketConnection) -> str:
11 |     return await test_websocket.receive()
12 | 
13 | 
14 | async def test_websocket() -> None:
15 |     test_client = app.test_client()
16 |     async with test_client.websocket("/ws") as test_websocket:
17 |         task = asyncio.ensure_future(_receive(test_websocket))
18 |         await test_websocket.send("message")
19 |         result = await task
20 |         assert result == "message"
21 | 


--------------------------------------------------------------------------------
/examples/video/README.rst:
--------------------------------------------------------------------------------
 1 | Tutorial: Serving video
 2 | =======================
 3 | 
 4 | This is the example code for the video server tutorial. It is managed
 5 | using `Poetry <https://python-poetry.org>`_ with the following
 6 | commands,
 7 | 
 8 | .. code-block:: console
 9 | 
10 |     poetry install
11 |     poetry run start
12 |     poetry run pytest -c pyproject.toml tests/
13 | 


--------------------------------------------------------------------------------
/examples/video/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [tool.poetry]
 2 | name = "video"
 3 | version = "0.1.0"
 4 | description = ""
 5 | authors = ["pgjones <philip.graham.jones@googlemail.com>"]
 6 | 
 7 | [tool.poetry.dependencies]
 8 | python = "^3.10"
 9 | 
10 | [tool.poetry.dev-dependencies]
11 | pytest = "*"
12 | pytest-asyncio = "*"
13 | 
14 | [tool.poetry.scripts]
15 | start = "video:run"
16 | 
17 | [tool.pytest.ini_options]
18 | asyncio_mode = "auto"
19 | 
20 | [build-system]
21 | requires = ["poetry-core>=1.0.0"]
22 | build-backend = "poetry.core.masonry.api"
23 | 


--------------------------------------------------------------------------------
/examples/video/src/video/__init__.py:
--------------------------------------------------------------------------------
 1 | from quart import Quart
 2 | from quart import render_template
 3 | from quart import send_file
 4 | 
 5 | app = Quart(__name__)
 6 | 
 7 | 
 8 | @app.get("/")
 9 | async def index():
10 |     return await render_template("index.html")
11 | 
12 | 
13 | @app.route("/video.mp4")
14 | async def auto_video():
15 |     return await send_file(app.static_folder / "video.mp4", conditional=True)
16 | 
17 | 
18 | def run() -> None:
19 |     app.run()
20 | 


--------------------------------------------------------------------------------
/examples/video/src/video/static/video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/video/src/video/static/video.mp4


--------------------------------------------------------------------------------
/examples/video/src/video/templates/index.html:
--------------------------------------------------------------------------------
1 | <video controls width="100%">
2 |   <source src="/video.mp4" type="video/mp4">
3 | </video>
4 | 


--------------------------------------------------------------------------------
/examples/video/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pallets/quart/8fb9bb58474a4b1ca73d5d244fc3c898d404e87d/examples/video/tests/__init__.py


--------------------------------------------------------------------------------
/examples/video/tests/test_video.py:
--------------------------------------------------------------------------------
 1 | from video import app
 2 | 
 3 | 
 4 | async def test_auto_video() -> None:
 5 |     test_client = app.test_client()
 6 |     response = await test_client.get("/video.mp4")
 7 |     data = await response.get_data()
 8 |     assert len(data) == 255_849
 9 | 
10 |     response = await test_client.get("/video.mp4", headers={"Range": "bytes=200-1000"})
11 |     data = await response.get_data()
12 |     assert len(data) == 801
13 | 


--------------------------------------------------------------------------------
/src/quart/__init__.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from markupsafe import escape as escape
 4 | from markupsafe import Markup as Markup
 5 | 
 6 | from .app import Quart as Quart
 7 | from .blueprints import Blueprint as Blueprint
 8 | from .config import Config as Config
 9 | from .ctx import after_this_request as after_this_request
10 | from .ctx import copy_current_app_context as copy_current_app_context
11 | from .ctx import copy_current_request_context as copy_current_request_context
12 | from .ctx import copy_current_websocket_context as copy_current_websocket_context
13 | from .ctx import has_app_context as has_app_context
14 | from .ctx import has_request_context as has_request_context
15 | from .ctx import has_websocket_context as has_websocket_context
16 | from .globals import current_app as current_app
17 | from .globals import g as g
18 | from .globals import request as request
19 | from .globals import session as session
20 | from .globals import websocket as websocket
21 | from .helpers import abort as abort
22 | from .helpers import flash as flash
23 | from .helpers import get_flashed_messages as get_flashed_messages
24 | from .helpers import get_template_attribute as get_template_attribute
25 | from .helpers import make_push_promise as make_push_promise
26 | from .helpers import make_response as make_response
27 | from .helpers import redirect as redirect
28 | from .helpers import send_file as send_file
29 | from .helpers import send_from_directory as send_from_directory
30 | from .helpers import stream_with_context as stream_with_context
31 | from .helpers import url_for as url_for
32 | from .json import jsonify as jsonify
33 | from .signals import appcontext_popped as appcontext_popped
34 | from .signals import appcontext_pushed as appcontext_pushed
35 | from .signals import appcontext_tearing_down as appcontext_tearing_down
36 | from .signals import before_render_template as before_render_template
37 | from .signals import got_request_exception as got_request_exception
38 | from .signals import got_websocket_exception as got_websocket_exception
39 | from .signals import message_flashed as message_flashed
40 | from .signals import request_finished as request_finished
41 | from .signals import request_started as request_started
42 | from .signals import request_tearing_down as request_tearing_down
43 | from .signals import signals_available as signals_available
44 | from .signals import template_rendered as template_rendered
45 | from .signals import websocket_finished as websocket_finished
46 | from .signals import websocket_started as websocket_started
47 | from .signals import websocket_tearing_down as websocket_tearing_down
48 | from .templating import render_template as render_template
49 | from .templating import render_template_string as render_template_string
50 | from .templating import stream_template as stream_template
51 | from .templating import stream_template_string as stream_template_string
52 | from .typing import ResponseReturnValue as ResponseReturnValue
53 | from .wrappers import Request as Request
54 | from .wrappers import Response as Response
55 | from .wrappers import Websocket as Websocket
56 | 


--------------------------------------------------------------------------------
/src/quart/__main__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | 
3 | if __name__ == "__main__":
4 |     from .cli import main
5 | 
6 |     main()
7 | 


--------------------------------------------------------------------------------
/src/quart/config.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import json
 4 | from typing import Any
 5 | from typing import Callable
 6 | 
 7 | from flask.config import Config as FlaskConfig  # noqa: F401
 8 | from flask.config import ConfigAttribute as ConfigAttribute  # noqa: F401
 9 | 
10 | 
11 | class Config(FlaskConfig):
12 |     def from_prefixed_env(
13 |         self, prefix: str = "QUART", *, loads: Callable[[str], Any] = json.loads
14 |     ) -> bool:
15 |         """Load any environment variables that start with the prefix.
16 | 
17 |         The prefix (default ``QUART_``) is dropped from the env key
18 |         for the config key. Values are passed through a loading
19 |         function to attempt to convert them to more specific types
20 |         than strings.
21 | 
22 |         Keys are loaded in :func:`sorted` order.
23 | 
24 |         The default loading function attempts to parse values as any
25 |         valid JSON type, including dicts and lists.  Specific items in
26 |         nested dicts can be set by separating the keys with double
27 |         underscores (``__``). If an intermediate key doesn't exist, it
28 |         will be initialized to an empty dict.
29 | 
30 |         Arguments:
31 |             prefix: Load env vars that start with this prefix,
32 |                 separated with an underscore (``_``).
33 |             loads: Pass each string value to this function and use the
34 |                 returned value as the config value. If any error is
35 |                 raised it is ignored and the value remains a
36 |                 string. The default is :func:`json.loads`.
37 |         """
38 |         return super().from_prefixed_env(prefix, loads=loads)
39 | 


--------------------------------------------------------------------------------
/src/quart/datastructures.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from os import PathLike
 4 | from pathlib import Path
 5 | from typing import IO
 6 | 
 7 | from aiofiles import open as async_open
 8 | from werkzeug.datastructures import FileStorage as WerkzeugFileStorage
 9 | from werkzeug.datastructures import Headers
10 | 
11 | 
12 | class FileStorage(WerkzeugFileStorage):
13 |     """A thin wrapper over incoming files."""
14 | 
15 |     def __init__(
16 |         self,
17 |         stream: IO[bytes] | None = None,
18 |         filename: str | None = None,
19 |         name: str | None = None,
20 |         content_type: str | None = None,
21 |         content_length: int | None = None,
22 |         headers: Headers | None = None,
23 |     ) -> None:
24 |         super().__init__(stream, filename, name, content_type, content_length, headers)
25 | 
26 |     async def save(self, destination: PathLike, buffer_size: int = 16384) -> None:  # type: ignore
27 |         """Save the file to the destination.
28 | 
29 |         Arguments:
30 |             destination: A filename (str) or file object to write to.
31 |             buffer_size: Buffer size to keep in memory.
32 |         """
33 |         async with async_open(destination, "wb") as file_:
34 |             data = self.stream.read(buffer_size)
35 |             while data != b"":
36 |                 await file_.write(data)
37 |                 data = self.stream.read(buffer_size)
38 | 
39 |     async def load(self, source: PathLike, buffer_size: int = 16384) -> None:
40 |         path = Path(source)
41 |         self.filename = path.name
42 |         async with async_open(path, "rb") as file_:
43 |             data = await file_.read(buffer_size)
44 |             while data != b"":
45 |                 self.stream.write(data)
46 |                 data = await file_.read(buffer_size)
47 | 


--------------------------------------------------------------------------------
/src/quart/debug.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import inspect
  4 | 
  5 | from jinja2 import Template
  6 | 
  7 | from .wrappers import Response
  8 | 
  9 | TEMPLATE = """
 10 | <style>
 11 | pre {
 12 |   margin: 0;
 13 | }
 14 | 
 15 | .traceback, .locals {
 16 |   display: table;
 17 |   width: 100%;
 18 |   margin: 5px;
 19 | }
 20 | 
 21 | .traceback>div, .locals>div {
 22 |   display: table-row;
 23 | }
 24 | 
 25 | .traceback>div>div, .locals>div>div {
 26 |   display: table-cell;
 27 | }
 28 | 
 29 | .locals>div>div {
 30 |   border-top: 1px solid lightgrey;
 31 | }
 32 | 
 33 | .header {
 34 |   background-color: #ececec;
 35 |   margin-bottom: 5px;
 36 | }
 37 | 
 38 | .highlight {
 39 |   background-color: #ececec;
 40 | }
 41 | 
 42 | .info {
 43 |   font-weight: bold;
 44 | }
 45 | 
 46 | li {
 47 |   border: 1px solid lightgrey;
 48 |   border-radius: 5px;
 49 |   padding: 5px;
 50 |   list-style-type: none;
 51 |   margin-bottom: 5px;
 52 | }
 53 | 
 54 | h1>span {
 55 |   font-weight: lighter;
 56 | }
 57 | </style>
 58 | 
 59 | <h1>{{ name }} <span>{{ value }}</span></h1>
 60 | <ul>
 61 |   {% for frame in frames %}
 62 |     <li>
 63 |       <div class="header">
 64 |         File <span class="info">{{ frame.file }}</span>,
 65 |         line <span class="info">{{ frame.line }}</span>, in
 66 |       </div>
 67 |       <div class="traceback">
 68 |         {% for line in frame.code[0] %}
 69 |           <div {% if frame.line == loop.index + frame.code[1] %}class="highlight"\
 70 | {% endif %}>
 71 |             <div>{{ loop.index + frame.code[1] }}</div>
 72 |             <div><pre>{{ line }}</pre></div>
 73 |           </div>
 74 |         {% endfor %}
 75 |       </div>
 76 |       <div class="locals">
 77 |         {% for name, repr in frame.locals.items() %}
 78 |           <div>
 79 |             <div>{{ name }}</div>
 80 |             <div>{{ repr }}</div>
 81 |           </div>
 82 |         {% endfor %}
 83 |       </div>
 84 |     </li>
 85 |   {% endfor %}
 86 | </ul>
 87 | """
 88 | 
 89 | 
 90 | async def traceback_response(error: Exception) -> Response:
 91 |     type_ = type(error)
 92 |     tb = error.__traceback__
 93 |     frames = []
 94 |     while tb:
 95 |         frame = tb.tb_frame
 96 |         try:
 97 |             code = inspect.getsourcelines(frame)
 98 |         except OSError:
 99 |             code = None
100 | 
101 |         frames.append(
102 |             {
103 |                 "file": inspect.getfile(frame),
104 |                 "line": frame.f_lineno,
105 |                 "locals": frame.f_locals,
106 |                 "code": code,
107 |             }
108 |         )
109 |         tb = tb.tb_next
110 | 
111 |     name = type_.__name__
112 |     template = Template(TEMPLATE)
113 |     html = template.render(frames=reversed(frames), name=name, value=error)
114 |     return Response(html, 500)
115 | 


--------------------------------------------------------------------------------
/src/quart/globals.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from contextvars import ContextVar
 4 | from typing import TYPE_CHECKING
 5 | 
 6 | from werkzeug.local import LocalProxy
 7 | 
 8 | if TYPE_CHECKING:
 9 |     from .app import Quart
10 |     from .ctx import _AppCtxGlobals
11 |     from .ctx import AppContext
12 |     from .ctx import RequestContext
13 |     from .ctx import WebsocketContext
14 |     from .sessions import SessionMixin
15 |     from .wrappers import Request
16 |     from .wrappers import Websocket
17 | 
18 | _no_app_msg = "Not within an app context"
19 | _cv_app: ContextVar[AppContext] = ContextVar("quart.app_ctx")
20 | app_ctx: _AppCtxGlobals = LocalProxy(  # type: ignore[assignment]
21 |     _cv_app, unbound_message=_no_app_msg
22 | )
23 | current_app: Quart = LocalProxy(  # type: ignore[assignment]
24 |     _cv_app, "app", unbound_message=_no_app_msg
25 | )
26 | g: _AppCtxGlobals = LocalProxy(  # type: ignore[assignment]
27 |     _cv_app, "g", unbound_message=_no_app_msg
28 | )
29 | 
30 | _no_req_msg = "Not within a request context"
31 | _cv_request: ContextVar[RequestContext] = ContextVar("quart.request_ctx")
32 | request_ctx: RequestContext = LocalProxy(  # type: ignore[assignment]
33 |     _cv_request, unbound_message=_no_req_msg
34 | )
35 | request: Request = LocalProxy(  # type: ignore[assignment]
36 |     _cv_request, "request", unbound_message=_no_req_msg
37 | )
38 | 
39 | _no_websocket_msg = "Not within a websocket context"
40 | _cv_websocket: ContextVar[WebsocketContext] = ContextVar("quart.websocket_ctx")
41 | websocket_ctx: WebsocketContext = LocalProxy(  # type: ignore[assignment]
42 |     _cv_websocket, unbound_message=_no_websocket_msg
43 | )
44 | websocket: Websocket = LocalProxy(  # type: ignore[assignment]
45 |     _cv_websocket, "websocket", unbound_message=_no_websocket_msg
46 | )
47 | 
48 | 
49 | def _session_lookup() -> RequestContext | WebsocketContext:
50 |     try:
51 |         return _cv_request.get()
52 |     except LookupError:
53 |         try:
54 |             return _cv_websocket.get()
55 |         except LookupError:
56 |             raise RuntimeError("Not within a request nor websocket context") from None
57 | 
58 | 
59 | session: SessionMixin = LocalProxy(_session_lookup, "session")  # type: ignore[assignment]
60 | 


--------------------------------------------------------------------------------
/src/quart/json/__init__.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import json
 4 | from typing import Any
 5 | from typing import IO
 6 | from typing import TYPE_CHECKING
 7 | 
 8 | from flask.json.provider import _default
 9 | 
10 | from ..globals import current_app
11 | 
12 | if TYPE_CHECKING:
13 |     from ..wrappers import Response  # noqa: F401
14 | 
15 | 
16 | def dumps(object_: Any, **kwargs: Any) -> str:
17 |     if current_app:
18 |         return current_app.json.dumps(object_, **kwargs)
19 |     else:
20 |         kwargs.setdefault("default", _default)
21 |         return json.dumps(object_, **kwargs)
22 | 
23 | 
24 | def dump(object_: Any, fp: IO[str], **kwargs: Any) -> None:
25 |     if current_app:
26 |         current_app.json.dump(object_, fp, **kwargs)
27 |     else:
28 |         kwargs.setdefault("default", _default)
29 |         json.dump(object_, fp, **kwargs)
30 | 
31 | 
32 | def loads(object_: str | bytes, **kwargs: Any) -> Any:
33 |     if current_app:
34 |         return current_app.json.loads(object_, **kwargs)
35 |     else:
36 |         return json.loads(object_, **kwargs)
37 | 
38 | 
39 | def load(fp: IO[str], **kwargs: Any) -> Any:
40 |     if current_app:
41 |         return current_app.json.load(fp, **kwargs)
42 |     else:
43 |         return json.load(fp, **kwargs)
44 | 
45 | 
46 | def jsonify(*args: Any, **kwargs: Any) -> Response:
47 |     return current_app.json.response(*args, **kwargs)  # type: ignore
48 | 


--------------------------------------------------------------------------------
/src/quart/json/provider.py:
--------------------------------------------------------------------------------
1 | from flask.json.provider import DefaultJSONProvider as DefaultJSONProvider  # noqa: F401
2 | from flask.json.provider import JSONProvider as JSONProvider  # noqa: F401
3 | 


--------------------------------------------------------------------------------
/src/quart/json/tag.py:
--------------------------------------------------------------------------------
 1 | from flask.json.tag import JSONTag as JSONTag  # noqa: F401
 2 | from flask.json.tag import PassDict as PassDict  # noqa: F401
 3 | from flask.json.tag import PassList as PassList  # noqa: F401
 4 | from flask.json.tag import TagBytes as TagBytes  # noqa: F401
 5 | from flask.json.tag import TagDateTime as TagDateTime  # noqa: F401
 6 | from flask.json.tag import TagDict as TagDict  # noqa: F401
 7 | from flask.json.tag import TaggedJSONSerializer as TaggedJSONSerializer  # noqa: F401
 8 | from flask.json.tag import TagMarkup as TagMarkup  # noqa: F401
 9 | from flask.json.tag import TagTuple as TagTuple  # noqa: F401
10 | from flask.json.tag import TagUUID as TagUUID  # noqa: F401
11 | 


--------------------------------------------------------------------------------
/src/quart/logging.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import sys
 4 | from logging import DEBUG
 5 | from logging import Formatter
 6 | from logging import getLogger
 7 | from logging import Handler
 8 | from logging import Logger
 9 | from logging import LogRecord
10 | from logging import NOTSET
11 | from logging import StreamHandler
12 | from logging.handlers import QueueHandler
13 | from logging.handlers import QueueListener
14 | from queue import SimpleQueue as Queue
15 | from typing import TYPE_CHECKING
16 | 
17 | if TYPE_CHECKING:
18 |     from .app import Quart  # noqa
19 | 
20 | default_handler = StreamHandler(sys.stderr)
21 | default_handler.setFormatter(
22 |     Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
23 | )
24 | 
25 | 
26 | class LocalQueueHandler(QueueHandler):
27 |     """Custom QueueHandler that skips record preparation.
28 | 
29 |     There is no need to prepare records that go into a local, in-process queue,
30 |     we can skip that process and minimise the cost of logging further.
31 |     """
32 | 
33 |     def prepare(self, record: LogRecord) -> LogRecord:
34 |         return record
35 | 
36 | 
37 | def _setup_logging_queue(*handlers: Handler) -> QueueHandler:
38 |     """Create a new LocalQueueHandler and start an associated QueueListener."""
39 |     queue: Queue = Queue()
40 |     queue_handler = LocalQueueHandler(queue)
41 | 
42 |     serving_listener = QueueListener(queue, *handlers, respect_handler_level=True)
43 |     serving_listener.start()
44 | 
45 |     return queue_handler
46 | 
47 | 
48 | def has_level_handler(logger: Logger) -> bool:
49 |     """Check if the logger already has a handler"""
50 |     level = logger.getEffectiveLevel()
51 |     current = logger
52 | 
53 |     while current:
54 |         if any(handler.level <= level for handler in current.handlers):
55 |             return True
56 | 
57 |         if not current.propagate:
58 |             break
59 | 
60 |         current = current.parent
61 | 
62 |     return False
63 | 
64 | 
65 | def create_logger(app: Quart) -> Logger:
66 |     """Create a logger for the app based on the app settings.
67 | 
68 |     This creates a logger named quart.app that has a log level based
69 |     on the app configuration.
70 |     """
71 |     logger = getLogger(app.name)
72 | 
73 |     if app.debug and logger.level == NOTSET:
74 |         logger.setLevel(DEBUG)
75 | 
76 |     if not has_level_handler(logger):
77 |         queue_handler = _setup_logging_queue(default_handler)
78 |         logger.addHandler(queue_handler)
79 | 
80 |     return logger
81 | 


--------------------------------------------------------------------------------
/src/quart/py.typed:
--------------------------------------------------------------------------------
1 | Marker
2 | 


--------------------------------------------------------------------------------
/src/quart/routing.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import warnings
 4 | from collections.abc import Iterable
 5 | 
 6 | from werkzeug.routing import Map
 7 | from werkzeug.routing import MapAdapter
 8 | from werkzeug.routing import Rule
 9 | 
10 | from .wrappers.base import BaseRequestWebsocket
11 | 
12 | 
13 | class QuartRule(Rule):
14 |     def __init__(
15 |         self,
16 |         string: str,
17 |         defaults: dict | None = None,
18 |         subdomain: str | None = None,
19 |         methods: Iterable[str] | None = None,
20 |         endpoint: str | None = None,
21 |         strict_slashes: bool | None = None,
22 |         merge_slashes: bool | None = None,
23 |         host: str | None = None,
24 |         websocket: bool = False,
25 |         provide_automatic_options: bool = False,
26 |     ) -> None:
27 |         super().__init__(
28 |             string,
29 |             defaults=defaults,
30 |             subdomain=subdomain,
31 |             methods=methods,
32 |             endpoint=endpoint,
33 |             strict_slashes=strict_slashes,
34 |             merge_slashes=merge_slashes,
35 |             host=host,
36 |             websocket=websocket,
37 |         )
38 |         self.provide_automatic_options = provide_automatic_options
39 | 
40 | 
41 | class QuartMap(Map):
42 |     def bind_to_request(
43 |         self,
44 |         request: BaseRequestWebsocket,
45 |         subdomain: str | None,
46 |         server_name: str | None,
47 |     ) -> MapAdapter:
48 |         host: str
49 |         if server_name is None:
50 |             host = request.host.lower()
51 |         else:
52 |             host = server_name.lower()
53 | 
54 |         host = _normalise_host(request.scheme, host)
55 | 
56 |         if subdomain is None and not self.host_matching:
57 |             request_host_parts = _normalise_host(
58 |                 request.scheme, request.host.lower()
59 |             ).split(".")
60 |             config_host_parts = host.split(".")
61 |             offset = -len(config_host_parts)
62 | 
63 |             if request_host_parts[offset:] != config_host_parts:
64 |                 warnings.warn(
65 |                     f"Current server name '{request.host}' doesn't match configured"
66 |                     f" server name '{host}'",
67 |                     stacklevel=2,
68 |                 )
69 |                 subdomain = "<invalid>"
70 |             else:
71 |                 subdomain = ".".join(filter(None, request_host_parts[:offset]))
72 | 
73 |         return super().bind(
74 |             host,
75 |             request.root_path,
76 |             subdomain,
77 |             request.scheme,
78 |             request.method,
79 |             request.path,
80 |             request.query_string.decode(),
81 |         )
82 | 
83 | 
84 | def _normalise_host(scheme: str, host: str) -> str:
85 |     # It is not common to write port 80 or 443 for a hostname,
86 |     # so strip it if present.
87 |     if scheme in {"http", "ws"} and host.endswith(":80"):
88 |         return host[:-3]
89 |     elif scheme in {"https", "wss"} and host.endswith(":443"):
90 |         return host[:-4]
91 |     else:
92 |         return host
93 | 


--------------------------------------------------------------------------------
/src/quart/signals.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from blinker import Namespace
 4 | 
 5 | signals_available = True
 6 | 
 7 | _signals = Namespace()
 8 | 
 9 | #: Called before a template is rendered, connection functions
10 | # should have a signature of Callable[[Quart, Template, dict], None]
11 | before_render_template = _signals.signal("before-render-template")
12 | 
13 | #: Called when a template has been rendered, connected functions
14 | # should have a signature of Callable[[Quart, Template, dict], None]
15 | template_rendered = _signals.signal("template-rendered")
16 | 
17 | #: Called just after the request context has been created, connected
18 | # functions should have a signature of Callable[[Quart], None]
19 | request_started = _signals.signal("request-started")
20 | 
21 | #: Called after a response is fully finalised, connected functions
22 | # should have a signature of Callable[[Quart, Response], None]
23 | request_finished = _signals.signal("request-finished")
24 | 
25 | #: Called as the request context is teared down, connected functions
26 | # should have a signature of Callable[[Quart, Exception], None]
27 | request_tearing_down = _signals.signal("request-tearing-down")
28 | 
29 | #: Called if there is an exception in a background task, connected
30 | # functions should have a signature of Callable[[Quart, Exception], None]
31 | got_background_exception = _signals.signal("got-background-exception")
32 | 
33 | #: Called if there is an exception in a before or after serving
34 | # function, connected functions should have a signature of
35 | # Callable[[Quart, Exception], None]
36 | got_serving_exception = _signals.signal("got-serving-exception")
37 | 
38 | #: Called if there is an exception handling the request, connected
39 | # functions should have a signature of Callable[[Quart, Exception], None]
40 | got_request_exception = _signals.signal("got-request-exception")
41 | 
42 | #: Called just after the websocket context has been created, connected
43 | # functions should have a signature of Callable[[Quart], None]
44 | websocket_started = _signals.signal("websocket-started")
45 | 
46 | #: Called on receipt of a message over the websocket, connected
47 | # functions should have a signature of Callable[[AnyStr], None]
48 | websocket_received = _signals.signal("websocket-received")
49 | 
50 | #: Called when a message has been sent over the websocket, connected
51 | # functions should have a signature of Callable[[AnyStr], None]
52 | websocket_sent = _signals.signal("websocket-sent")
53 | 
54 | #: Called after a response is fully finalised, connected functions
55 | # should have a signature of Callable[[Quart, Optional[Response]], None]
56 | websocket_finished = _signals.signal("websocket-finished")
57 | 
58 | #: Called as the websocket context is teared down, connected functions
59 | # should have a signature of Callable[[Quart, Exception], None]
60 | websocket_tearing_down = _signals.signal("websocket-tearing-down")
61 | 
62 | #: Called if there is an exception handling the websocket, connected
63 | # functions should have a signature of Callable[[Quart, Exception], None]
64 | got_websocket_exception = _signals.signal("got-websocket-exception")
65 | 
66 | #: Called as the application context is teared down, connected functions
67 | # should have a signature of Callable[[Quart, Exception], None]
68 | appcontext_tearing_down = _signals.signal("appcontext-tearing-down")
69 | 
70 | #: Called when the app context is pushed, connected functions should
71 | # have a signature of Callable[[Quart], None]
72 | appcontext_pushed = _signals.signal("appcontext-pushed")
73 | 
74 | #: Called when the app context is popped, connected functions should
75 | # have a signature of Callable[[Quart], None]
76 | appcontext_popped = _signals.signal("appcontext-popped")
77 | 
78 | #: Called on a flash invocation, connection functions
79 | # should have a signature of Callable[[Quart, str, str], None]
80 | message_flashed = _signals.signal("message-flashed")
81 | 


--------------------------------------------------------------------------------
/src/quart/testing/__init__.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Any
 4 | from typing import TYPE_CHECKING
 5 | 
 6 | from click.testing import CliRunner
 7 | 
 8 | from ..cli import ScriptInfo
 9 | from .app import TestApp
10 | from .client import QuartClient
11 | from .connections import WebsocketResponseError
12 | from .utils import make_test_body_with_headers
13 | from .utils import make_test_headers_path_and_query_string
14 | from .utils import make_test_scope
15 | from .utils import no_op_push
16 | from .utils import sentinel
17 | 
18 | if TYPE_CHECKING:
19 |     from ..app import Quart
20 | 
21 | 
22 | class QuartCliRunner(CliRunner):
23 |     def __init__(self, app: Quart, **kwargs: Any) -> None:
24 |         self.app = app
25 |         super().__init__(**kwargs)
26 | 
27 |     def invoke(self, cli: Any = None, args: Any = None, **kwargs: Any) -> Any:  # type: ignore
28 |         if cli is None:
29 |             cli = self.app.cli
30 | 
31 |         if "obj" not in kwargs:
32 |             kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
33 | 
34 |         return super().invoke(cli, args, **kwargs)
35 | 
36 | 
37 | __all__ = (
38 |     "make_test_body_with_headers",
39 |     "make_test_headers_path_and_query_string",
40 |     "make_test_scope",
41 |     "no_op_push",
42 |     "QuartClient",
43 |     "QuartCliRunner",
44 |     "sentinel",
45 |     "TestApp",
46 |     "WebsocketResponseError",
47 | )
48 | 


--------------------------------------------------------------------------------
/src/quart/testing/app.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import asyncio
 4 | from collections.abc import Awaitable
 5 | from types import TracebackType
 6 | from typing import TYPE_CHECKING
 7 | 
 8 | from hypercorn.typing import ASGIReceiveEvent
 9 | from hypercorn.typing import ASGISendEvent
10 | from hypercorn.typing import LifespanScope
11 | 
12 | from ..typing import TestClientProtocol
13 | 
14 | if TYPE_CHECKING:
15 |     from ..app import Quart  # noqa
16 | 
17 | DEFAULT_TIMEOUT = 6
18 | 
19 | 
20 | class LifespanError(Exception):
21 |     pass
22 | 
23 | 
24 | class TestApp:
25 |     def __init__(
26 |         self,
27 |         app: Quart,
28 |         startup_timeout: int = DEFAULT_TIMEOUT,
29 |         shutdown_timeout: int = DEFAULT_TIMEOUT,
30 |     ) -> None:
31 |         self.app = app
32 |         self.startup_timeout = startup_timeout
33 |         self.shutdown_timeout = shutdown_timeout
34 |         self._startup = asyncio.Event()
35 |         self._shutdown = asyncio.Event()
36 |         self._app_queue: asyncio.Queue = asyncio.Queue()
37 |         self._task: Awaitable[None] = None
38 | 
39 |     def test_client(self) -> TestClientProtocol:
40 |         return self.app.test_client()
41 | 
42 |     async def startup(self) -> None:
43 |         scope: LifespanScope = {
44 |             "type": "lifespan",
45 |             "asgi": {"spec_version": "2.0"},
46 |             "state": {},
47 |         }
48 |         self._task = asyncio.ensure_future(
49 |             self.app(scope, self._asgi_receive, self._asgi_send)
50 |         )
51 |         await self._app_queue.put({"type": "lifespan.startup"})
52 |         await asyncio.wait_for(self._startup.wait(), timeout=self.startup_timeout)
53 |         if self._task.done():
54 |             # This will re-raise any exceptions in the task
55 |             await self._task
56 | 
57 |     async def shutdown(self) -> None:
58 |         await self._app_queue.put({"type": "lifespan.shutdown"})
59 |         await asyncio.wait_for(self._shutdown.wait(), timeout=self.shutdown_timeout)
60 |         await self._task
61 | 
62 |     async def __aenter__(self) -> TestApp:
63 |         await self.startup()
64 |         return self
65 | 
66 |     async def __aexit__(
67 |         self, exc_type: type, exc_value: BaseException, tb: TracebackType
68 |     ) -> None:
69 |         await self.shutdown()
70 | 
71 |     async def _asgi_receive(self) -> ASGIReceiveEvent:
72 |         return await self._app_queue.get()
73 | 
74 |     async def _asgi_send(self, message: ASGISendEvent) -> None:
75 |         if message["type"] == "lifespan.startup.complete":
76 |             self._startup.set()
77 |         elif message["type"] == "lifespan.shutdown.complete":
78 |             self._shutdown.set()
79 |         elif message["type"] == "lifespan.startup.failed":
80 |             self._startup.set()
81 |             raise LifespanError(f"Error during startup {message['message']}")
82 |         elif message["type"] == "lifespan.shutdown.failed":
83 |             self._shutdown.set()
84 |             raise LifespanError(f"Error during shutdown {message['message']}")
85 | 


--------------------------------------------------------------------------------
/src/quart/views.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | from collections.abc import Collection
  4 | from typing import Any
  5 | from typing import Callable
  6 | from typing import ClassVar
  7 | 
  8 | from .globals import current_app
  9 | from .globals import request
 10 | from .typing import ResponseReturnValue
 11 | from .typing import RouteCallable
 12 | 
 13 | http_method_funcs = frozenset(
 14 |     ["get", "post", "head", "options", "delete", "put", "trace", "patch"]
 15 | )
 16 | 
 17 | 
 18 | class View:
 19 |     """Use to define routes within a class structure.
 20 | 
 21 |     A View subclass must implement the :meth:`dispatch_request` in
 22 |     order to respond to requests. For automatic method finding based on
 23 |     the request HTTP Verb see :class:`MethodView`.
 24 | 
 25 |     An example usage is,
 26 | 
 27 |     .. code-block:: python
 28 | 
 29 |           class SimpleView:
 30 |               methods = ['GET']
 31 | 
 32 |               async def dispatch_request(id):
 33 |                   return f"ID is {id}"
 34 | 
 35 |           app.add_url_rule('/<id>', view_func=SimpleView.as_view('simple'))
 36 | 
 37 |     Note that class
 38 | 
 39 |     Attributes:
 40 |         decorators: A list of decorators to apply to a view
 41 |             method. The decorators are applied in the order of
 42 |             the list.
 43 |         methods: List of methods this view allows.
 44 |         provide_automatic_options: Override automatic OPTIONS
 45 |             if set, to either True or False.
 46 |         init_every_request: Create a new instance of this class
 47 |             for every request.
 48 |     """
 49 | 
 50 |     decorators: ClassVar[list[Callable]] = []
 51 |     methods: ClassVar[Collection[str] | None] = None
 52 |     provide_automatic_options: ClassVar[bool | None] = None
 53 |     init_every_request: ClassVar[bool] = True
 54 | 
 55 |     async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue:
 56 |         """Override and return a Response.
 57 | 
 58 |         This will be called with the request view_args, i.e. any url
 59 |         parameters.
 60 |         """
 61 |         raise NotImplementedError()
 62 | 
 63 |     @classmethod
 64 |     def as_view(cls, name: str, *class_args: Any, **class_kwargs: Any) -> RouteCallable:
 65 |         if cls.init_every_request:
 66 | 
 67 |             async def view(**kwargs: Any) -> ResponseReturnValue:
 68 |                 self = view.view_class(*class_args, **class_kwargs)  # type: ignore
 69 |                 return await current_app.ensure_async(self.dispatch_request)(**kwargs)
 70 | 
 71 |         else:
 72 |             self = cls(*class_args, **class_kwargs)
 73 | 
 74 |             async def view(**kwargs: Any) -> ResponseReturnValue:
 75 |                 return await current_app.ensure_async(self.dispatch_request)(**kwargs)
 76 | 
 77 |         if cls.decorators:
 78 |             view.__name__ = name
 79 |             view.__module__ = cls.__module__
 80 |             for decorator in cls.decorators:
 81 |                 view = decorator(view)
 82 | 
 83 |         view.view_class: type[View] = cls  # type: ignore
 84 |         view.__name__ = name
 85 |         view.__doc__ = cls.__doc__
 86 |         view.__module__ = cls.__module__
 87 |         view.methods = cls.methods  # type: ignore
 88 |         view.provide_automatic_options = cls.provide_automatic_options  # type: ignore
 89 |         return view
 90 | 
 91 | 
 92 | class MethodView(View):
 93 |     """A HTTP Method (verb) specific view class.
 94 | 
 95 |     This has an implementation of :meth:`dispatch_request` such that
 96 |     it calls a method based on the verb i.e. GET requests are handled
 97 |     by a `get` method. For example,
 98 | 
 99 |     .. code-block:: python
100 | 
101 |         class SimpleView(MethodView):
102 |             async def get(id):
103 |                 return f"Get {id}"
104 | 
105 |             async def post(id):
106 |                 return f"Post {id}"
107 | 
108 |           app.add_url_rule('/<id>', view_func=SimpleView.as_view('simple'))
109 |     """
110 | 
111 |     def __init_subclass__(cls, **kwargs: Any) -> None:
112 |         super().__init_subclass__(**kwargs)
113 | 
114 |         if "methods" not in cls.__dict__:
115 |             methods = set()
116 | 
117 |             for base in cls.__bases__:
118 |                 if getattr(base, "methods", None):
119 |                     methods.update(base.methods)  # type: ignore[attr-defined]
120 | 
121 |             for key in http_method_funcs:
122 |                 if hasattr(cls, key):
123 |                     methods.add(key.upper())
124 | 
125 |             if methods:
126 |                 cls.methods = methods
127 | 
128 |     async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue:
129 |         handler = getattr(self, request.method.lower(), None)
130 | 
131 |         if handler is None and request.method == "HEAD":
132 |             handler = getattr(self, "get", None)
133 | 
134 |         return await current_app.ensure_async(handler)(**kwargs)
135 | 


--------------------------------------------------------------------------------
/src/quart/wrappers/__init__.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from .base import BaseRequestWebsocket
 4 | from .request import Body
 5 | from .request import Request
 6 | from .response import Response
 7 | from .websocket import Websocket
 8 | 
 9 | __all__ = (
10 |     "BaseRequestWebsocket",
11 |     "Body",
12 |     "Request",
13 |     "Response",
14 |     "Websocket",
15 | )
16 | 


--------------------------------------------------------------------------------
/src/quart/wrappers/base.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | from typing import Any
  4 | from typing import TYPE_CHECKING
  5 | 
  6 | from hypercorn.typing import WWWScope
  7 | from werkzeug.datastructures import Headers
  8 | from werkzeug.sansio.request import Request as SansIORequest
  9 | 
 10 | from .. import json
 11 | 
 12 | if TYPE_CHECKING:
 13 |     from ..routing import QuartRule  # noqa
 14 | 
 15 | 
 16 | class BaseRequestWebsocket(SansIORequest):
 17 |     """This class is the basis for Requests and websockets..
 18 | 
 19 |     Attributes:
 20 |         json_module: A custom json decoding/encoding module, it should
 21 |             have `dump`, `dumps`, `load`, and `loads` methods
 22 |         routing_exception: If an exception is raised during the route
 23 |             matching it will be stored here.
 24 |         url_rule: The rule that this request has been matched too.
 25 |         view_args: The keyword arguments for the view from the route
 26 |             matching.
 27 |     """
 28 | 
 29 |     json_module: json.provider.JSONProvider = json  # type: ignore
 30 |     routing_exception: Exception | None = None
 31 |     url_rule: QuartRule | None = None
 32 |     view_args: dict[str, Any] | None = None
 33 | 
 34 |     def __init__(
 35 |         self,
 36 |         method: str,
 37 |         scheme: str,
 38 |         path: str,
 39 |         query_string: bytes,
 40 |         headers: Headers,
 41 |         root_path: str,
 42 |         http_version: str,
 43 |         scope: WWWScope,
 44 |     ) -> None:
 45 |         """Create a request or websocket base object.
 46 | 
 47 |         Arguments:
 48 |             method: The HTTP verb.
 49 |             scheme: The scheme used for the request.
 50 |             path: The full unquoted path of the request.
 51 |             query_string: The raw bytes for the query string part.
 52 |             headers: The request headers.
 53 |             root_path: The root path that should be prepended to all
 54 |                 routes.
 55 |             http_version: The HTTP version of the request.
 56 |             scope: Underlying ASGI scope dictionary.
 57 | 
 58 |         Attributes:
 59 |             args: The query string arguments.
 60 |             scheme: The URL scheme, http or https.
 61 |         """
 62 |         super().__init__(
 63 |             method,
 64 |             scheme,
 65 |             scope.get("server"),
 66 |             root_path,
 67 |             path,
 68 |             query_string,
 69 |             headers,
 70 |             headers.get("Remote-Addr"),
 71 |         )
 72 |         self.http_version = http_version
 73 |         self.scope = scope
 74 | 
 75 |     @property
 76 |     def endpoint(self) -> str | None:
 77 |         """Returns the corresponding endpoint matched for this request.
 78 | 
 79 |         This can be None if the request has not been matched with a
 80 |         rule.
 81 |         """
 82 |         if self.url_rule is not None:
 83 |             return self.url_rule.endpoint
 84 |         else:
 85 |             return None
 86 | 
 87 |     @property
 88 |     def blueprint(self) -> str | None:
 89 |         """Returns the blueprint the matched endpoint belongs to.
 90 | 
 91 |         This can be None if the request has not been matched or the
 92 |         endpoint is not in a blueprint.
 93 |         """
 94 |         if self.endpoint is not None and "." in self.endpoint:
 95 |             return self.endpoint.rsplit(".", 1)[0]
 96 |         else:
 97 |             return None
 98 | 
 99 |     @property
100 |     def blueprints(self) -> list[str]:
101 |         """Return the names of the current blueprints.
102 |         The returned list is ordered from the current blueprint,
103 |         upwards through parent blueprints.
104 |         """
105 |         # Avoid circular import
106 |         from ..helpers import _split_blueprint_path
107 | 
108 |         if self.blueprint is not None:
109 |             return _split_blueprint_path(self.blueprint)
110 |         else:
111 |             return []
112 | 
113 |     @property
114 |     def script_root(self) -> str:
115 |         return self.root_path
116 | 
117 |     @property
118 |     def url_root(self) -> str:
119 |         return self.root_url
120 | 


--------------------------------------------------------------------------------
/src/quart/wrappers/websocket.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import asyncio
  4 | from typing import Any
  5 | from typing import AnyStr
  6 | from typing import Callable
  7 | 
  8 | from hypercorn.typing import WebsocketScope
  9 | from werkzeug.datastructures import Headers
 10 | 
 11 | from .base import BaseRequestWebsocket
 12 | 
 13 | 
 14 | class Websocket(BaseRequestWebsocket):
 15 |     def __init__(
 16 |         self,
 17 |         path: str,
 18 |         query_string: bytes,
 19 |         scheme: str,
 20 |         headers: Headers,
 21 |         root_path: str,
 22 |         http_version: str,
 23 |         subprotocols: list[str],
 24 |         receive: Callable,
 25 |         send: Callable,
 26 |         accept: Callable,
 27 |         close: Callable,
 28 |         scope: WebsocketScope,
 29 |     ) -> None:
 30 |         """Create a request object.
 31 | 
 32 |         Arguments:
 33 |             path: The full unquoted path of the request.
 34 |             query_string: The raw bytes for the query string part.
 35 |             scheme: The scheme used for the request.
 36 |             headers: The request headers.
 37 |             root_path: The root path that should be prepended to all
 38 |                 routes.
 39 |             http_version: The HTTP version of the request.
 40 |             subprotocols: The subprotocols requested.
 41 |             receive: Returns an awaitable of the current data
 42 |             accept: Idempotent callable to accept the websocket connection.
 43 |             scope: Underlying ASGI scope dictionary.
 44 |         """
 45 |         super().__init__(
 46 |             "GET", scheme, path, query_string, headers, root_path, http_version, scope
 47 |         )
 48 |         self._accept = accept
 49 |         self._close = close
 50 |         self._receive = receive
 51 |         self._send = send
 52 |         self._subprotocols = subprotocols
 53 | 
 54 |     @property
 55 |     def requested_subprotocols(self) -> list[str]:
 56 |         return self._subprotocols
 57 | 
 58 |     async def receive(self) -> AnyStr:
 59 |         await self.accept()
 60 |         return await self._receive()
 61 | 
 62 |     async def send(self, data: AnyStr) -> None:
 63 |         # Must allow for the event loop to act if the user has say
 64 |         # setup a tight loop sending data over a websocket (as in the
 65 |         # example). So yield via the sleep.
 66 |         await asyncio.sleep(0)
 67 |         await self.accept()
 68 |         await self._send(data)
 69 | 
 70 |     async def receive_json(self) -> Any:
 71 |         data = await self.receive()
 72 |         return self.json_module.loads(data)
 73 | 
 74 |     async def send_json(self, *args: Any, **kwargs: Any) -> None:
 75 |         if args and kwargs:
 76 |             raise TypeError(
 77 |                 "jsonify() behavior undefined when passed both args and kwargs"
 78 |             )
 79 |         elif len(args) == 1:
 80 |             data = args[0]
 81 |         else:
 82 |             data = args or kwargs
 83 | 
 84 |         raw = self.json_module.dumps(data)
 85 |         await self.send(raw)
 86 | 
 87 |     async def accept(
 88 |         self, headers: dict | Headers | None = None, subprotocol: str | None = None
 89 |     ) -> None:
 90 |         """Manually chose to accept the websocket connection.
 91 | 
 92 |         Arguments:
 93 |             headers: Additional headers to send with the acceptance
 94 |                 response.
 95 |             subprotocol: The chosen subprotocol, optional.
 96 |         """
 97 |         if headers is None:
 98 |             headers_ = Headers()
 99 |         else:
100 |             headers_ = Headers(headers)
101 |         await self._accept(headers_, subprotocol)
102 | 
103 |     async def close(self, code: int, reason: str = "") -> None:
104 |         await self._close(code, reason)
105 | 


--------------------------------------------------------------------------------
/tests/assets/config.cfg:
--------------------------------------------------------------------------------
1 | FOO=bar
2 | BOB=jeff
3 | 


--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import pytest
 4 | from hypercorn.typing import HTTPScope
 5 | from hypercorn.typing import WebsocketScope
 6 | 
 7 | 
 8 | @pytest.fixture(name="http_scope")
 9 | def _http_scope() -> HTTPScope:
10 |     return {
11 |         "type": "http",
12 |         "asgi": {},
13 |         "http_version": "1.1",
14 |         "method": "GET",
15 |         "scheme": "https",
16 |         "path": "/",
17 |         "raw_path": b"/",
18 |         "query_string": b"a=b",
19 |         "root_path": "",
20 |         "headers": [
21 |             (b"User-Agent", b"Hypercorn"),
22 |             (b"X-Hypercorn", b"Hypercorn"),
23 |             (b"Referer", b"hypercorn"),
24 |         ],
25 |         "client": ("127.0.0.1", 80),
26 |         "server": None,
27 |         "state": {},  # type: ignore[typeddict-item]
28 |         "extensions": {},
29 |     }
30 | 
31 | 
32 | @pytest.fixture(name="websocket_scope")
33 | def _websocket_scope() -> WebsocketScope:
34 |     return {
35 |         "type": "websocket",
36 |         "asgi": {},
37 |         "http_version": "1.1",
38 |         "scheme": "https",
39 |         "path": "/",
40 |         "raw_path": b"/",
41 |         "query_string": b"a=b",
42 |         "root_path": "",
43 |         "headers": [
44 |             (b"User-Agent", b"Hypercorn"),
45 |             (b"X-Hypercorn", b"Hypercorn"),
46 |             (b"Referer", b"hypercorn"),
47 |         ],
48 |         "client": ("127.0.0.1", 80),
49 |         "server": None,
50 |         "subprotocols": [],
51 |         "state": {},  # type: ignore[typeddict-item]
52 |         "extensions": {},
53 |     }
54 | 


--------------------------------------------------------------------------------
/tests/test_background_tasks.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import asyncio
 4 | import time
 5 | 
 6 | from quart import current_app
 7 | from quart import Quart
 8 | 
 9 | 
10 | async def test_background_task() -> None:
11 |     app = Quart(__name__)
12 |     app.config["DATA"] = "data"
13 | 
14 |     data = None
15 | 
16 |     async def background() -> None:
17 |         nonlocal data
18 |         await asyncio.sleep(0.5)
19 |         data = current_app.config["DATA"]
20 | 
21 |     @app.route("/")
22 |     async def index() -> str:
23 |         app.add_background_task(background)
24 |         return ""
25 | 
26 |     async with app.test_app():
27 |         test_client = app.test_client()
28 |         await test_client.get("/")
29 | 
30 |     assert data == "data"
31 | 
32 | 
33 | async def test_lifespan_background_task() -> None:
34 |     app = Quart(__name__)
35 |     app.config["DATA"] = "data"
36 | 
37 |     data = None
38 | 
39 |     async def background() -> None:
40 |         nonlocal data
41 |         await asyncio.sleep(0.5)
42 |         data = current_app.config["DATA"]
43 | 
44 |     @app.before_serving
45 |     async def startup() -> None:
46 |         app.add_background_task(background)
47 | 
48 |     async with app.test_app():
49 |         pass
50 | 
51 |     assert data == "data"
52 | 
53 | 
54 | async def test_sync_background_task() -> None:
55 |     app = Quart(__name__)
56 |     app.config["DATA"] = "data"
57 | 
58 |     data = None
59 | 
60 |     def background() -> None:
61 |         nonlocal data
62 |         time.sleep(0.5)
63 |         data = current_app.config["DATA"]
64 | 
65 |     @app.route("/")
66 |     async def index() -> str:
67 |         app.add_background_task(background)
68 |         return ""
69 | 
70 |     async with app.test_app():
71 |         test_client = app.test_client()
72 |         await test_client.get("/")
73 | 
74 |     assert data == "data"
75 | 


--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import os
  4 | import tempfile
  5 | from collections.abc import Generator
  6 | from pathlib import Path
  7 | from unittest.mock import Mock
  8 | 
  9 | import pytest
 10 | from _pytest.monkeypatch import MonkeyPatch
 11 | from click.testing import CliRunner
 12 | 
 13 | import quart.cli
 14 | from quart.app import Quart
 15 | from quart.cli import AppGroup
 16 | from quart.cli import cli
 17 | from quart.cli import load_dotenv
 18 | from quart.cli import ScriptInfo
 19 | 
 20 | 
 21 | @pytest.fixture(scope="module")
 22 | def reset_env() -> None:
 23 |     os.environ.pop("QUART_DEBUG", None)
 24 | 
 25 | 
 26 | @pytest.fixture(name="app")
 27 | def loadable_app(monkeypatch: MonkeyPatch) -> Mock:
 28 |     app = Mock(spec=Quart)
 29 |     app.cli = AppGroup()
 30 |     module = Mock()
 31 |     module.app = app
 32 |     monkeypatch.setattr(quart.cli, "import_module", lambda _: module)
 33 |     return app
 34 | 
 35 | 
 36 | @pytest.fixture(name="dev_app")
 37 | def loadable_dev_app(app: Mock) -> Mock:
 38 |     app.debug = True
 39 |     return app
 40 | 
 41 | 
 42 | @pytest.fixture(name="debug_env")
 43 | def debug_env_patch(monkeypatch: MonkeyPatch) -> None:
 44 |     monkeypatch.setenv("QUART_DEBUG", "true")
 45 | 
 46 | 
 47 | @pytest.fixture(name="no_debug_env")
 48 | def no_debug_env_patch(monkeypatch: MonkeyPatch) -> None:
 49 |     monkeypatch.setenv("QUART_DEBUG", "false")
 50 | 
 51 | 
 52 | @pytest.fixture(name="empty_cwd")
 53 | def empty_cwd() -> Generator[Path, None, None]:
 54 |     directory = tempfile.TemporaryDirectory()
 55 |     cwd = os.getcwd()
 56 |     os.chdir(directory.name)
 57 | 
 58 |     yield Path(directory.name)
 59 | 
 60 |     os.chdir(cwd)
 61 |     directory.cleanup()
 62 | 
 63 | 
 64 | def test_script_info_load_app(app: Mock) -> None:
 65 |     info = ScriptInfo("module:app")
 66 |     assert info.load_app() == app
 67 | 
 68 | 
 69 | def test_version_command() -> None:
 70 |     runner = CliRunner()
 71 |     result = runner.invoke(cli, ["--version"])
 72 |     assert "Quart" in result.output
 73 | 
 74 | 
 75 | def test_run_command(app: Mock) -> None:
 76 |     runner = CliRunner()
 77 |     runner.invoke(cli, ["--app", "module:app", "run"])
 78 |     app.run.assert_called_once_with(
 79 |         debug=False,
 80 |         host="127.0.0.1",
 81 |         port=5000,
 82 |         certfile=None,
 83 |         keyfile=None,
 84 |         use_reloader=False,
 85 |     )
 86 | 
 87 | 
 88 | def test_run_command_development_debug_disabled(
 89 |     dev_app: Mock, no_debug_env: None
 90 | ) -> None:
 91 |     runner = CliRunner()
 92 |     runner.invoke(cli, ["--app", "module:app", "run"])
 93 |     dev_app.run.assert_called_once_with(
 94 |         debug=False,
 95 |         host="127.0.0.1",
 96 |         port=5000,
 97 |         certfile=None,
 98 |         keyfile=None,
 99 |         use_reloader=False,
100 |     )
101 | 
102 | 
103 | def test_load_dotenv(empty_cwd: Path) -> None:
104 |     value = "dotenv"
105 |     with open(empty_cwd / ".env", "w", encoding="utf8") as env:
106 |         env.write(f"TEST_ENV_VAR={value}\n")
107 | 
108 |     load_dotenv()
109 | 
110 |     assert os.environ.pop("TEST_ENV_VAR", None) == value
111 | 
112 | 
113 | def test_load_dotquartenv(empty_cwd: Path) -> None:
114 |     value = "dotquartenv"
115 |     with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env:
116 |         env.write(f"TEST_ENV_VAR={value}\n")
117 | 
118 |     load_dotenv()
119 | 
120 |     assert os.environ.pop("TEST_ENV_VAR", None) == value
121 | 
122 | 
123 | def test_load_dotenv_beats_dotquartenv(empty_cwd: Path) -> None:
124 |     env_value = "dotenv"
125 |     quartenv_value = "dotquartenv"
126 | 
127 |     with open(empty_cwd / ".env", "w", encoding="utf8") as env:
128 |         env.write(f"TEST_ENV_VAR={env_value}\n")
129 |     with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env:
130 |         env.write(f"TEST_ENV_VAR={quartenv_value}\n")
131 | 
132 |     load_dotenv()
133 | 
134 |     assert os.environ.pop("TEST_ENV_VAR", None) == env_value
135 | 


--------------------------------------------------------------------------------
/tests/test_debug.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from quart import Quart
 4 | from quart.debug import traceback_response
 5 | 
 6 | 
 7 | async def test_debug() -> None:
 8 |     app = Quart(__name__)
 9 |     async with app.test_request_context("/"):
10 |         response = await traceback_response(Exception("Unique error"))
11 | 
12 |     assert response.status_code == 500
13 |     assert b"Unique error" in (await response.get_data())
14 | 


--------------------------------------------------------------------------------
/tests/test_exceptions.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from http import HTTPStatus
 4 | 
 5 | import pytest
 6 | from werkzeug.exceptions import abort
 7 | from werkzeug.exceptions import HTTPException
 8 | 
 9 | from quart import Response
10 | 
11 | 
12 | @pytest.mark.parametrize("status", [400, HTTPStatus.BAD_REQUEST])
13 | def test_abort(status: int | HTTPStatus) -> None:
14 |     with pytest.raises(HTTPException) as exc_info:
15 |         abort(status)
16 |     assert exc_info.value.get_response().status_code == 400
17 | 
18 | 
19 | def test_abort_with_response() -> None:
20 |     with pytest.raises(HTTPException) as exc_info:
21 |         abort(Response("Message", 205))
22 |     assert exc_info.value.get_response().status_code == 205
23 | 


--------------------------------------------------------------------------------
/tests/test_formparser.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import pytest
 4 | from werkzeug.exceptions import RequestEntityTooLarge
 5 | 
 6 | from quart.formparser import MultiPartParser
 7 | from quart.wrappers.request import Body
 8 | 
 9 | 
10 | async def test_multipart_max_form_memory_size() -> None:
11 |     """max_form_memory_size is tracked across multiple data events."""
12 |     data = b"--bound\r\nContent-Disposition: form-field; name=a\r\n\r\n"
13 |     data += b"a" * 15 + b"\r\n--bound--"
14 |     body = Body(None, None)
15 |     body.set_result(data)
16 |     # The buffer size is less than the max size, so multiple data events will be
17 |     # returned. The field size is greater than the max.
18 |     parser = MultiPartParser(max_form_memory_size=10, buffer_size=5)
19 | 
20 |     with pytest.raises(RequestEntityTooLarge):
21 |         await parser.parse(body, b"bound", 0)
22 | 


--------------------------------------------------------------------------------
/tests/test_routing.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import warnings
 4 | 
 5 | import pytest
 6 | from hypercorn.typing import HTTPScope
 7 | from werkzeug.datastructures import Headers
 8 | 
 9 | from quart.routing import QuartMap
10 | from quart.testing import no_op_push
11 | from quart.wrappers.request import Request
12 | 
13 | 
14 | @pytest.mark.parametrize(
15 |     "server_name, warns",
16 |     [("localhost", False), ("quart.com", True)],
17 | )
18 | async def test_bind_warning(
19 |     server_name: str, warns: bool, http_scope: HTTPScope
20 | ) -> None:
21 |     map_ = QuartMap(host_matching=False)
22 |     request = Request(
23 |         "GET",
24 |         "http",
25 |         "/",
26 |         b"",
27 |         Headers([("host", "Localhost")]),
28 |         "",
29 |         "1.1",
30 |         http_scope,
31 |         send_push_promise=no_op_push,
32 |     )
33 | 
34 |     if warns:
35 |         with pytest.warns(UserWarning):
36 |             map_.bind_to_request(request, subdomain=None, server_name=server_name)
37 |     else:
38 |         with warnings.catch_warnings():
39 |             warnings.simplefilter("error")
40 |             map_.bind_to_request(request, subdomain=None, server_name=server_name)
41 | 


--------------------------------------------------------------------------------
/tests/test_sessions.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from http.cookies import SimpleCookie
 4 | 
 5 | from hypercorn.typing import HTTPScope
 6 | from werkzeug.datastructures import Headers
 7 | 
 8 | from quart.app import Quart
 9 | from quart.sessions import SecureCookieSession
10 | from quart.sessions import SecureCookieSessionInterface
11 | from quart.testing import no_op_push
12 | from quart.wrappers import Request
13 | from quart.wrappers import Response
14 | 
15 | 
16 | async def test_secure_cookie_session_interface_open_session(
17 |     http_scope: HTTPScope,
18 | ) -> None:
19 |     session = SecureCookieSession()
20 |     session["something"] = "else"
21 |     interface = SecureCookieSessionInterface()
22 |     app = Quart(__name__)
23 |     app.secret_key = "secret"
24 |     response = Response("")
25 |     await interface.save_session(app, session, response)
26 |     request = Request(
27 |         "GET",
28 |         "http",
29 |         "/",
30 |         b"",
31 |         Headers(),
32 |         "",
33 |         "1.1",
34 |         http_scope,
35 |         send_push_promise=no_op_push,
36 |     )
37 |     request.headers["Cookie"] = response.headers["Set-Cookie"]
38 |     new_session = await interface.open_session(app, request)
39 |     assert new_session == session
40 | 
41 | 
42 | async def test_secure_cookie_session_interface_save_session() -> None:
43 |     session = SecureCookieSession()
44 |     session["something"] = "else"
45 |     interface = SecureCookieSessionInterface()
46 |     app = Quart(__name__)
47 |     app.secret_key = "secret"
48 |     response = Response("")
49 |     await interface.save_session(app, session, response)
50 |     cookies: SimpleCookie = SimpleCookie()
51 |     cookies.load(response.headers["Set-Cookie"])
52 |     cookie = cookies[app.config["SESSION_COOKIE_NAME"]]
53 |     assert cookie["path"] == interface.get_cookie_path(app)
54 |     assert cookie["httponly"] == "" if not interface.get_cookie_httponly(app) else True
55 |     assert cookie["secure"] == "" if not interface.get_cookie_secure(app) else True
56 |     assert cookie["samesite"] == (interface.get_cookie_samesite(app) or "")
57 |     assert cookie["domain"] == (interface.get_cookie_domain(app) or "")
58 |     assert cookie["expires"] == (interface.get_expiration_time(app, session) or "")
59 |     assert response.headers["Vary"] == "Cookie"
60 | 
61 | 
62 | async def _save_session(session: SecureCookieSession) -> Response:
63 |     interface = SecureCookieSessionInterface()
64 |     app = Quart(__name__)
65 |     app.secret_key = "secret"
66 |     response = Response("")
67 |     await interface.save_session(app, session, response)
68 |     return response
69 | 
70 | 
71 | async def test_secure_cookie_session_interface_save_session_no_modification() -> None:
72 |     session = SecureCookieSession()
73 |     session["something"] = "else"
74 |     session.modified = False
75 |     response = await _save_session(session)
76 |     assert response.headers.get("Set-Cookie") is None
77 | 
78 | 
79 | async def test_secure_cookie_session_interface_save_session_no_access() -> None:
80 |     session = SecureCookieSession()
81 |     session["something"] = "else"
82 |     session.accessed = False
83 |     session.modified = False
84 |     response = await _save_session(session)
85 |     assert response.headers.get("Set-Cookie") is None
86 |     assert response.headers.get("Vary") is None
87 | 


--------------------------------------------------------------------------------
/tests/test_static_hosting.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from pathlib import Path
 4 | 
 5 | from quart.app import Quart
 6 | 
 7 | 
 8 | async def test_host_matching() -> None:
 9 |     app = Quart(__name__, static_folder="./assets", static_url_path="/static")
10 | 
11 |     test_client = app.test_client()
12 | 
13 |     response = await test_client.get("/static/config.cfg")
14 |     assert response.status_code == 200
15 |     data = await response.get_data(as_text=False)
16 |     expected_data = (Path(__file__).parent / "assets/config.cfg").read_bytes()
17 |     assert data == expected_data
18 | 
19 |     response = await test_client.get("/static/foo")
20 |     assert response.status_code == 404
21 | 
22 |     # Should not be able to escape !
23 |     response = await test_client.get("/static/../foo")
24 |     assert response.status_code == 404
25 | 
26 |     response = await test_client.get("/static/../assets/config.cfg")
27 |     assert response.status_code == 404
28 | 
29 |     # Non-escaping path with ..
30 |     response = await test_client.get("/static/foo/../config.cfg")
31 |     assert response.status_code == 200
32 |     data = await response.get_data(as_text=False)
33 |     assert data == expected_data
34 | 


--------------------------------------------------------------------------------
/tests/test_sync.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | import threading
 4 | from collections.abc import Generator
 5 | 
 6 | import pytest
 7 | 
 8 | from quart import Quart
 9 | from quart import request
10 | from quart import ResponseReturnValue
11 | 
12 | 
13 | @pytest.fixture(name="app")
14 | def _app() -> Quart:
15 |     app = Quart(__name__)
16 | 
17 |     @app.route("/", methods=["GET", "POST"])
18 |     def index() -> ResponseReturnValue:
19 |         return request.method
20 | 
21 |     @app.route("/gen")
22 |     def gen() -> ResponseReturnValue:
23 |         def _gen() -> Generator[bytes, None, None]:
24 |             yield b"%d" % threading.current_thread().ident
25 |             for _ in range(2):
26 |                 yield b"b"
27 | 
28 |         return _gen(), 200
29 | 
30 |     return app
31 | 
32 | 
33 | async def test_sync_request_context(app: Quart) -> None:
34 |     test_client = app.test_client()
35 |     response = await test_client.get("/")
36 |     assert b"GET" in (await response.get_data())
37 |     response = await test_client.post("/")
38 |     assert b"POST" in (await response.get_data())
39 | 
40 | 
41 | async def test_sync_generator(app: Quart) -> None:
42 |     test_client = app.test_client()
43 |     response = await test_client.get("/gen")
44 |     result = await response.get_data()
45 |     assert result[-2:] == b"bb"
46 |     assert int(result[:-2]) != threading.current_thread().ident
47 | 


--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from werkzeug.datastructures import Headers
 4 | 
 5 | from quart.utils import decode_headers
 6 | from quart.utils import encode_headers
 7 | 
 8 | 
 9 | def test_encode_headers() -> None:
10 |     assert encode_headers(Headers({"Foo": "Bar"})) == [(b"foo", b"Bar")]
11 | 
12 | 
13 | def test_decode_headers() -> None:
14 |     assert decode_headers([(b"foo", b"Bar")]) == Headers({"Foo": "Bar"})
15 | 


--------------------------------------------------------------------------------
/tests/test_views.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Any
 4 | from typing import Callable
 5 | 
 6 | import pytest
 7 | 
 8 | from quart import Quart
 9 | from quart import request
10 | from quart import ResponseReturnValue
11 | from quart.views import MethodView
12 | from quart.views import View
13 | 
14 | 
15 | @pytest.fixture
16 | def app() -> Quart:
17 |     app = Quart(__name__)
18 |     return app
19 | 
20 | 
21 | async def test_view(app: Quart) -> None:
22 |     class Views(View):
23 |         methods = ["GET", "POST"]
24 | 
25 |         async def dispatch_request(
26 |             self, *args: Any, **kwargs: Any
27 |         ) -> ResponseReturnValue:
28 |             return request.method
29 | 
30 |     app.add_url_rule("/", view_func=Views.as_view("simple"))
31 | 
32 |     test_client = app.test_client()
33 |     response = await test_client.get("/")
34 |     assert "GET" == (await response.get_data(as_text=True))
35 |     response = await test_client.put("/")
36 |     assert response.status_code == 405
37 | 
38 | 
39 | async def test_method_view(app: Quart) -> None:
40 |     class Views(MethodView):
41 |         async def get(self) -> ResponseReturnValue:
42 |             return "GET"
43 | 
44 |         async def post(self) -> ResponseReturnValue:
45 |             return "POST"
46 | 
47 |     app.add_url_rule("/", view_func=Views.as_view("simple"))
48 | 
49 |     test_client = app.test_client()
50 |     response = await test_client.get("/")
51 |     assert "GET" == (await response.get_data(as_text=True))
52 |     response = await test_client.post("/")
53 |     assert "POST" == (await response.get_data(as_text=True))
54 | 
55 | 
56 | async def test_view_decorators(app: Quart) -> None:
57 |     def decorate_status_code(func: Callable) -> Callable:
58 |         async def wrapper(*args: Any, **kwargs: Any) -> ResponseReturnValue:
59 |             response = await func(*args, **kwargs)
60 |             return response, 201
61 | 
62 |         return wrapper
63 | 
64 |     class Views(View):
65 |         decorators = [decorate_status_code]
66 |         methods = ["GET"]
67 | 
68 |         async def dispatch_request(
69 |             self, *args: Any, **kwargs: Any
70 |         ) -> ResponseReturnValue:
71 |             return request.method
72 | 
73 |     app.add_url_rule("/", view_func=Views.as_view("simple"))
74 | 
75 |     test_client = app.test_client()
76 |     response = await test_client.get("/")
77 |     assert response.status_code == 201
78 | 


--------------------------------------------------------------------------------
/tests/wrappers/test_base.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | from base64 import b64encode
  4 | 
  5 | import pytest
  6 | from hypercorn.typing import HTTPScope
  7 | from werkzeug.datastructures import Headers
  8 | 
  9 | from quart.wrappers.base import BaseRequestWebsocket
 10 | 
 11 | 
 12 | def test_basic_authorization(http_scope: HTTPScope) -> None:
 13 |     headers = Headers()
 14 |     headers["Authorization"] = "Basic {}".format(
 15 |         b64encode(b"identity:secret").decode("ascii")
 16 |     )
 17 |     request = BaseRequestWebsocket(
 18 |         "GET", "http", "/", b"", headers, "", "1.1", http_scope
 19 |     )
 20 |     auth = request.authorization
 21 |     assert auth.username == "identity"
 22 |     assert auth.password == "secret"
 23 | 
 24 | 
 25 | def test_digest_authorization(http_scope: HTTPScope) -> None:
 26 |     headers = Headers()
 27 |     headers["Authorization"] = (
 28 |         "Digest "
 29 |         'username="identity", '
 30 |         'realm="realm@rea.lm", '
 31 |         'nonce="abcd1234", '
 32 |         'uri="/path", '
 33 |         'response="abcd1235", '
 34 |         'opaque="abcd1236"'
 35 |     )
 36 |     request = BaseRequestWebsocket(
 37 |         "GET", "http", "/", b"", headers, "", "1.1", http_scope
 38 |     )
 39 |     auth = request.authorization
 40 |     assert auth.username == "identity"
 41 |     assert auth.realm == "realm@rea.lm"
 42 |     assert auth.nonce == "abcd1234"
 43 |     assert auth.uri == "/path"
 44 |     assert auth.response == "abcd1235"
 45 |     assert auth.opaque == "abcd1236"
 46 | 
 47 | 
 48 | @pytest.mark.parametrize(
 49 |     "method, scheme, host, path, query_string,"
 50 |     "expected_path, expected_full_path, expected_url, expected_base_url,"
 51 |     "expected_url_root, expected_host_url",
 52 |     [
 53 |         (
 54 |             "GET",
 55 |             "http",
 56 |             "quart.com",
 57 |             "/",
 58 |             b"",
 59 |             "/",
 60 |             "/?",
 61 |             "http://quart.com/",
 62 |             "http://quart.com/",
 63 |             "http://quart.com/",
 64 |             "http://quart.com/",
 65 |         ),
 66 |         (
 67 |             "GET",
 68 |             "http",
 69 |             "quart.com",
 70 |             "/",
 71 |             b"a=b",
 72 |             "/",
 73 |             "/?a=b",
 74 |             "http://quart.com/?a=b",
 75 |             "http://quart.com/",
 76 |             "http://quart.com/",
 77 |             "http://quart.com/",
 78 |         ),
 79 |         (
 80 |             "GET",
 81 |             "https",
 82 |             "quart.com",
 83 |             "/branch/leaf",
 84 |             b"a=b",
 85 |             "/branch/leaf",
 86 |             "/branch/leaf?a=b",
 87 |             "https://quart.com/branch/leaf?a=b",
 88 |             "https://quart.com/branch/leaf",
 89 |             "https://quart.com/",
 90 |             "https://quart.com/",
 91 |         ),
 92 |     ],
 93 | )
 94 | def test_url_structure(
 95 |     method: str,
 96 |     scheme: str,
 97 |     host: str,
 98 |     path: str,
 99 |     query_string: bytes,
100 |     expected_path: str,
101 |     expected_full_path: str,
102 |     expected_url: str,
103 |     expected_base_url: str,
104 |     expected_url_root: str,
105 |     expected_host_url: str,
106 |     http_scope: HTTPScope,
107 | ) -> None:
108 |     base_request_websocket = BaseRequestWebsocket(
109 |         method,
110 |         scheme,
111 |         path,
112 |         query_string,
113 |         Headers({"host": host}),
114 |         "",
115 |         "1.1",
116 |         http_scope,
117 |     )
118 | 
119 |     assert base_request_websocket.path == expected_path
120 |     assert base_request_websocket.query_string == query_string
121 |     assert base_request_websocket.full_path == expected_full_path
122 |     assert base_request_websocket.url == expected_url
123 |     assert base_request_websocket.base_url == expected_base_url
124 |     assert base_request_websocket.url_root == expected_url_root
125 |     assert base_request_websocket.host_url == expected_host_url
126 |     assert base_request_websocket.host == host
127 |     assert base_request_websocket.method == method
128 |     assert base_request_websocket.scheme == scheme
129 |     assert base_request_websocket.is_secure == scheme.endswith("s")
130 | 
131 | 
132 | def test_query_string(http_scope: HTTPScope) -> None:
133 |     base_request_websocket = BaseRequestWebsocket(
134 |         "GET",
135 |         "http",
136 |         "/",
137 |         b"a=b&a=c&f",
138 |         Headers({"host": "localhost"}),
139 |         "",
140 |         "1.1",
141 |         http_scope,
142 |     )
143 |     assert base_request_websocket.query_string == b"a=b&a=c&f"
144 |     assert base_request_websocket.args.getlist("a") == ["b", "c"]
145 |     assert base_request_websocket.args["f"] == ""
146 | 


--------------------------------------------------------------------------------
/tests/wrappers/test_request.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import asyncio
  4 | from urllib.parse import urlencode
  5 | 
  6 | import pytest
  7 | from hypercorn.typing import HTTPScope
  8 | from werkzeug.datastructures import Headers
  9 | from werkzeug.exceptions import RequestEntityTooLarge
 10 | from werkzeug.exceptions import RequestTimeout
 11 | 
 12 | from quart.testing import no_op_push
 13 | from quart.wrappers.request import Body
 14 | from quart.wrappers.request import Request
 15 | 
 16 | 
 17 | async def _fill_body(body: Body, semaphore: asyncio.Semaphore, limit: int) -> None:
 18 |     for number in range(limit):
 19 |         body.append(b"%d" % number)
 20 |         await semaphore.acquire()
 21 |     body.set_complete()
 22 | 
 23 | 
 24 | async def test_full_body() -> None:
 25 |     body = Body(None, None)
 26 |     limit = 3
 27 |     semaphore = asyncio.Semaphore(limit)
 28 |     asyncio.ensure_future(_fill_body(body, semaphore, limit))
 29 |     assert b"012" == await body
 30 | 
 31 | 
 32 | async def test_body_streaming() -> None:
 33 |     body = Body(None, None)
 34 |     limit = 3
 35 |     semaphore = asyncio.Semaphore(0)
 36 |     asyncio.ensure_future(_fill_body(body, semaphore, limit))
 37 |     index = 0
 38 |     async for data in body:
 39 |         semaphore.release()
 40 |         assert data == b"%d" % index
 41 |         index += 1
 42 |     assert b"" == await body
 43 | 
 44 | 
 45 | async def test_body_stream_single_chunk() -> None:
 46 |     body = Body(None, None)
 47 |     body.append(b"data")
 48 |     body.set_complete()
 49 | 
 50 |     async def _check_data() -> None:
 51 |         async for data in body:
 52 |             assert data == b"data"
 53 | 
 54 |     await asyncio.wait_for(_check_data(), 1)
 55 | 
 56 | 
 57 | async def test_body_streaming_no_data() -> None:
 58 |     body = Body(None, None)
 59 |     semaphore = asyncio.Semaphore(0)
 60 |     asyncio.ensure_future(_fill_body(body, semaphore, 0))
 61 |     async for _ in body:  # noqa: F841
 62 |         raise AssertionError("Should not reach this line")
 63 |     assert b"" == await body
 64 | 
 65 | 
 66 | async def test_body_exceeds_max_content_length() -> None:
 67 |     max_content_length = 5
 68 |     body = Body(None, max_content_length)
 69 |     body.append(b" " * (max_content_length + 1))
 70 |     with pytest.raises(RequestEntityTooLarge):
 71 |         await body
 72 | 
 73 | 
 74 | async def test_request_exceeds_max_content_length(http_scope: HTTPScope) -> None:
 75 |     max_content_length = 5
 76 |     headers = Headers()
 77 |     headers["Content-Length"] = str(max_content_length + 1)
 78 |     request = Request(
 79 |         "POST",
 80 |         "http",
 81 |         "/",
 82 |         b"",
 83 |         headers,
 84 |         "",
 85 |         "1.1",
 86 |         http_scope,
 87 |         max_content_length=max_content_length,
 88 |         send_push_promise=no_op_push,
 89 |     )
 90 |     with pytest.raises(RequestEntityTooLarge):
 91 |         await request.get_data()
 92 | 
 93 | 
 94 | async def test_request_get_data_timeout(http_scope: HTTPScope) -> None:
 95 |     request = Request(
 96 |         "POST",
 97 |         "http",
 98 |         "/",
 99 |         b"",
100 |         Headers(),
101 |         "",
102 |         "1.1",
103 |         http_scope,
104 |         body_timeout=1,
105 |         send_push_promise=no_op_push,
106 |     )
107 |     with pytest.raises(RequestTimeout):
108 |         await request.get_data()
109 | 
110 | 
111 | @pytest.mark.parametrize(
112 |     "method, expected",
113 |     [("GET", ["b", "c"]), ("POST", ["b", "c", "d"])],
114 | )
115 | async def test_request_values(
116 |     method: str, expected: list[str], http_scope: HTTPScope
117 | ) -> None:
118 |     request = Request(
119 |         method,
120 |         "http",
121 |         "/",
122 |         b"a=b&a=c",
123 |         Headers(
124 |             {"host": "quart.com", "Content-Type": "application/x-www-form-urlencoded"}
125 |         ),
126 |         "",
127 |         "1.1",
128 |         http_scope,
129 |         send_push_promise=no_op_push,
130 |     )
131 |     request.body.append(urlencode({"a": "d"}).encode())
132 |     request.body.set_complete()
133 |     assert (await request.values).getlist("a") == expected
134 | 
135 | 
136 | async def test_request_send_push_promise(http_scope: HTTPScope) -> None:
137 |     push_promise: tuple[str, Headers] = None
138 | 
139 |     async def _push(path: str, headers: Headers) -> None:
140 |         nonlocal push_promise
141 |         push_promise = (path, headers)
142 | 
143 |     request = Request(
144 |         "GET",
145 |         "http",
146 |         "/",
147 |         b"a=b&a=c",
148 |         Headers(
149 |             {
150 |                 "host": "quart.com",
151 |                 "Content-Type": "application/x-www-form-urlencoded",
152 |                 "Accept": "*/*",
153 |                 "Accept-Encoding": "gzip",
154 |                 "User-Agent": "quart",
155 |             }
156 |         ),
157 |         "",
158 |         "2",
159 |         http_scope,
160 |         send_push_promise=_push,
161 |     )
162 |     await request.send_push_promise("/")
163 |     assert push_promise[0] == "/"
164 |     valid_headers = {"Accept": "*/*", "Accept-Encoding": "gzip", "User-Agent": "quart"}
165 |     assert len(push_promise[1]) == len(valid_headers)
166 |     for name, value in valid_headers.items():
167 |         assert push_promise[1][name] == value
168 | 


--------------------------------------------------------------------------------