pax_global_header 0000666 0000000 0000000 00000000064 15254677127 0014532 g ustar 00root root 0000000 0000000 52 comment=2269e9a08c1edd3dfdea865710f40f84c857b623
starlette-1.7.0/ 0000775 0000000 0000000 00000000000 15254677127 0013546 5 ustar 00root root 0000000 0000000 starlette-1.7.0/.github/ 0000775 0000000 0000000 00000000000 15254677127 0015106 5 ustar 00root root 0000000 0000000 starlette-1.7.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000017 15254677127 0016721 0 ustar 00root root 0000000 0000000 github: Kludex
starlette-1.7.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15254677127 0017271 5 ustar 00root root 0000000 0000000 starlette-1.7.0/.github/ISSUE_TEMPLATE/1-issue.md 0000664 0000000 0000000 00000001120 15254677127 0021073 0 ustar 00root root 0000000 0000000 ---
name: Issue
about: Please only raise an issue if you've been advised to do so after discussion. Thanks! �
---
The starting point for issues should usually be a discussion...
https://github.com/Kludex/starlette/discussions
Possible bugs may be raised as a "Potential Issue" discussion, feature requests may be raised as an "Ideas" discussion. We can then determine if the discussion needs to be escalated into an "Issue" or not.
This will help us ensure that the "Issues" list properly reflects ongoing or needed work on the project.
---
- [ ] Initially raised as discussion #...
starlette-1.7.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000677 15254677127 0021273 0 ustar 00root root 0000000 0000000 # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/Kludex/starlette/discussions
about: >
The "Discussions" forum is where you want to start. 💖
- name: Chat
url: https://discord.gg/SWU73HffbV
about: >
Our community chat forum.
starlette-1.7.0/.github/dependabot.yml 0000664 0000000 0000000 00000000635 15254677127 0017742 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
groups:
python-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
cooldown:
default-days: 7
groups:
github-actions:
patterns:
- "*"
starlette-1.7.0/.github/pull_request_template.md 0000664 0000000 0000000 00000001043 15254677127 0022045 0 ustar 00root root 0000000 0000000
# Summary
# Checklist
- [ ] I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
- [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
- [ ] I've updated the documentation accordingly.
starlette-1.7.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15254677127 0017143 5 ustar 00root root 0000000 0000000 starlette-1.7.0/.github/workflows/benchmark.yml 0000664 0000000 0000000 00000003203 15254677127 0021616 0 ustar 00root root 0000000 0000000 ---
name: CodSpeed
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
env:
PYTHONHASHSEED: "0"
# https://sourceware.org/glibc/manual/latest/html_node/Hardware-Capability-Tunables.html
# Keep upload buffers above a fixed mmap threshold to reduce heap-dependent realloc copies.
GLIBC_TUNABLES: "glibc.cpu.hwcaps=-AVX512F,-AVX512VL,-AVX512BW,-ERMS,-FSRM:glibc.malloc.mmap_threshold=32768"
steps:
- uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.14"
enable-cache: true
- name: Install dependencies
run: scripts/install
shell: bash
- name: Run the benchmarks
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
with:
run: >-
uv run pytest
benchmarks/gzip_benchmark.py
benchmarks/multipart_benchmark.py
benchmarks/request_benchmark.py
benchmarks/response_benchmark.py
benchmarks/routing_benchmark.py
benchmarks/json_benchmark.py
benchmarks/file_response_benchmark.py
benchmarks/streaming_response_benchmark.py
benchmarks/urlencoded_benchmark.py
benchmarks/websocket_benchmark.py
--codspeed
mode: simulation,memory
starlette-1.7.0/.github/workflows/main.yml 0000664 0000000 0000000 00000005533 15254677127 0020620 0 ustar 00root root 0000000 0000000 ---
name: Test Suite
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
tests:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install dependencies
run: scripts/install
- name: Run linting checks
run: scripts/check
if: ${{ matrix.python-version != '3.14' }}
- name: "Build package & docs"
run: scripts/build
- name: "Run tests"
run: scripts/test
- name: "Enforce coverage"
run: scripts/coverage
# https://github.com/marketplace/actions/alls-green#why
check:
if: always()
needs: [tests]
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
with:
jobs: ${{ toJSON(needs) }}
docs-cloudflare-preview:
name: Docs preview (Cloudflare)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
HAS_CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN != '' }}
permissions:
contents: read
pull-requests: write
environment:
name: cloudflare
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"
enable-cache: false
- name: Install dependencies
run: scripts/install
- name: Build docs
run: uv run zensical build --clean
- uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
id: deploy
if: ${{ env.HAS_CLOUDFLARE_TOKEN == 'true' }}
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload --tag pr-${{ github.event.pull_request.number }}
- name: Comment preview URL
if: ${{ steps.deploy.outcome == 'success' }}
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
message: |
**Docs preview:** ${{ steps.deploy.outputs.deployment-url }}
starlette-1.7.0/.github/workflows/publish.yml 0000664 0000000 0000000 00000006156 15254677127 0021344 0 ustar 00root root 0000000 0000000 name: Publish
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"
enable-cache: false
- name: Install dependencies
run: scripts/install
- name: Build package & docs
run: scripts/build
- name: Upload package distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: package-distributions
path: dist/
- name: Upload documentation
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: documentation
path: site/
pypi-publish:
runs-on: ubuntu-latest
needs: build
if: success() && startsWith(github.ref, 'refs/tags/')
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/project/starlette
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
docs-publish:
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Configure GitHub Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: documentation
path: site/
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
id: deployment
docs-cloudflare:
runs-on: ubuntu-latest
needs: build
environment:
name: cloudflare
url: https://starlette.dev
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: documentation
path: site/
- uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
starlette-1.7.0/.github/workflows/zizmor.yml 0000664 0000000 0000000 00000001121 15254677127 0021213 0 ustar 00root root 0000000 0000000 name: GitHub Actions Security Analysis
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
starlette-1.7.0/.gitignore 0000664 0000000 0000000 00000000207 15254677127 0015535 0 ustar 00root root 0000000 0000000 *.pyc
test.db
.coverage
.pytest_cache/
.mypy_cache/
__pycache__/
htmlcov/
site/
*.egg-info/
venv*/
.venv/
.python-version
build/
dist/
starlette-1.7.0/AGENTS.md 0000664 0000000 0000000 00000000166 15254677127 0015054 0 ustar 00root root 0000000 0000000 ## Writing Documentation
1. After each heading, always include text explaining the section before any code snippets.
starlette-1.7.0/AI_POLICY.md 0000664 0000000 0000000 00000000751 15254677127 0015443 0 ustar 00root root 0000000 0000000 # AI Policy
You may use AI tools when contributing to Starlette. You remain responsible for every contribution you submit.
Before you open a pull request, search the open and closed pull requests, issues, and discussions. Do not open a pull
request when the same change has already been proposed or is already in progress.
Duplicate pull requests waste maintainer time and create unnecessary review work. Contributors who submit duplicate
pull requests will be banned from the project.
starlette-1.7.0/CITATION.cff 0000664 0000000 0000000 00000001163 15254677127 0015441 0 ustar 00root root 0000000 0000000 # This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: Starlette
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- given-names: Marcelo
family-names: Trylesinski
email: marcelotryle@gmail.com
- given-names: Tom
family-names: Christie
email: tom@tomchristie.com
repository-code: "https://github.com/Kludex/starlette"
url: "https://starlette.dev/"
abstract: Starlette is an ASGI web framework for Python.
keywords:
- asgi
- starlette
license: BSD-3-Clause
starlette-1.7.0/LICENSE.md 0000664 0000000 0000000 00000002756 15254677127 0015164 0 ustar 00root root 0000000 0000000 Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/).
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
starlette-1.7.0/README.md 0000664 0000000 0000000 00000011512 15254677127 0015025 0 ustar 00root root 0000000 0000000
✨ The little ASGI framework that shines. ✨
---
[](https://github.com/Kludex/starlette/actions)
[](https://pypi.python.org/pypi/starlette)
[](https://pypi.org/project/starlette)
[](https://discord.gg/RxKUF5JuHs)
---
**Documentation**: https://starlette.dev
**Source Code**: https://github.com/Kludex/starlette
---
# Starlette
Starlette is a lightweight [ASGI][asgi] framework/toolkit,
which is ideal for building async web services in Python.
It is production-ready, and gives you the following:
* A lightweight, low-complexity HTTP web framework.
* WebSocket support.
* In-process background tasks.
* Startup and shutdown events.
* Test client built on `httpx2`.
* CORS, GZip, Static Files, Streaming responses.
* Session and Cookie support.
* 100% test coverage.
* 100% type annotated codebase.
* Few hard dependencies.
* Compatible with `asyncio` and `trio` backends.
* Great overall performance [against independent benchmarks][techempower].
## Installation
```shell
$ pip install starlette
```
You'll also want to install an ASGI server, such as [uvicorn](https://uvicorn.dev) or any of the [other ASGI server implementations](https://asgi.readthedocs.io/en/latest/implementations.html#servers).
```shell
$ pip install uvicorn
```
## Example
```python title="main.py"
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(request):
return JSONResponse({'hello': 'world'})
routes = [
Route("/", endpoint=homepage)
]
app = Starlette(debug=True, routes=routes)
```
Then run the application using Uvicorn:
```shell
$ uvicorn main:app
```
## Dependencies
Starlette only requires `anyio`, and the following are optional:
* [`httpx2`][httpx2] - Required if you want to use the `TestClient`.
* [`jinja2`][jinja2] - Required if you want to use `Jinja2Templates`.
* [`opentelemetry-api`][opentelemetry-api] - Required for `OpenTelemetryMiddleware`.
* [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`.
* [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support.
* [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support.
You can install all of these with `pip install starlette[full]`.
## Framework or Toolkit
Starlette is designed to be used either as a complete framework, or as
an ASGI toolkit. You can use any of its components independently.
```python
from starlette.responses import PlainTextResponse
async def app(scope, receive, send):
assert scope['type'] == 'http'
response = PlainTextResponse('Hello, world!')
await response(scope, receive, send)
```
Run the `app` application in `example.py`:
```shell
$ uvicorn example:app
INFO: Started server process [11509]
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
Run uvicorn with `--reload` to enable auto-reloading on code changes.
## Modularity
The modularity that Starlette is designed on promotes building reusable
components that can be shared between any ASGI framework. This should enable
an ecosystem of shared middleware and mountable applications.
The clean API separation also means it's easier to understand each component
in isolation.
---
Starlette is BSD licensed code. Designed & crafted with care. — â�ï¸� —
[asgi]: https://asgi.readthedocs.io/en/latest/
[httpx2]: https://pypi.org/project/httpx2/
[jinja2]: https://jinja.palletsprojects.com/
[opentelemetry-api]: https://opentelemetry.io/docs/languages/python/api/
[python-multipart]: https://multipart.fastapiexpert.com/
[itsdangerous]: https://itsdangerous.palletsprojects.com/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
[techempower]: https://github.com/TechEmpower/FrameworkBenchmarks
starlette-1.7.0/benchmarks/ 0000775 0000000 0000000 00000000000 15254677127 0015663 5 ustar 00root root 0000000 0000000 starlette-1.7.0/benchmarks/README.md 0000664 0000000 0000000 00000015713 15254677127 0017151 0 ustar 00root root 0000000 0000000 # Benchmarks
## Minimal response dispatch
The response benchmark measures a two-byte response through progressively larger
parts of the public stack: raw ASGI, a prebuilt `Response`, a newly constructed
`Response`, a single `Route`, and a complete `Starlette` application. The raw
ASGI case is the control for event-loop and message-capture overhead.
Each dispatch receives a fresh ASGI scope. The application cases are warmed
before measurement so lazy middleware construction and interpreter specialization
are not attributed to every request.
Run it locally with:
```console
uv run pytest benchmarks/response_benchmark.py --codspeed
```
## Request bodies
The request benchmark compares `Request.body()` with `Request.stream()` for
1 KiB, 1 MiB, and 10 MiB payloads. The larger payloads are delivered both as a
single ASGI message and in 64 KiB chunks to separate byte volume from
per-message overhead.
Payload and chunk construction happen outside the measured region. Every
dispatch creates a fresh `Request`, ASGI scope, and receive callable. The
response reports the consumed byte count, which is validated after measurement.
Run it locally with:
```console
uv run pytest benchmarks/request_benchmark.py --codspeed
```
## JSON
The JSON benchmark measures `Request.json()` and `JSONResponse` with structured
payloads of approximately 1 KiB and 1 MiB. The large request is delivered both
as a single ASGI message and in 64 KiB chunks.
Payload encoding and chunk construction happen outside the measured region.
Request decoding and response encoding happen inside it, and each result is
validated after measurement.
Run it locally with:
```console
uv run pytest benchmarks/json_benchmark.py --codspeed
```
## URL-encoded forms
The URL-encoded benchmark measures `Request.form()` with 1,000 fields, a large
field, percent-encoded data, and field-count and part-size limit failures. The
large field is delivered as a single ASGI message and in 64 KiB chunks.
Run it locally with:
```console
uv run pytest benchmarks/urlencoded_benchmark.py --codspeed
```
## Streaming responses
The streaming benchmark measures `StreamingResponse` with 1 KiB and 1 MiB
bodies. The large body uses 1 KiB, 64 KiB, and single-chunk delivery to expose
the per-chunk dispatch cost.
Run it locally with:
```console
uv run pytest benchmarks/streaming_response_benchmark.py --codspeed
```
## File responses
The file benchmark measures `FileResponse` with fallback reads,
`http.response.pathsend`, and a single byte range. File creation happens outside
the measured region, while response construction, stat, and delivery happen
inside it.
Run it locally with:
```console
uv run pytest benchmarks/file_response_benchmark.py --codspeed
```
## WebSockets
The WebSocket benchmark measures complete text, bytes, and JSON echo exchanges
through the public `WebSocket` API. Each dispatch includes connect, accept,
receive, send, and close state transitions.
Run it locally with:
```console
uv run pytest benchmarks/websocket_benchmark.py --codspeed
```
## Multipart forms
The multipart benchmark measures `Request.form()` with field-heavy, in-memory
file, spooled file, mixed form, and boundary-like payloads. It also covers
malformed forms and field, file, and part-size limit failures.
File workloads use 64 KiB ASGI chunks. The 10 MiB file also has a single-message
control case. Multipart body and chunk construction happen outside the measured
region, while parsing and upload cleanup happen inside it.
Run it locally with:
```console
uv run pytest benchmarks/multipart_benchmark.py --codspeed
```
On Linux with glibc, you can use the same allocator setting as CI:
```console
GLIBC_TUNABLES=glibc.malloc.mmap_threshold=32768 PYTHONHASHSEED=0 uv run pytest benchmarks/multipart_benchmark.py --codspeed
```
glibc normally adjusts the
[`mmap` threshold](https://sourceware.org/glibc/manual/2.39/html_node/Memory-Allocation-Tunables.html)
based on earlier allocations. This can make an upload buffer grow in place
in one run and require a copy in another. CI fixes the threshold at 32 KiB,
below the 64 KiB upload chunks, to reduce this variation. Allocation, parsing,
and cleanup stay inside the measured region. Changing this setting shifts
the simulation baseline, so compare runs that use the same setting.
## Routing
The routing benchmark exercises `Router` dispatch through its ASGI interface
against a synthetic REST-style route table (120 routes as 30 resource groups
of four routes each, plus a 20-route variant for small applications).
The scenarios pin down the cases that scale differently with table size:
a static hit on an early route, a static and a parameterized hit on the last
routes, a full miss, and a wrong-method request (`405`). Each measured call
dispatches one request through the router, with a fresh ASGI scope built per
dispatch so CodSpeed warmup runs cannot pollute the measured one; response
status is validated on the benchmark's return value, outside the measured
region.
Run it locally with:
```console
uv run pytest benchmarks/routing_benchmark.py --codspeed
```
## GZip
The gzip benchmark exercises Starlette's `GZipMiddleware` through its ASGI
interface with deterministic payloads representing valid JSON, repetitive
text, and incompressible bytes.
It compares levels 1 through 9 at 1 MiB. It also compares representative levels
1, 6, and 9 at 32 KiB, 256 KiB, 5 MiB, and 10 MiB. This keeps the suite useful
for detecting payload-size regressions without running the full Cartesian
product of every level and large size.
Run it locally with:
```console
uv run pytest benchmarks/gzip_benchmark.py --codspeed
```
Payload construction, middleware configuration, and decompression validation
are outside the measured region. The measured call includes responder
construction, fresh mutable ASGI message containers, header handling,
compression, and resource cleanup. Recreating the message containers prevents
CodSpeed warmups from mutating the response used by the measured invocation.
Each case creates only one input payload, so the largest case does not leave all
benchmark inputs resident in memory. CodSpeed CI records simulated CPU
performance, peak heap usage, and allocation counts.
The end-to-end bypass benchmarks run the complete `GZipMiddleware` ASGI path
for responses below `minimum_size`, responses with an existing
`Content-Encoding`, `text/event-stream` responses, and
`http.response.pathsend`. Their response payloads are also constructed outside
the measured region, isolating middleware allocation overhead.
The responsiveness benchmark schedules a 10 MiB JSON response immediately
before a tiny response that bypasses compression. CodSpeed measures the work
needed for the tiny response to complete, then drains and validates the large
response outside the measured region. This provides a stable CPU-simulation
benchmark for detecting event-loop starvation without relying on wall-clock
timing or concurrent request storms. The existing `json-10MiB-level-9`
compression case separately measures the large response's total completion.
starlette-1.7.0/benchmarks/__init__.py 0000664 0000000 0000000 00000000074 15254677127 0017775 0 ustar 00root root 0000000 0000000 from __future__ import annotations
__all__: list[str] = []
starlette-1.7.0/benchmarks/_utils.py 0000664 0000000 0000000 00000003155 15254677127 0017540 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import asyncio
from starlette.types import ASGIApp, Message, Receive, Scope, Send
async def unreadable_receive() -> Message:
raise AssertionError("The benchmark app must not receive a request body")
class ChunkedReceive:
def __init__(self, chunks: tuple[bytes, ...]) -> None:
self.chunks = chunks
self.index = 0
async def __call__(self) -> Message:
if self.index >= len(self.chunks):
raise AssertionError("The benchmark app read beyond the request body")
chunk = self.chunks[self.index]
self.index += 1
return {
"type": "http.request",
"body": chunk,
"more_body": self.index < len(self.chunks),
}
async def send_result(send: Send, status: int, body: bytes) -> None:
await send({"type": "http.response.start", "status": status, "headers": []})
await send({"type": "http.response.body", "body": body})
async def run_asgi(app: ASGIApp, scope: Scope, receive: Receive = unreadable_receive) -> list[Message]:
messages: list[Message] = []
async def send(message: Message) -> None:
messages.append(message)
await app(scope, receive, send)
return messages
class ASGIRunner:
def __init__(self) -> None:
self.loop = asyncio.new_event_loop()
def run(self, app: ASGIApp, scope: Scope, receive: Receive = unreadable_receive) -> list[Message]:
return self.loop.run_until_complete(run_asgi(app, scope, receive))
def close(self) -> None:
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
self.loop.close()
starlette-1.7.0/benchmarks/conftest.py 0000664 0000000 0000000 00000000415 15254677127 0020062 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from collections.abc import Iterator
import pytest
from benchmarks._utils import ASGIRunner
@pytest.fixture(scope="module")
def asgi_runner() -> Iterator[ASGIRunner]:
runner = ASGIRunner()
yield runner
runner.close()
starlette-1.7.0/benchmarks/file_response_benchmark.py 0000664 0000000 0000000 00000007412 15254677127 0023110 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner
from starlette.datastructures import Headers
from starlette.responses import FileResponse
from starlette.types import Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
@dataclass(frozen=True)
class BenchmarkCase:
id: str
file_size: int
pathsend: bool = False
range_header: bytes | None = None
expected_size: int | None = None
CASES = (
BenchmarkCase("fallback-1KiB", KiB),
BenchmarkCase("fallback-10MiB", 10 * MiB),
BenchmarkCase("pathsend-10MiB", 10 * MiB, pathsend=True),
BenchmarkCase("range-64KiB", 10 * MiB, range_header=b"bytes=262144-327679", expected_size=64 * KiB),
)
class FileApp:
def __init__(self, path: Path) -> None:
self.path = path
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await FileResponse(self.path, media_type="application/octet-stream")(scope, receive, send)
def http_scope(case: BenchmarkCase) -> Scope:
headers = [] if case.range_header is None else [(b"range", case.range_header)]
extensions: dict[str, dict[str, object]] = {"http.response.pathsend": {}} if case.pathsend else {}
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": headers,
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
"extensions": extensions,
}
def dispatch(runner: ASGIRunner, app: FileApp, case: BenchmarkCase) -> list[Message]:
return runner.run(app, http_scope(case))
@pytest.fixture(scope="module", autouse=True)
def warm_file_response(asgi_runner: ASGIRunner, tmp_path_factory: pytest.TempPathFactory) -> None:
path = tmp_path_factory.mktemp("file-response-warm") / "file.bin"
path.write_bytes(b"x" * KiB)
app = FileApp(path)
cases = (
BenchmarkCase("warm-fallback", KiB),
BenchmarkCase("warm-pathsend", KiB, pathsend=True),
BenchmarkCase("warm-range", KiB, range_header=b"bytes=0-0", expected_size=1),
)
for case in cases:
for _ in range(100):
dispatch(asgi_runner, app, case)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_file_response(
tmp_path: Path,
asgi_runner: ASGIRunner,
benchmark: BenchmarkFixture,
case: BenchmarkCase,
) -> None:
path = tmp_path / "file.bin"
path.write_bytes(b"x" * case.file_size)
messages = benchmark.pedantic(dispatch, args=(asgi_runner, FileApp(path), case), rounds=1)
expected_status = 206 if case.range_header is not None else 200
assert messages[0]["type"] == "http.response.start"
assert messages[0]["status"] == expected_status
headers = Headers(raw=messages[0]["headers"])
expected_size = case.expected_size or case.file_size
assert headers["content-length"] == str(expected_size)
if case.pathsend:
assert messages[1:] == [{"type": "http.response.pathsend", "path": str(path)}]
else:
body_messages = messages[1:]
assert sum(len(message.get("body", b"")) for message in body_messages) == expected_size
assert body_messages[-1].get("more_body") is False
if case.range_header is not None:
assert headers["content-range"] == f"bytes 262144-327679/{case.file_size}"
benchmark.extra_info["file_bytes"] = case.file_size
benchmark.extra_info["response_bytes"] = expected_size
benchmark.extra_info["pathsend"] = case.pathsend
starlette-1.7.0/benchmarks/gzip_benchmark.py 0000664 0000000 0000000 00000030032 15254677127 0021216 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import asyncio
import gzip
import hashlib
import io
import json
from collections.abc import Iterator
from contextlib import ExitStack, closing, contextmanager
from dataclasses import dataclass
from typing import Literal
import anyio
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner, run_asgi
from starlette.middleware.gzip import GZipMiddleware
from starlette.types import ASGIApp, Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
PayloadKind = Literal["json", "text", "incompressible"]
BypassReason = Literal["below-minimum-size", "content-encoding", "event-stream", "pathsend"]
@dataclass(frozen=True)
class BenchmarkCase:
payload_kind: PayloadKind
size: int
level: int
@property
def id(self) -> str:
size = f"{self.size // MiB}MiB" if self.size >= MiB else f"{self.size // KiB}KiB"
return f"{self.payload_kind}-{size}-level-{self.level}"
@dataclass(frozen=True)
class BypassCase:
reason: BypassReason
body_size: int
class StaticResponseApp:
def __init__(self, messages: tuple[Message, ...]) -> None:
self.messages = messages
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
for message in self.messages:
outgoing_message = dict(message)
if "headers" in outgoing_message:
outgoing_message["headers"] = list(outgoing_message["headers"])
await send(outgoing_message)
@dataclass
class ResponsePair:
loop: asyncio.AbstractEventLoop
large_app: ASGIApp
tiny_app: ASGIApp
scope: Scope
payload: bytes
large_task: asyncio.Task[list[Message]] | None = None
tiny_messages: list[Message] | None = None
async def warm_worker(self) -> None:
await anyio.to_thread.run_sync(bool)
def run_until_tiny_response(self) -> None:
self.loop.run_until_complete(self._run_until_tiny_response())
async def _run_until_tiny_response(self) -> None:
self.large_task = asyncio.create_task(run_asgi(self.large_app, self.scope))
tiny_task = asyncio.create_task(run_asgi(self.tiny_app, self.scope))
self.tiny_messages = await tiny_task
def drain_and_validate(self) -> None:
self.loop.run_until_complete(self._drain_and_validate())
async def _drain_and_validate(self) -> None:
assert self.large_task is not None
large_messages = await self.large_task
assert self.tiny_messages is not None
assert len(large_messages) == 2
assert large_messages[0]["type"] == "http.response.start"
assert (b"content-encoding", b"gzip") in large_messages[0]["headers"]
assert large_messages[1]["type"] == "http.response.body"
compressed = large_messages[1]["body"]
assert isinstance(compressed, bytes)
assert gzip.decompress(compressed) == self.payload
assert len(self.tiny_messages) == 2
assert self.tiny_messages[0]["type"] == "http.response.start"
assert (b"content-encoding", b"gzip") not in self.tiny_messages[0]["headers"]
assert self.tiny_messages[1] == {"type": "http.response.body", "body": b"{}"}
@contextmanager
def response_pair(large_app: ASGIApp, tiny_app: ASGIApp, scope: Scope, payload: bytes) -> Iterator[ResponsePair]:
with closing(asyncio.new_event_loop()) as loop:
pair = ResponsePair(loop, large_app, tiny_app, scope, payload)
loop.run_until_complete(pair.warm_worker())
yield pair
pair.drain_and_validate()
class ResponsivenessBenchmark:
def __init__(self, large_app: ASGIApp, tiny_app: ASGIApp, scope: Scope, payload: bytes) -> None:
self.large_app = large_app
self.tiny_app = tiny_app
self.scope = scope
self.payload = payload
self._stack: ExitStack | None = None
self._pair: ResponsePair | None = None
def setup(self) -> None:
stack = ExitStack()
pair = stack.enter_context(response_pair(self.large_app, self.tiny_app, self.scope, self.payload))
self._stack = stack
self._pair = pair
def run_until_tiny_response(self) -> None:
assert self._pair is not None
self._pair.run_until_tiny_response()
def teardown(self) -> None:
assert self._stack is not None
self._stack.close()
self._stack = None
self._pair = None
# All compression levels are compared at 1 MiB. The size curve uses three
# representative levels so that the suite still reaches 10 MiB without making
# every CI run exercise the full large-payload Cartesian product.
PAYLOAD_KINDS: tuple[PayloadKind, ...] = ("json", "text", "incompressible")
CASES = tuple(
BenchmarkCase(payload_kind, MiB, level) for payload_kind in PAYLOAD_KINDS for level in range(1, 10)
) + tuple(
BenchmarkCase(payload_kind, size, level)
for payload_kind in PAYLOAD_KINDS
for size in (32 * KiB, 256 * KiB, 5 * MiB, 10 * MiB)
for level in (1, 6, 9)
)
def make_json_payload(size: int) -> bytes:
"""Build a deterministic, valid JSON document of exactly ``size`` bytes."""
prefix = b'{"requests":['
padding_prefix = b'],"padding":"'
suffix = b'"}'
output = io.BytesIO()
output.write(prefix)
index = 0
while True:
row = json.dumps(
{
"id": index,
"timestamp": f"2026-08-04T12:{index % 60:02d}:{index * 7 % 60:02d}.{index * 997 % 1000:03d}Z",
"method": ("GET", "POST", "PATCH", "DELETE")[index % 4],
"path": f"/api/v1/projects/{index % 1_009}/events/{index * 17 % 65_537}",
"status": (200, 201, 204, 400, 404, 409, 422, 500)[index % 8],
"duration_ms": round((index * 37 % 10_000) / 100, 2),
"request_id": f"{index * 0x9E3779B97F4A7C15 % (1 << 128):032x}",
"message": ("request completed", "validation failed", "resource updated")[index % 3],
},
separators=(",", ":"),
).encode()
separator = b"," if index else b""
required_tail = len(padding_prefix) + len(suffix)
if output.tell() + len(separator) + len(row) + required_tail > size:
break
output.write(separator)
output.write(row)
index += 1
output.write(padding_prefix)
output.write(b"x" * (size - output.tell() - len(suffix)))
output.write(suffix)
payload = output.getvalue()
assert len(payload) == size
return payload
def make_text_payload(size: int) -> bytes:
paragraph = (
b"Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python. "
b"It is production-ready and gives you the following: seriously impressive performance, WebSocket support, "
b"in-process background tasks, startup and shutdown events, and a test client built on HTTPX.\n"
)
return (paragraph * (size // len(paragraph) + 1))[:size]
def make_payload(kind: PayloadKind, size: int) -> bytes:
if kind == "json":
return make_json_payload(size)
if kind == "text":
return make_text_payload(size)
# SHAKE provides deterministic high-entropy bytes without keeping another
# 10 MiB random-data buffer alive alongside the returned payload.
return hashlib.shake_256(b"starlette-gzip-benchmark-v1").digest(size)
def make_bypass_messages(case: BypassCase) -> tuple[Message, ...]:
headers = [(b"content-type", b"application/json"), (b"content-length", str(case.body_size).encode())]
if case.reason == "content-encoding":
headers.append((b"content-encoding", b"br"))
elif case.reason == "event-stream":
headers[0] = (b"content-type", b"text/event-stream")
response_start: Message = {"type": "http.response.start", "status": 200, "headers": headers}
if case.reason == "pathsend":
response_body: Message = {"type": "http.response.pathsend", "path": "/tmp/starlette-benchmark"}
else:
response_body = {"type": "http.response.body", "body": b"x" * case.body_size}
return response_start, response_body
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=10)
def test_gzip(benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
# Payload construction is intentionally outside the measured region. Cases
# are function-scoped, so only one source payload is resident at a time.
payload = make_payload(case.payload_kind, case.size)
messages: tuple[Message, ...] = (
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"application/json"), (b"content-length", str(len(payload)).encode())],
},
{"type": "http.response.body", "body": payload},
)
app = GZipMiddleware(StaticResponseApp(messages), minimum_size=0, compresslevel=case.level)
scope: Scope = {"type": "http", "headers": [(b"accept-encoding", b"gzip")]}
with closing(ASGIRunner()) as runner:
sent = benchmark.pedantic(runner.run, args=(app, scope), rounds=1)
assert len(sent) == 2
assert sent[0]["type"] == "http.response.start"
assert (b"content-encoding", b"gzip") in sent[0]["headers"]
assert sent[1]["type"] == "http.response.body"
compressed = sent[1]["body"]
assert isinstance(compressed, bytes)
assert gzip.decompress(compressed) == payload
benchmark.extra_info["input_bytes"] = len(payload)
benchmark.extra_info["output_bytes"] = len(compressed)
benchmark.extra_info["compression_ratio"] = len(compressed) / len(payload)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_gzip_event_loop_responsiveness(benchmark: BenchmarkFixture) -> None:
payload = make_json_payload(10 * MiB)
large_messages: tuple[Message, ...] = (
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"application/json"), (b"content-length", str(len(payload)).encode())],
},
{"type": "http.response.body", "body": payload},
)
tiny_messages: tuple[Message, ...] = (
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"application/json"), (b"content-length", b"2")],
},
{"type": "http.response.body", "body": b"{}"},
)
large_app = GZipMiddleware(StaticResponseApp(large_messages), compresslevel=9)
tiny_app = GZipMiddleware(StaticResponseApp(tiny_messages), compresslevel=9)
scope: Scope = {"type": "http", "headers": [(b"accept-encoding", b"gzip")]}
responsiveness = ResponsivenessBenchmark(large_app, tiny_app, scope, payload)
benchmark.pedantic(
responsiveness.run_until_tiny_response,
setup=responsiveness.setup,
teardown=responsiveness.teardown,
rounds=1,
)
benchmark.extra_info["large_response_bytes"] = len(payload)
benchmark.extra_info["compression_level"] = 9
benchmark.extra_info["tiny_response_bytes"] = len(b"{}")
@pytest.mark.parametrize(
"case",
(
BypassCase("below-minimum-size", 499),
BypassCase("content-encoding", MiB),
BypassCase("event-stream", MiB),
BypassCase("pathsend", MiB),
),
ids=lambda case: case.reason,
)
@pytest.mark.benchmark(max_time=0.5, max_rounds=10)
def test_gzip_bypass(benchmark: BenchmarkFixture, case: BypassCase) -> None:
# The response payload is constructed outside the measured region. The
# benchmark covers the complete GZipMiddleware ASGI call, including fresh
# ASGI message containers, responder construction, and teardown.
expected = make_bypass_messages(case)
app = GZipMiddleware(StaticResponseApp(expected), minimum_size=500)
scope: Scope = {"type": "http", "headers": [(b"accept-encoding", b"gzip")]}
if case.reason == "pathsend":
scope["extensions"] = {"http.response.pathsend": {}}
with closing(ASGIRunner()) as runner:
sent = benchmark.pedantic(runner.run, args=(app, scope), rounds=1)
assert sent == list(expected)
benchmark.extra_info["response_bytes"] = case.body_size
benchmark.extra_info["bypass_reason"] = case.reason
starlette-1.7.0/benchmarks/json_benchmark.py 0000664 0000000 0000000 00000011775 15254677127 0021233 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import json
from dataclasses import dataclass
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner, ChunkedReceive
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.types import ASGIApp, Message, Receive, Scope, Send
KiB = 1024
@dataclass(frozen=True)
class JSONCase:
name: str
items: int
@dataclass(frozen=True)
class RequestCase:
payload: JSONCase
chunk_size: int | None
@property
def id(self) -> str:
chunk_size = "single" if self.chunk_size is None else f"{self.chunk_size // KiB}KiB"
return f"{self.payload.name}-{chunk_size}"
def make_content(case: JSONCase) -> list[dict[str, object]]:
return [{"id": index, "name": f"item-{index}", "enabled": index % 2 == 0} for index in range(case.items)]
def render_json(content: list[dict[str, object]]) -> bytes:
return json.dumps(content, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode()
SMALL_PAYLOAD = JSONCase("1KiB", 25)
LARGE_PAYLOAD = JSONCase("1MiB", 22_000)
PAYLOADS = (SMALL_PAYLOAD, LARGE_PAYLOAD)
REQUEST_CASES = (
RequestCase(SMALL_PAYLOAD, None),
RequestCase(LARGE_PAYLOAD, None),
RequestCase(LARGE_PAYLOAD, 64 * KiB),
)
class RequestJSONApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
content = await Request(scope, receive).json()
await send_result(send, str(len(content)).encode())
class JSONResponseApp:
def __init__(self, content: list[dict[str, object]]) -> None:
self.content = content
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await JSONResponse(self.content)(scope, receive, send)
async def send_result(send: Send, body: bytes) -> None:
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": body})
REQUEST_APP = RequestJSONApp()
def http_scope(method: str, headers: list[tuple[bytes, bytes]]) -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": headers,
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def make_chunks(body: bytes, chunk_size: int | None) -> tuple[bytes, ...]:
if chunk_size is None:
return (body,)
return tuple(body[offset : offset + chunk_size] for offset in range(0, len(body), chunk_size))
def dispatch_request(runner: ASGIRunner, chunks: tuple[bytes, ...]) -> list[Message]:
scope = http_scope("POST", [(b"content-type", b"application/json")])
return runner.run(REQUEST_APP, scope, ChunkedReceive(chunks))
def dispatch_response(runner: ASGIRunner, app: ASGIApp) -> list[Message]:
return runner.run(app, http_scope("GET", []))
@pytest.fixture(scope="module", autouse=True)
def warm_apps(asgi_runner: ASGIRunner) -> None:
content = make_content(SMALL_PAYLOAD)
chunks = (render_json(content),)
app = JSONResponseApp(content)
for _ in range(100):
dispatch_request(asgi_runner, chunks)
dispatch_response(asgi_runner, app)
@pytest.mark.parametrize("case", REQUEST_CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_request_json(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: RequestCase) -> None:
body = render_json(make_content(case.payload))
chunks = make_chunks(body, case.chunk_size)
messages = benchmark.pedantic(dispatch_request, args=(asgi_runner, chunks), rounds=1)
assert messages == [
{"type": "http.response.start", "status": 200, "headers": []},
{"type": "http.response.body", "body": str(case.payload.items).encode()},
]
benchmark.extra_info["body_bytes"] = len(body)
benchmark.extra_info["chunk_bytes"] = case.chunk_size or len(body)
benchmark.extra_info["chunks"] = len(chunks)
benchmark.extra_info["items"] = case.payload.items
@pytest.mark.parametrize("payload", PAYLOADS, ids=lambda payload: payload.name)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_json_response(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, payload: JSONCase) -> None:
content = make_content(payload)
body = render_json(content)
messages = benchmark.pedantic(
dispatch_response,
args=(asgi_runner, JSONResponseApp(content)),
rounds=1,
)
assert messages == [
{
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-length", str(len(body)).encode()),
(b"content-type", b"application/json"),
],
},
{"type": "http.response.body", "body": body},
]
benchmark.extra_info["body_bytes"] = len(body)
benchmark.extra_info["items"] = payload.items
starlette-1.7.0/benchmarks/multipart_benchmark.py 0000664 0000000 0000000 00000016101 15254677127 0022267 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner, ChunkedReceive, send_result
from starlette.datastructures import UploadFile
from starlette.formparsers import MultiPartException
from starlette.requests import Request
from starlette.types import ASGIApp, Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
BOUNDARY = b"starlette-benchmark-boundary"
CONTENT_TYPE = b"multipart/form-data; boundary=" + BOUNDARY
@dataclass(frozen=True)
class BenchmarkCase:
id: str
fields: int = 0
field_size: int = 8
file_sizes: tuple[int, ...] = ()
chunk_size: int | None = 64 * KiB
boundary_like: bool = False
malformed: bool = False
max_files: int = 1000
max_fields: int = 1000
max_part_size: int = MiB
expected_error: str | None = None
warmup: bool = False
CASES = (
BenchmarkCase("fields-1000", fields=1000),
BenchmarkCase("file-512KiB", file_sizes=(512 * KiB,), warmup=True),
BenchmarkCase("file-10MiB-single", file_sizes=(10 * MiB,), chunk_size=None),
BenchmarkCase("file-10MiB-64KiB", file_sizes=(10 * MiB,)),
BenchmarkCase("mixed", fields=100, file_sizes=(512 * KiB,), warmup=True),
BenchmarkCase("boundary-like-file", file_sizes=(MiB,), boundary_like=True, warmup=True),
BenchmarkCase(
"malformed-after-spooled-file",
file_sizes=(2 * MiB,),
malformed=True,
expected_error='The Content-Disposition header field "name" must be provided.',
),
BenchmarkCase(
"max-fields",
fields=101,
max_fields=100,
expected_error="Too many fields. Maximum number of fields is 100.",
),
BenchmarkCase(
"max-files",
file_sizes=(1, 1),
max_files=1,
expected_error="Too many files. Maximum number of files is 1.",
),
BenchmarkCase(
"max-part-size",
fields=1,
field_size=2 * KiB,
max_part_size=KiB,
expected_error="Part exceeded maximum size of 1KB.",
),
)
class MultipartApp:
def __init__(self, case: BenchmarkCase) -> None:
self.case = case
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive)
try:
async with request.form(
max_files=self.case.max_files,
max_fields=self.case.max_fields,
max_part_size=self.case.max_part_size,
) as form:
fields = 0
files = 0
file_bytes = 0
for _, value in form.multi_items():
if isinstance(value, UploadFile):
files += 1
assert value.size is not None
file_bytes += value.size
else:
fields += 1
except MultiPartException as exc:
await send_result(send, 400, exc.message.encode())
return
await send_result(send, 200, f"{fields}:{files}:{file_bytes}".encode())
def http_scope() -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "POST",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [(b"content-type", CONTENT_TYPE)],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def field_part(index: int, size: int) -> bytes:
value = bytes((65 + index % 26,)) * size
return (
b"--"
+ BOUNDARY
+ b'\r\nContent-Disposition: form-data; name="field'
+ str(index).encode()
+ b'"\r\n\r\n'
+ value
+ b"\r\n"
)
def file_part(index: int, size: int, boundary_like: bool) -> bytes:
if boundary_like:
marker = b"\r\n--" + BOUNDARY[:-1] + b"x"
content = (marker * (size // len(marker) + 1))[:size]
else:
content = bytes((65 + index % 26,)) * size
return (
b"--"
+ BOUNDARY
+ b'\r\nContent-Disposition: form-data; name="file'
+ str(index).encode()
+ b'"; filename="file.bin"\r\nContent-Type: application/octet-stream\r\n\r\n'
+ content
+ b"\r\n"
)
def make_body(case: BenchmarkCase) -> bytes:
parts = [field_part(index, case.field_size) for index in range(case.fields)]
parts.extend(file_part(index, size, case.boundary_like) for index, size in enumerate(case.file_sizes))
if case.malformed:
parts.append(b"--" + BOUNDARY + b"\r\nContent-Disposition: form-data\r\n\r\ninvalid\r\n")
parts.append(b"--" + BOUNDARY + b"--\r\n")
return b"".join(parts)
def make_chunks(body: bytes, chunk_size: int | None) -> tuple[bytes, ...]:
if chunk_size is None:
return (body,)
return tuple(body[offset : offset + chunk_size] for offset in range(0, len(body), chunk_size))
def dispatch(runner: ASGIRunner, app: ASGIApp, chunks: tuple[bytes, ...]) -> list[Message]:
return runner.run(app, http_scope(), ChunkedReceive(chunks))
@pytest.fixture(scope="module", autouse=True)
def warm_multipart(asgi_runner: ASGIRunner) -> None:
case = BenchmarkCase("warm", fields=1, file_sizes=(1,))
chunks = make_chunks(make_body(case), case.chunk_size)
app = MultipartApp(case)
for _ in range(100):
dispatch(asgi_runner, app, chunks)
spooled_case = BenchmarkCase("warm-spooled", file_sizes=(MiB + 1,))
chunks = make_chunks(make_body(spooled_case), spooled_case.chunk_size)
dispatch(asgi_runner, MultipartApp(spooled_case), chunks)
boundary_case = BenchmarkCase("warm-boundary-like", file_sizes=(64 * KiB,), boundary_like=True)
chunks = make_chunks(make_body(boundary_case), boundary_case.chunk_size)
app = MultipartApp(boundary_case)
for _ in range(10):
dispatch(asgi_runner, app, chunks)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_multipart(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
body = make_body(case)
chunks = make_chunks(body, case.chunk_size)
app = MultipartApp(case)
if case.warmup:
dispatch(asgi_runner, app, chunks)
messages = benchmark.pedantic(dispatch, args=(asgi_runner, app, chunks), rounds=1)
if case.expected_error is None:
expected_status = 200
expected_body = f"{case.fields}:{len(case.file_sizes)}:{sum(case.file_sizes)}".encode()
else:
expected_status = 400
expected_body = case.expected_error.encode()
assert messages == [
{"type": "http.response.start", "status": expected_status, "headers": []},
{"type": "http.response.body", "body": expected_body},
]
benchmark.extra_info["body_bytes"] = len(body)
benchmark.extra_info["chunk_bytes"] = case.chunk_size or len(body)
benchmark.extra_info["chunks"] = len(chunks)
benchmark.extra_info["fields"] = case.fields
benchmark.extra_info["files"] = len(case.file_sizes)
starlette-1.7.0/benchmarks/request_benchmark.py 0000664 0000000 0000000 00000007147 15254677127 0021750 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner, ChunkedReceive
from starlette.requests import Request
from starlette.types import ASGIApp, Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
ReadMode = Literal["body", "stream"]
class BodyApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
body = await Request(scope, receive).body()
await send_length(send, len(body))
class StreamApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
length = 0
async for chunk in Request(scope, receive).stream():
length += len(chunk)
await send_length(send, length)
async def send_length(send: Send, length: int) -> None:
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": str(length).encode()})
@dataclass(frozen=True)
class BenchmarkCase:
mode: ReadMode
body_size: int
chunk_size: int
@property
def id(self) -> str:
body_size = format_size(self.body_size)
chunk_size = "single" if self.chunk_size == self.body_size else format_size(self.chunk_size)
return f"{self.mode}-{body_size}-{chunk_size}"
READ_MODES: tuple[ReadMode, ...] = ("body", "stream")
BODY_CASES = (
(KiB, KiB),
(MiB, MiB),
(MiB, 64 * KiB),
(10 * MiB, 10 * MiB),
(10 * MiB, 64 * KiB),
)
CASES = tuple(BenchmarkCase(mode, body_size, chunk_size) for mode in READ_MODES for body_size, chunk_size in BODY_CASES)
APPS: dict[ReadMode, ASGIApp] = {"body": BodyApp(), "stream": StreamApp()}
def format_size(size: int) -> str:
if size >= MiB:
return f"{size // MiB}MiB"
return f"{size // KiB}KiB"
def http_scope() -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "POST",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def make_chunks(body_size: int, chunk_size: int) -> tuple[bytes, ...]:
payload = b"x" * body_size
return tuple(payload[offset : offset + chunk_size] for offset in range(0, body_size, chunk_size))
def dispatch(runner: ASGIRunner, app: ASGIApp, chunks: tuple[bytes, ...]) -> list[Message]:
return runner.run(app, http_scope(), ChunkedReceive(chunks))
@pytest.fixture(scope="module", autouse=True)
def warm_apps(asgi_runner: ASGIRunner) -> None:
chunks = (b"x" * KiB,)
for app in APPS.values():
for _ in range(100):
dispatch(asgi_runner, app, chunks)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_request_body(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
chunks = make_chunks(case.body_size, case.chunk_size)
messages = benchmark.pedantic(dispatch, args=(asgi_runner, APPS[case.mode], chunks), rounds=1)
assert messages == [
{"type": "http.response.start", "status": 200, "headers": []},
{"type": "http.response.body", "body": str(case.body_size).encode()},
]
benchmark.extra_info["read_mode"] = case.mode
benchmark.extra_info["body_bytes"] = case.body_size
benchmark.extra_info["chunk_bytes"] = case.chunk_size
benchmark.extra_info["chunks"] = len(chunks)
starlette-1.7.0/benchmarks/response_benchmark.py 0000664 0000000 0000000 00000005107 15254677127 0022110 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route
from starlette.types import ASGIApp, Message, Receive, Scope, Send
PAYLOAD = b"ok"
EXPECTED_HEADERS = [(b"content-length", b"2")]
async def endpoint(request: Request) -> Response:
return Response(PAYLOAD)
class RawResponseApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await send({"type": "http.response.start", "status": 200, "headers": EXPECTED_HEADERS})
await send({"type": "http.response.body", "body": PAYLOAD})
class ConstructedResponseApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await Response(PAYLOAD)(scope, receive, send)
@dataclass(frozen=True)
class BenchmarkCase:
name: str
app: ASGIApp
CASES = (
BenchmarkCase("raw-asgi", RawResponseApp()),
BenchmarkCase("prebuilt-response", Response(PAYLOAD)),
BenchmarkCase("constructed-response", ConstructedResponseApp()),
BenchmarkCase("route", Route("/", endpoint)),
BenchmarkCase("starlette", Starlette(routes=[Route("/", endpoint)])),
)
def http_scope() -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def dispatch(runner: ASGIRunner, app: ASGIApp) -> list[Message]:
return runner.run(app, http_scope())
@pytest.fixture(scope="module", autouse=True)
def warm_apps(asgi_runner: ASGIRunner) -> None:
for case in CASES:
for _ in range(100):
dispatch(asgi_runner, case.app)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.name)
@pytest.mark.benchmark(max_time=0.5, max_rounds=20)
def test_minimal_response_dispatch(
asgi_runner: ASGIRunner,
benchmark: BenchmarkFixture,
case: BenchmarkCase,
) -> None:
messages = benchmark(dispatch, asgi_runner, case.app)
assert messages == [
{"type": "http.response.start", "status": 200, "headers": EXPECTED_HEADERS},
{"type": "http.response.body", "body": PAYLOAD},
]
benchmark.extra_info["stack"] = case.name
starlette-1.7.0/benchmarks/routing_benchmark.py 0000664 0000000 0000000 00000005456 15254677127 0021750 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Router
from starlette.types import ASGIApp, Message, Scope
async def endpoint(request: Request) -> PlainTextResponse:
return PlainTextResponse("ok")
def build_router(groups: int) -> Router:
routes: list[Route] = []
for i in range(groups):
routes.extend(
[
Route(f"/resources{i}", endpoint, methods=["GET", "POST"]),
Route(f"/resources{i}/{{id:int}}", endpoint, methods=["GET", "PUT", "DELETE"]),
Route(f"/resources{i}/{{id:int}}/items", endpoint, methods=["GET", "POST"]),
Route(f"/resources{i}/{{id:int}}/items/{{item}}", endpoint, methods=["GET"]),
]
)
return Router(routes=routes)
LARGE_ROUTER = build_router(groups=30) # 120 routes
SMALL_ROUTER = build_router(groups=5) # 20 routes
def http_scope(method: str, path: str) -> Scope:
# Built per dispatch: the router mutates the scope while matching, so a
# fresh one keeps CodSpeed warmup runs from polluting the measured run.
return {"type": "http", "method": method, "path": path, "root_path": "", "headers": [], "query_string": b""}
def dispatch(runner: ASGIRunner, app: ASGIApp, method: str, path: str) -> list[Message]:
return runner.run(app, http_scope(method, path))
def test_routing_static_early(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, LARGE_ROUTER, "GET", "/resources0")
assert messages[0]["status"] == 200
def test_routing_static_late(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, LARGE_ROUTER, "GET", "/resources29")
assert messages[0]["status"] == 200
def test_routing_param_late(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, LARGE_ROUTER, "GET", "/resources29/123/items/first")
assert messages[0]["status"] == 200
def test_routing_miss(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, LARGE_ROUTER, "GET", "/no/such/path")
assert messages[0]["status"] == 404
def test_routing_method_not_allowed(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, LARGE_ROUTER, "DELETE", "/resources29")
assert messages[0]["status"] == 405
def test_routing_small_app(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(dispatch, asgi_runner, SMALL_ROUTER, "GET", "/resources4/7")
assert messages[0]["status"] == 200
starlette-1.7.0/benchmarks/streaming_response_benchmark.py 0000664 0000000 0000000 00000005543 15254677127 0024165 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from collections.abc import AsyncIterator
from dataclasses import dataclass
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner
from starlette.responses import StreamingResponse
from starlette.types import ASGIApp, Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
@dataclass(frozen=True)
class BenchmarkCase:
id: str
body_size: int
chunk_size: int
CASES = (
BenchmarkCase("1KiB-single", KiB, KiB),
BenchmarkCase("1MiB-1KiB", MiB, KiB),
BenchmarkCase("1MiB-64KiB", MiB, 64 * KiB),
BenchmarkCase("1MiB-single", MiB, MiB),
)
class StreamingApp:
def __init__(self, chunks: tuple[bytes, ...]) -> None:
self.chunks = chunks
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await StreamingResponse(iter_chunks(self.chunks))(scope, receive, send)
async def iter_chunks(chunks: tuple[bytes, ...]) -> AsyncIterator[bytes]:
for chunk in chunks:
yield chunk
def make_chunks(body_size: int, chunk_size: int) -> tuple[bytes, ...]:
payload = b"x" * body_size
return tuple(payload[offset : offset + chunk_size] for offset in range(0, body_size, chunk_size))
def http_scope() -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def dispatch(runner: ASGIRunner, app: ASGIApp) -> list[Message]:
return runner.run(app, http_scope())
@pytest.fixture(scope="module", autouse=True)
def warm_streaming(asgi_runner: ASGIRunner) -> None:
app = StreamingApp((b"x",))
for _ in range(100):
dispatch(asgi_runner, app)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_streaming_response(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
chunks = make_chunks(case.body_size, case.chunk_size)
messages = benchmark.pedantic(dispatch, args=(asgi_runner, StreamingApp(chunks)), rounds=1)
assert messages[0] == {"type": "http.response.start", "status": 200, "headers": []}
assert messages[-1] == {"type": "http.response.body", "body": b"", "more_body": False}
assert len(messages) == len(chunks) + 2
assert sum(len(message.get("body", b"")) for message in messages[1:]) == case.body_size
assert all(message.get("more_body") is True for message in messages[1:-1])
benchmark.extra_info["body_bytes"] = case.body_size
benchmark.extra_info["chunk_bytes"] = case.chunk_size
benchmark.extra_info["chunks"] = len(chunks)
starlette-1.7.0/benchmarks/urlencoded_benchmark.py 0000664 0000000 0000000 00000011351 15254677127 0022374 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass
from urllib.parse import urlencode
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner, ChunkedReceive, send_result
from starlette.formparsers import MultiPartException
from starlette.requests import Request
from starlette.types import Message, Receive, Scope, Send
KiB = 1024
MiB = 1024 * KiB
@dataclass(frozen=True)
class BenchmarkCase:
id: str
fields: int
field_size: int
chunk_size: int | None = 64 * KiB
percent_encoded: bool = False
max_fields: int = 1000
max_part_size: int = MiB
expected_error: str | None = None
warmup: bool = False
CASES = (
BenchmarkCase("fields-1000", fields=1000, field_size=8),
BenchmarkCase("field-1MiB-single", fields=1, field_size=MiB, chunk_size=None, max_part_size=2 * MiB),
BenchmarkCase("field-1MiB-64KiB", fields=1, field_size=MiB, max_part_size=2 * MiB, warmup=True),
BenchmarkCase("percent-encoded", fields=100, field_size=8, percent_encoded=True),
BenchmarkCase(
"max-fields",
fields=101,
field_size=8,
max_fields=100,
expected_error="Too many fields. Maximum number of fields is 100.",
),
BenchmarkCase(
"max-part-size",
fields=1,
field_size=2 * KiB,
max_part_size=KiB,
expected_error="Field exceeded maximum size of 1KB.",
),
)
class FormApp:
def __init__(self, case: BenchmarkCase) -> None:
self.case = case
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive)
try:
async with request.form(
max_fields=self.case.max_fields,
max_part_size=self.case.max_part_size,
) as form:
value_size = 0
for value in form.values():
assert isinstance(value, str)
value_size += len(value)
result = f"{len(form)}:{value_size}".encode()
except MultiPartException as exc:
await send_result(send, 400, exc.message.encode())
return
await send_result(send, 200, result)
def make_fields(case: BenchmarkCase) -> list[tuple[str, str]]:
if case.percent_encoded:
return [(f"field {index}", f"café / {index}" * case.field_size) for index in range(case.fields)]
return [(f"field{index}", "x" * case.field_size) for index in range(case.fields)]
def make_chunks(body: bytes, chunk_size: int | None) -> tuple[bytes, ...]:
if chunk_size is None:
return (body,)
return tuple(body[offset : offset + chunk_size] for offset in range(0, len(body), chunk_size))
def http_scope() -> Scope:
return {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"http_version": "1.1",
"method": "POST",
"scheme": "http",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [(b"content-type", b"application/x-www-form-urlencoded")],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
}
def dispatch(runner: ASGIRunner, app: FormApp, chunks: tuple[bytes, ...]) -> list[Message]:
return runner.run(app, http_scope(), ChunkedReceive(chunks))
@pytest.fixture(scope="module", autouse=True)
def warm_form(asgi_runner: ASGIRunner) -> None:
case = BenchmarkCase("warm", fields=1, field_size=1)
body = urlencode(make_fields(case)).encode()
for _ in range(100):
dispatch(asgi_runner, FormApp(case), (body,))
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=1)
def test_urlencoded_form(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
fields = make_fields(case)
body = urlencode(fields).encode()
chunks = make_chunks(body, case.chunk_size)
app = FormApp(case)
if case.warmup:
dispatch(asgi_runner, app, chunks)
messages = benchmark.pedantic(dispatch, args=(asgi_runner, app, chunks), rounds=1)
if case.expected_error is None:
expected_status = 200
expected_body = f"{case.fields}:{sum(len(value) for _, value in fields)}".encode()
else:
expected_status = 400
expected_body = case.expected_error.encode()
assert messages == [
{"type": "http.response.start", "status": expected_status, "headers": []},
{"type": "http.response.body", "body": expected_body},
]
benchmark.extra_info["body_bytes"] = len(body)
benchmark.extra_info["chunk_bytes"] = case.chunk_size or len(body)
benchmark.extra_info["chunks"] = len(chunks)
benchmark.extra_info["fields"] = case.fields
starlette-1.7.0/benchmarks/websocket_benchmark.py 0000664 0000000 0000000 00000007206 15254677127 0022242 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import json
from dataclasses import dataclass
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
from benchmarks._utils import ASGIRunner
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from starlette.websockets import WebSocket
TEXT = "x" * 1024
BYTES = b"x" * 1024
JSON_CONTENT = {"items": [{"id": index, "name": f"item-{index}"} for index in range(25)]}
JSON_TEXT = json.dumps(JSON_CONTENT, ensure_ascii=False, separators=(",", ":"))
class TextEchoApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive, send)
await websocket.accept()
await websocket.send_text(await websocket.receive_text())
await websocket.close()
class BytesEchoApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive, send)
await websocket.accept()
await websocket.send_bytes(await websocket.receive_bytes())
await websocket.close()
class JSONEchoApp:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive, send)
await websocket.accept()
await websocket.send_json(await websocket.receive_json())
await websocket.close()
@dataclass(frozen=True)
class BenchmarkCase:
id: str
app: ASGIApp
incoming: Message
outgoing: Message
CASES = (
BenchmarkCase(
"text", TextEchoApp(), {"type": "websocket.receive", "text": TEXT}, {"type": "websocket.send", "text": TEXT}
),
BenchmarkCase(
"bytes",
BytesEchoApp(),
{"type": "websocket.receive", "bytes": BYTES},
{"type": "websocket.send", "bytes": BYTES},
),
BenchmarkCase(
"json",
JSONEchoApp(),
{"type": "websocket.receive", "text": JSON_TEXT},
{"type": "websocket.send", "text": JSON_TEXT},
),
)
class WebSocketReceive:
def __init__(self, incoming: Message) -> None:
self.messages: tuple[Message, ...] = ({"type": "websocket.connect"}, incoming)
self.index = 0
async def __call__(self) -> Message:
if self.index >= len(self.messages):
raise AssertionError("The benchmark app read beyond the WebSocket message")
message = self.messages[self.index]
self.index += 1
return message
def websocket_scope() -> Scope:
return {
"type": "websocket",
"asgi": {"version": "3.0", "spec_version": "2.5"},
"scheme": "ws",
"path": "/",
"raw_path": b"/",
"root_path": "",
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 50000),
"server": ("127.0.0.1", 80),
"subprotocols": [],
}
def dispatch(runner: ASGIRunner, case: BenchmarkCase) -> list[Message]:
return runner.run(case.app, websocket_scope(), WebSocketReceive(case.incoming))
@pytest.fixture(scope="module", autouse=True)
def warm_websockets(asgi_runner: ASGIRunner) -> None:
for case in CASES:
for _ in range(100):
dispatch(asgi_runner, case)
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id)
@pytest.mark.benchmark(max_time=0.5, max_rounds=20)
def test_websocket_echo(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
messages = benchmark(dispatch, asgi_runner, case)
assert messages == [
{"type": "websocket.accept", "subprotocol": None, "headers": []},
case.outgoing,
{"type": "websocket.close", "code": 1000, "reason": ""},
]
benchmark.extra_info["mode"] = case.id
starlette-1.7.0/docs/ 0000775 0000000 0000000 00000000000 15254677127 0014476 5 ustar 00root root 0000000 0000000 starlette-1.7.0/docs/CNAME 0000664 0000000 0000000 00000000021 15254677127 0015235 0 ustar 00root root 0000000 0000000 www.starlette.io
starlette-1.7.0/docs/applications.md 0000664 0000000 0000000 00000003372 15254677127 0017513 0 ustar 00root root 0000000 0000000
??? abstract "API Reference"
::: starlette.applications.Starlette
options:
parameter_headings: false
show_root_heading: true
heading_level: 3
filters:
- "__init__"
Starlette includes an application class `Starlette` that nicely ties together all of
its other functionality.
```python
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Mount, WebSocketRoute
from starlette.staticfiles import StaticFiles
def homepage(request):
return PlainTextResponse('Hello, world!')
def user_me(request):
username = "John Doe"
return PlainTextResponse('Hello, %s!' % username)
def user(request):
username = request.path_params['username']
return PlainTextResponse('Hello, %s!' % username)
async def websocket_endpoint(websocket):
await websocket.accept()
await websocket.send_text('Hello, websocket!')
await websocket.close()
@asynccontextmanager
async def lifespan(app):
print('Startup')
yield
print('Shutdown')
routes = [
Route('/', homepage),
Route('/user/me', user_me),
Route('/user/{username}', user),
WebSocketRoute('/ws', websocket_endpoint),
Mount('/static', StaticFiles(directory="static")),
]
app = Starlette(debug=True, routes=routes, lifespan=lifespan)
```
### Storing state on the app instance
You can store arbitrary extra state on the application instance, using the
generic `app.state` attribute.
For example:
```python
app.state.ADMIN_EMAIL = 'admin@example.org'
```
### Accessing the app instance
Where a `request` is available (i.e. endpoints and middleware), the app is available on `request.app`.
starlette-1.7.0/docs/authentication.md 0000664 0000000 0000000 00000012554 15254677127 0020046 0 ustar 00root root 0000000 0000000 Starlette offers a simple but powerful interface for handling authentication
and permissions. Once you've installed `AuthenticationMiddleware` with an
appropriate authentication backend the `request.user` and `request.auth`
interfaces will be available in your endpoints.
```python
from starlette.applications import Starlette
from starlette.authentication import (
AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
)
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.responses import PlainTextResponse
from starlette.routing import Route
import base64
import binascii
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, conn):
if "Authorization" not in conn.headers:
return
auth = conn.headers["Authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != 'basic':
return
decoded = base64.b64decode(credentials).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError('Invalid basic auth credentials')
username, _, password = decoded.partition(":")
# TODO: You'd want to verify the username and password here.
return AuthCredentials(["authenticated"]), SimpleUser(username)
async def homepage(request):
if request.user.is_authenticated:
return PlainTextResponse('Hello, ' + request.user.display_name)
return PlainTextResponse('Hello, you')
routes = [
Route("/", endpoint=homepage)
]
middleware = [
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
]
app = Starlette(routes=routes, middleware=middleware)
```
## Users
Once `AuthenticationMiddleware` is installed the `request.user` interface
will be available to endpoints or other middleware.
This interface should subclass `BaseUser`, which provides two properties,
as well as whatever other information your user model includes.
* `.is_authenticated`
* `.display_name`
Starlette provides two built-in user implementations: `UnauthenticatedUser()`,
and `SimpleUser(username)`.
## AuthCredentials
It is important that authentication credentials are treated as a separate concept
from users. An authentication scheme should be able to restrict or grant
particular privileges independently of the user identity.
The `AuthCredentials` class provides the basic interface that `request.auth`
exposes:
* `.scopes`
## Permissions
Permissions are implemented as an endpoint decorator, that enforces that the
incoming request includes the required authentication scopes.
```python
from starlette.authentication import requires
@requires('authenticated')
async def dashboard(request):
...
```
You can include either one or multiple required scopes:
```python
from starlette.authentication import requires
@requires(['authenticated', 'admin'])
async def dashboard(request):
...
```
By default 403 responses will be returned when permissions are not granted.
In some cases you might want to customize this, for example to hide information
about the URL layout from unauthenticated users.
```python
from starlette.authentication import requires
@requires(['authenticated', 'admin'], status_code=404)
async def dashboard(request):
...
```
!!! note
The `status_code` parameter is not supported with WebSockets. The 403 (Forbidden)
status code will always be used for those.
Alternatively you might want to redirect unauthenticated users to a different
page.
```python
from starlette.authentication import requires
async def homepage(request):
...
@requires('authenticated', redirect='homepage')
async def dashboard(request):
...
```
When redirecting users, the page you redirect them to will include the URL they originally requested at the `next` query param:
```python
from starlette.authentication import requires
from starlette.responses import RedirectResponse
@requires('authenticated', redirect='login')
async def admin(request):
...
async def login(request):
if request.method == "POST":
# Now that the user is authenticated,
# we can send them to their original request destination
if request.user.is_authenticated:
next_url = request.query_params.get("next")
if next_url:
return RedirectResponse(next_url)
return RedirectResponse("/")
```
For class-based endpoints, you should wrap the decorator
around a method on the class.
```python
from starlette.authentication import requires
from starlette.endpoints import HTTPEndpoint
class Dashboard(HTTPEndpoint):
@requires("authenticated")
async def get(self, request):
...
```
## Custom authentication error responses
You can customise the error response sent when a `AuthenticationError` is
raised by an auth backend:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
def on_auth_error(request: Request, exc: Exception):
return JSONResponse({"error": str(exc)}, status_code=401)
app = Starlette(
middleware=[
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error),
],
)
```
starlette-1.7.0/docs/background.md 0000664 0000000 0000000 00000003651 15254677127 0017144 0 ustar 00root root 0000000 0000000
Starlette includes a `BackgroundTask` class for in-process background tasks.
A background task should be attached to a response, and will run only once
the response has been sent.
### Background Task
Used to add a single background task to a response.
Signature: `BackgroundTask(func, *args, **kwargs)`
```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.background import BackgroundTask
...
async def signup(request):
data = await request.json()
username = data['username']
email = data['email']
task = BackgroundTask(send_welcome_email, to_address=email)
message = {'status': 'Signup successful'}
return JSONResponse(message, background=task)
async def send_welcome_email(to_address):
...
routes = [
...
Route('/user/signup', endpoint=signup, methods=['POST'])
]
app = Starlette(routes=routes)
```
### BackgroundTasks
Used to add multiple background tasks to a response.
Signature: `BackgroundTasks(tasks=[])`
```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.background import BackgroundTasks
async def signup(request):
data = await request.json()
username = data['username']
email = data['email']
tasks = BackgroundTasks()
tasks.add_task(send_welcome_email, to_address=email)
tasks.add_task(send_admin_notification, username=username)
message = {'status': 'Signup successful'}
return JSONResponse(message, background=tasks)
async def send_welcome_email(to_address):
...
async def send_admin_notification(username):
...
routes = [
Route('/user/signup', endpoint=signup, methods=['POST'])
]
app = Starlette(routes=routes)
```
!!! important
The tasks are executed in order. In case one of the tasks raises
an exception, the following tasks will not get the opportunity to be executed.
starlette-1.7.0/docs/config.md 0000664 0000000 0000000 00000015460 15254677127 0016273 0 ustar 00root root 0000000 0000000 Starlette encourages a strict separation of configuration from code,
following [the twelve-factor pattern][twelve-factor].
Configuration should be stored in environment variables, or in a `.env` file
that is not committed to source control.
```python title="main.py"
from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette.config import Config
from starlette.datastructures import CommaSeparatedStrings, Secret
# Config will be read from environment variables and/or ".env" files.
config = Config(".env")
DEBUG = config('DEBUG', cast=bool, default=False)
DATABASE_URL = config('DATABASE_URL')
SECRET_KEY = config('SECRET_KEY', cast=Secret)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeparatedStrings)
app = Starlette(debug=DEBUG)
engine = create_engine(DATABASE_URL)
...
```
```shell title=".env"
# Don't commit this to source control.
# Eg. Include ".env" in your `.gitignore` file.
DEBUG=True
DATABASE_URL=postgresql://user:password@localhost:5432/database
SECRET_KEY=43n080musdfjt54t-09sdgr
ALLOWED_HOSTS=127.0.0.1, localhost
```
## Configuration precedence
The order in which configuration values are read is:
* From an environment variable.
* From the `.env` file.
* The default value given in `config`.
If none of those match, then `config(...)` will raise an error.
## Secrets
For sensitive keys, the `Secret` class is useful, since it helps minimize
occasions where the value it holds could leak out into tracebacks or
other code introspection.
To get the value of a `Secret` instance, you must explicitly cast it to a string.
You should only do this at the point at which the value is used.
```python
>>> from myproject import settings
>>> settings.SECRET_KEY
Secret('**********')
>>> str(settings.SECRET_KEY)
'98n349$%8b8-7yjn0n8y93T$23r'
```
!!! tip
You can use `DatabaseURL` from `databases`
package [here](https://github.com/encode/databases/blob/ab5eb718a78a27afe18775754e9c0fa2ad9cd211/databases/core.py#L420)
to store database URLs and avoid leaking them in the logs.
## CommaSeparatedStrings
For holding multiple inside a single config key, the `CommaSeparatedStrings`
type is useful.
```python
>>> from myproject import settings
>>> print(settings.ALLOWED_HOSTS)
CommaSeparatedStrings(['127.0.0.1', 'localhost'])
>>> print(list(settings.ALLOWED_HOSTS))
['127.0.0.1', 'localhost']
>>> print(len(settings.ALLOWED_HOSTS))
2
>>> print(settings.ALLOWED_HOSTS[0])
'127.0.0.1'
```
## Reading or modifying the environment
In some cases you might want to read or modify the environment variables programmatically.
This is particularly useful in testing, where you may want to override particular
keys in the environment.
Rather than reading or writing from `os.environ`, you should use Starlette's
`environ` instance. This instance is a mapping onto the standard `os.environ`
that additionally protects you by raising an error if any environment variable
is set *after* the point that it has already been read by the configuration.
If you're using `pytest`, then you can setup any initial environment in
`tests/conftest.py`.
```python title="tests/conftest.py"
from starlette.config import environ
environ['DEBUG'] = 'TRUE'
```
## Reading prefixed environment variables
You can namespace the environment variables by setting `env_prefix` argument.
```python title="myproject/settings.py"
import os
from starlette.config import Config
os.environ['APP_DEBUG'] = 'yes'
os.environ['ENVIRONMENT'] = 'dev'
config = Config(env_prefix='APP_')
DEBUG = config('DEBUG') # lookups APP_DEBUG, returns "yes"
ENVIRONMENT = config('ENVIRONMENT') # lookups APP_ENVIRONMENT, raises KeyError as variable is not defined
```
## Custom encoding for environment files
By default, Starlette reads environment files using UTF-8 encoding.
You can specify a different encoding by setting `encoding` argument.
```python title="myproject/settings.py"
from starlette.config import Config
# Using custom encoding for .env file
config = Config(".env", encoding="latin-1")
```
## A full example
Structuring large applications can be complex. You need proper separation of
configuration and code, database isolation during tests, separate test and
production databases, etc...
Here we'll take a look at a complete example, that demonstrates how
we can start to structure an application.
First, let's keep our settings, our database table definitions, and our
application logic separated:
```python title="myproject/settings.py"
from starlette.config import Config
from starlette.datastructures import Secret
config = Config(".env")
DEBUG = config('DEBUG', cast=bool, default=False)
SECRET_KEY = config('SECRET_KEY', cast=Secret)
DATABASE_URL = config('DATABASE_URL')
```
```python title="myproject/tables.py"
import sqlalchemy
# Database table definitions.
metadata = sqlalchemy.MetaData()
organisations = sqlalchemy.Table(
...
)
```
```python title="myproject/app.py"
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Route
from myproject import settings
async def homepage(request):
...
routes = [
Route("/", endpoint=homepage)
]
middleware = [
Middleware(
SessionMiddleware,
secret_key=settings.SECRET_KEY,
)
]
app = Starlette(debug=settings.DEBUG, routes=routes, middleware=middleware)
```
Now let's deal with our test configuration.
We'd like to create a new test database every time the test suite runs,
and drop it once the tests complete. We'd also like to ensure
```python title="tests/conftest.py"
from starlette.config import environ
from starlette.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy_utils import create_database, database_exists, drop_database
# This line would raise an error if we use it after 'settings' has been imported.
environ['DEBUG'] = 'TRUE'
from myproject import settings
from myproject.app import app
from myproject.tables import metadata
@pytest.fixture(autouse=True, scope="session")
def setup_test_database():
"""
Create a clean test database every time the tests are run.
"""
url = settings.DATABASE_URL
engine = create_engine(url)
assert not database_exists(url), 'Test database already exists. Aborting tests.'
create_database(url) # Create the test database.
metadata.create_all(engine) # Create the tables.
yield # Run the tests.
drop_database(url) # Drop the test database.
@pytest.fixture()
def client():
"""
Make a 'client' fixture available to test cases.
"""
# Our fixture is created within a context manager. This ensures that
# application lifespan runs for every test case.
with TestClient(app) as test_client:
yield test_client
```
[twelve-factor]: https://12factor.net/config
starlette-1.7.0/docs/contributing.md 0000664 0000000 0000000 00000012752 15254677127 0017536 0 ustar 00root root 0000000 0000000 # Contributing
Thank you for being interested in contributing to Starlette.
There are many ways you can contribute to the project:
- Try Starlette and [report bugs/issues you find](https://github.com/Kludex/starlette/issues/new)
- [Implement new features](https://github.com/Kludex/starlette/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [Review Pull Requests of others](https://github.com/Kludex/starlette/pulls)
- Write documentation
- Participate in discussions
## Reporting Bugs or Other Issues
Found something that Starlette should support?
Stumbled upon some unexpected behaviour?
Contributions should generally start out with [a discussion](https://github.com/Kludex/starlette/discussions).
Possible bugs may be raised as a "Potential Issue" discussion, feature requests may
be raised as an "Ideas" discussion. We can then determine if the discussion needs
to be escalated into an "Issue" or not, or if we'd consider a pull request.
Try to be as descriptive as you can and, in the case of a bug report,
provide as much information as possible like:
- OS platform
- Python version
- Installed dependencies and versions (`python -m pip freeze`)
- Code snippet
- Error traceback
You should always try to reduce any examples to the *simplest possible case*
that demonstrates the issue.
## Development
To start developing Starlette, create a **fork** of the
[Starlette repository](https://github.com/Kludex/starlette) on GitHub.
Then clone your fork with the following command replacing `YOUR-USERNAME` with
your GitHub username:
```shell
$ git clone https://github.com/YOUR-USERNAME/starlette
```
You can now install the project and its dependencies using:
```shell
$ cd starlette
$ scripts/install
```
## Testing and Linting
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
To run the tests, use:
```shell
$ scripts/test
```
Any additional arguments will be passed to `pytest`. See the [pytest documentation](https://docs.pytest.org/en/latest/how-to/usage.html) for more information.
For example, to run a single test script:
```shell
$ scripts/test tests/test_application.py
```
To run the code auto-formatting:
```shell
$ scripts/lint
```
Lastly, to run code checks separately (they are also run as part of `scripts/test`), run:
```shell
$ scripts/check
```
## Documenting
Documentation pages are located under the `docs/` folder.
To run the documentation site locally (useful for previewing changes), use:
```shell
$ scripts/docs
```
## Resolving Build / CI Failures
Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed.
Here are some common ways the test suite can fail:
### Check Job Failed
This job failing means there is either a code formatting issue or a type-annotation issue.
You can look at the job output to figure out why it failed, or run the following within a shell:
```shell
$ scripts/check
```
It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code
and if that job succeeds commit the changes.
### Docs Job Failed
This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`.
### Python 3.X Job Failed
This job failing means the unit tests failed or not all code paths are covered by unit tests.
If tests are failing you will see this message under the coverage report:
`=== 1 failed, 435 passed, 1 skipped, 1 xfailed in 11.09s ===`
If tests succeed but coverage doesn't reach our current threshold, you will see this
message under the coverage report:
`FAIL Required test coverage of 100% not reached. Total coverage: 99.00%`
## Releasing
*This section is targeted at Starlette maintainers.*
Before releasing a new version, create a pull request that includes:
- **An update to the changelog**:
- We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/).
- [Compare](https://github.com/Kludex/starlette/compare/) `main` with the tag of the latest release, and list all entries that are of interest to our users:
- Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes.
- Things that **should not** go in the changelog: changes to documentation, tests or tooling.
- Try sorting entries in descending order of impact / importance.
- Keep it concise and to-the-point. 🎯
- **A version bump**: see `__version__.py`.
For an example, see [#1600](https://github.com/Kludex/starlette/pull/1600).
Once the release PR is merged, create a
[new release](https://github.com/Kludex/starlette/releases/new) including:
- Tag version like `0.13.3`.
- Release title `Version 0.13.3`
- Description copied from the changelog.
Once created this release will be automatically uploaded to PyPI.
starlette-1.7.0/docs/css/ 0000775 0000000 0000000 00000000000 15254677127 0015266 5 ustar 00root root 0000000 0000000 starlette-1.7.0/docs/css/custom.css 0000664 0000000 0000000 00000003115 15254677127 0017312 0 ustar 00root root 0000000 0000000 /* Lighter dark mode colors */
[data-md-color-scheme="slate"] {
--md-default-bg-color: #263238;
--md-default-fg-color: #e0e0e0;
--md-code-bg-color: #2e3c43;
}
/* Logfire announcement banner */
.md-banner {
text-align: center;
}
a.md-banner__link {
color: currentcolor;
text-decoration: none;
}
a.md-banner__link:hover,
a.md-banner__link:focus {
color: currentcolor;
text-decoration: underline;
}
/* Sidebar sponsors */
.md-nav__sponsors {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin: 1.2rem 0.4rem 0.6rem;
padding: 0.9rem 0.6rem 0.8rem;
background-color: color-mix(in srgb, var(--md-primary-fg-color) 8%, transparent);
border-radius: 0.4rem;
}
.md-nav__sponsors-title {
margin: 0 0 0.1rem;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--md-default-fg-color--light);
}
.md-nav__sponsor {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0.25rem;
border-radius: 0.2rem;
transition: opacity 0.15s;
}
.md-nav__sponsor:hover {
opacity: 0.75;
}
.md-nav__sponsor img {
max-width: 100%;
max-height: 1.6rem;
object-fit: contain;
}
.md-nav__sponsor-cta {
display: inline-block;
margin-top: 0.15rem;
padding: 0.25rem 0.6rem;
font-size: 0.65rem;
font-weight: 600;
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
border-radius: 0.2rem;
text-decoration: none;
transition: opacity 0.15s;
}
.md-nav__sponsor-cta:hover {
opacity: 0.85;
color: var(--md-primary-bg-color);
}
starlette-1.7.0/docs/database.md 0000664 0000000 0000000 00000001262 15254677127 0016565 0 ustar 00root root 0000000 0000000 Starlette is not strictly tied to any particular database implementation.
You are free to use any async database library that you prefer. Some popular options include:
- [SQLAlchemy](https://www.sqlalchemy.org/) - The Python SQL toolkit with native async support (2.0+).
- [SQLModel](https://sqlmodel.tiangolo.com/) - SQL databases in Python, designed for simplicity, built on top of SQLAlchemy and Pydantic.
- [Tortoise ORM](https://tortoise.github.io/) - An easy-to-use asyncio ORM inspired by Django.
- [Piccolo](https://piccolo-orm.com/) - A fast, user-friendly ORM and query builder.
Refer to your chosen database library's documentation for specific connection and query patterns.
starlette-1.7.0/docs/endpoints.md 0000664 0000000 0000000 00000010226 15254677127 0017024 0 ustar 00root root 0000000 0000000
Starlette includes the classes `HTTPEndpoint` and `WebSocketEndpoint` that provide a class-based view pattern for
handling HTTP method dispatching and WebSocket sessions.
### HTTPEndpoint
The `HTTPEndpoint` class can be used as an ASGI application:
```python
from starlette.responses import PlainTextResponse
from starlette.endpoints import HTTPEndpoint
class App(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse(f"Hello, world!")
```
If you're using a Starlette application instance to handle routing, you can
dispatch to an `HTTPEndpoint` class. Make sure to dispatch to the class itself,
rather than to an instance of the class:
```python
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.endpoints import HTTPEndpoint
from starlette.routing import Route
class Homepage(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse(f"Hello, world!")
async def query(self, request):
return PlainTextResponse(f"Hello, query!")
class User(HTTPEndpoint):
async def get(self, request):
username = request.path_params['username']
return PlainTextResponse(f"Hello, {username}")
routes = [
Route("/", Homepage),
Route("/{username}", User)
]
app = Starlette(routes=routes)
```
HTTP endpoint classes will respond with "405 Method not allowed" responses for any
request methods which do not map to a corresponding handler.
### WebSocketEndpoint
The `WebSocketEndpoint` class is an ASGI application that presents a wrapper around
the functionality of a `WebSocket` instance.
The ASGI connection scope is accessible on the endpoint instance via `.scope` and
has an attribute `encoding` which may optionally be set, in order to validate the expected websocket data in the `on_receive` method.
The encoding types are:
* `'json'`
* `'bytes'`
* `'text'`
There are three overridable methods for handling specific ASGI websocket message types:
* `async def on_connect(websocket, **kwargs)`
* `async def on_receive(websocket, data)`
* `async def on_disconnect(websocket, close_code)`
```python
from starlette.endpoints import WebSocketEndpoint
class App(WebSocketEndpoint):
encoding = 'bytes'
async def on_connect(self, websocket):
await websocket.accept()
async def on_receive(self, websocket, data):
await websocket.send_bytes(b"Message: " + data)
async def on_disconnect(self, websocket, close_code):
pass
```
The `WebSocketEndpoint` can also be used with the `Starlette` application class:
```python
import uvicorn
from starlette.applications import Starlette
from starlette.endpoints import WebSocketEndpoint, HTTPEndpoint
from starlette.responses import HTMLResponse
from starlette.routing import Route, WebSocketRoute
html = """
Chat
WebSocket Chat
"""
class Homepage(HTTPEndpoint):
async def get(self, request):
return HTMLResponse(html)
class Echo(WebSocketEndpoint):
encoding = "text"
async def on_receive(self, websocket, data):
await websocket.send_text(f"Message text was: {data}")
routes = [
Route("/", Homepage),
WebSocketRoute("/ws", Echo)
]
app = Starlette(routes=routes)
```
starlette-1.7.0/docs/exceptions.md 0000664 0000000 0000000 00000012271 15254677127 0017204 0 ustar 00root root 0000000 0000000
Starlette allows you to install custom exception handlers to deal with
how you return responses when errors or handled exceptions occur.
```python
from starlette.applications import Starlette
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
HTML_404_PAGE = ...
HTML_500_PAGE = ...
async def not_found(request: Request, exc: HTTPException):
return HTMLResponse(content=HTML_404_PAGE, status_code=exc.status_code)
async def server_error(request: Request, exc: HTTPException):
return HTMLResponse(content=HTML_500_PAGE, status_code=exc.status_code)
exception_handlers = {
404: not_found,
500: server_error
}
app = Starlette(routes=routes, exception_handlers=exception_handlers)
```
If `debug` is enabled and an error occurs, then instead of using the installed
500 handler, Starlette will respond with a traceback response.
```python
app = Starlette(debug=True, routes=routes, exception_handlers=exception_handlers)
```
As well as registering handlers for specific status codes, you can also
register handlers for classes of exceptions.
In particular you might want to override how the built-in `HTTPException` class
is handled. For example, to use JSON style responses:
```python
async def http_exception(request: Request, exc: HTTPException):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
exception_handlers = {
HTTPException: http_exception
}
```
The `HTTPException` is also equipped with the `headers` argument. Which allows the propagation
of the headers to the response class:
```python
async def http_exception(request: Request, exc: HTTPException):
return JSONResponse(
{"detail": exc.detail},
status_code=exc.status_code,
headers=exc.headers
)
```
You might also want to override how `WebSocketException` is handled:
```python
async def websocket_exception(websocket: WebSocket, exc: WebSocketException):
await websocket.close(code=1008)
exception_handlers = {
WebSocketException: websocket_exception
}
```
## Errors and handled exceptions
It is important to differentiate between handled exceptions and errors.
Handled exceptions do not represent error cases. They are coerced into appropriate
HTTP responses, which are then sent through the standard middleware stack. By default
the `HTTPException` class is used to manage any handled exceptions.
Errors are any other exception that occurs within the application. These cases
should bubble through the entire middleware stack as exceptions. Any error
logging middleware should ensure that it re-raises the exception all the
way up to the server.
In practical terms, the error handled used is `exception_handler[500]` or `exception_handler[Exception]`.
Both keys `500` and `Exception` can be used. See below:
```python
async def handle_error(request: Request, exc: HTTPException):
# Perform some logic
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
exception_handlers = {
Exception: handle_error # or "500: handle_error"
}
```
It's important to notice that in case a [`BackgroundTask`](background.md) raises an exception,
it will be handled by the `handle_error` function, but at that point, the response was already sent. In other words,
the response created by `handle_error` will be discarded. In case the error happens before the response was sent, then
it will use the response object - in the above example, the returned `JSONResponse`.
In order to deal with this behaviour correctly, the middleware stack of a
`Starlette` application is configured like this:
* `ServerErrorMiddleware` - Returns 500 responses when server errors occur.
* Installed middleware
* `ExceptionMiddleware` - Deals with handled exceptions, and returns responses.
* Router
* Endpoints
## HTTPException
The `HTTPException` class provides a base class that you can use for any handled exceptions.
The `ExceptionMiddleware` implementation defaults to returning plain-text HTTP responses for any `HTTPException`.
* `HTTPException(status_code, detail=None, headers=None)`
When `detail` is not provided, it defaults to the standard phrase for the status code. If no standard phrase exists,
it defaults to an empty string.
You should only raise `HTTPException` inside routing or endpoints.
Middleware classes should instead just return appropriate responses directly.
You can use an `HTTPException` on a WebSocket endpoint. In case it's raised before `websocket.accept()`
the connection is not upgraded to a WebSocket connection, and the proper HTTP response is returned.
```python
from starlette.applications import Starlette
from starlette.exceptions import HTTPException
from starlette.routing import WebSocketRoute
from starlette.websockets import WebSocket
async def websocket_endpoint(websocket: WebSocket):
raise HTTPException(status_code=400, detail="Bad request")
app = Starlette(routes=[WebSocketRoute("/ws", websocket_endpoint)])
```
## WebSocketException
You can use the `WebSocketException` class to raise errors inside of WebSocket endpoints.
* `WebSocketException(code, reason=None)`
You can set any code valid as defined [in the specification](https://tools.ietf.org/html/rfc6455#section-7.4.1).
starlette-1.7.0/docs/graphql.md 0000664 0000000 0000000 00000001144 15254677127 0016456 0 ustar 00root root 0000000 0000000 GraphQL support in Starlette was deprecated in version 0.15.0, and removed in version 0.17.0.
Although GraphQL support is no longer built in to Starlette, you can still use GraphQL with Starlette via 3rd party libraries. These libraries all have Starlette-specific guides to help you do just that:
- [Ariadne](https://ariadnegraphql.org/docs/starlette-integration.html)
- [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3#example)
- [Strawberry](https://strawberry.rocks/docs/integrations/starlette)
- [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/usage/#starlette)
starlette-1.7.0/docs/img/ 0000775 0000000 0000000 00000000000 15254677127 0015252 5 ustar 00root root 0000000 0000000 starlette-1.7.0/docs/img/fastapi-logo.png 0000664 0000000 0000000 00000046603 15254677127 0020356 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR q í[Ú@ pHYs Û ÛͬùC tEXtSoftware www.inkscape.org›î< IDATxœìÝyœ•uÝÿñ×ç:3€
â¾ç®hîíYyW¶è]R¶h(:0ƒšË`Ë4w)sF™3’VšIšv«©ýÜ2½Õ\rIMMsO@Á˜�s®Ïï�M…áúž¹Î9×ûùxÃçûf80çú|7swDDDDDDD¤¶Ei‘òS@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É 5 DDDDDDD2@
‘P@DDDDDD$Ô É€º´ˆ„dsÎÜ–ÜÊñh[ŒmpßccÜGâŒÀl㾆öý÷À1Vàþ2ØKà/c¼HlOñOÜž†øïÞÔ²8�ß™ˆˆˆˆˆH2æîigY'vÎ;á~0î {{–wTxã>Üî Žo÷‰Ó_*ï˜""""""É© UÃÎíØŠ|‹? ö~`TÚ™Àxç&¢Üµ¼n7úäÉKÓN%"""""òvj HE³9ù=ÈÙ‘¸ì‘vž5ó°[À/#×s¹7¶.J;‘ˆˆˆˆˆ¨ Èæžµ9¥î±ÀQ`»¥�'�"æ7‚ý’†îË}lkwÚ�DDDDD$»Ô �Š``òŸÂ9ãÓÔÚ•ÆË8‚Íõ¦©O;Žˆˆˆˆˆd� ’*›=»žºec€ÉÀîiç)?wˆn ?›¦iW;è/ ˆˆˆˆˆ5 $6·m(¥Á ú6°IÚyRr7ð?Þ4íi‘Ú§€(kk̨†ñàSÀ6K;OeðÛ1~àZnH;‰ˆˆˆHv~Çú¬ˆG熕†S²
‰l8îÃßü6[¯÷ø`œa½ÿÍ¿ÿÀmæoܲ´¬|9ÆK8‹pù"<·/½@ÝÆOycãÊþ-‹T5 dÀØœÓ?Ný¸Êö+#»ž¨ôm?ý¡´“d�òß}ó
ˆ¼;�ïóæ–_§CDäíìÜÓ7eeݤDErÅŸxã©/ŠT³ììŽ-¨‹w%f`;ŒíÀ¶ÆØg$0œbÄ"ðð8æÿÀ£Ç°ø¯Žîö㦾–b®²³®öÿÆ£ƒñ¸Û›[~&‘TšÚ:hM*’Í™±3QôS¢ÜÇÒÎRÙüãÄÑ�Vh?›¥u?ôÉ“—®ù×HΉ˜ošvŒª`v1 €ˆTž¹S0Ÿ–¨F©.¦† Týlþü‹žÚŠc¶?ÎÀîäŽØÛ~AåÌ+Ö;;â8¸A�ÇVÈ?܉ñb»É›§>œnÔÀbû$æãÕ0[ü H©8j HÙX[[£ê'EßÒÎS%ƒ}‹aÅ/ZgûDon¹.í@""²vl^[K‡îà§Ü“vž,±ÙmP×Д¼’�·³Î:ÝO9eIòZÕͺòM8Àz`•ôpŸDìì�s,æX!ÿ$p5p݃oòI“–§Q¤¼Ô �²°sò{3ªáçÀžig©N¶-Æ•ÖÙ~!uvŠ7N{%íD""ÒËÚÚêØlðÎĶÎ^˜íŒ††ˆâXàaTÖZ]}°a€JPßÓœ Vus¶ ²°=n{`"0‘†Ë¬�¿óó˜Ðr£nk’Z¤€e`ò'‚ŸVŸvžªgö5Jþa›“?Ö'NûSÚqDD²¤÷kZûö¸í�Ùž{á¾;£v#îÛßüö%Ð2àl^[4|3XAçd›×öcÛÚ¬¦T‹¡ÀWqû*…üæv>1?÷§>Ÿv0‘PÔ �`ì'§oÊàÜ/€CôŽ($ۚȯµB~ºäqÚ‰DDj•už¶5V×
Œv[¯÷Kšæ+Öòúc€ÍƒÕ36¥»áX ¬¦T£0ÿ9¾g]íç—Ú½ù;ϤJ$©(í R¬Ð¾ƒs·‡¤�¥6Yø.›6\c]m£ÒN#"R³lЖÀ8à ²±ü¹ªÙüù9°ÉÁ;“{k‹P�[3V÷˜ògÛìŽÒ$’„ ’˜uæO »Ø&í,5Ïù(^«fŽN;ŠˆˆHêþãH`§2TÞ�—žS†ºR½ê�‰Ôù£VhŸ¦‘T+5 d�˜uu|céÞõš1¶-”n²Bþ3i'I•Ù”²ÕvZVsÑ�Ȱv=q—�Ó±ÚaDúK
Y'6kÖ
í¿Áý{igɨõ€ù½«/DDD²ÇÎé8دŒCìMWþ¿ÊX_ªÛ>ÄþÖÕñ=5Фš¨ ýf3g£aÅïÀŽH;KÆÕa̱Bþ´´ƒˆˆˆ¸Ø[Ê>†3ìcH5«Ãýèì¸Ò
í¥Fdm¨ ýbgµ
gXñÀÇÒÎ"oš¢&€ˆˆd‰u¶ÀÀ¼9ÄæÌ8h Æ‘jf~ØÖuúžiGY5 dY¡}#êë¯;8í,ò¦X!FÚ!DDD„ÙÀÍÌ[¤U ²6vÂsîkN‰T,5 dØÌ™ÃÀ.Û'í,ò޾i]í?H;„ˆˆH9Y¡cà¿n@ްsfì>`ãI5Û³ët8 T²º´Hå³Y³†0¬ô{àig‘5p;Õ
íK½©E«DD¤6¹OÂtËðè q ǬE¯€½
þðjï7{åß?íËÁºWý%¾¬<¶ÄC j ß lllÝ÷ãJ0œØ¯±ÂÌ�yÓäûÓ#òvj È»²¶¶ˆQõ€}(í,²Ö~d]ÿò S™v�Œ¹|~Ú!Ê/z8í"’]6gÆ¢èèØùºÍžñ?iú»ÚãQåþ¼Ùüù9>º6x;Ì·Ã}{Œí�ýpvrå5F@ñ÷vÞ¬}}ܤ—xl‘w¥€¼»M†t€ë´ÿªb†Ç]6çôç}â©×§�&3Œ|BK>í""5-ŠÆCSyuÑxàG)Œ]ÝêG,)÷>fL x¦ïÛ-oý9ël[�¨~<:üp`çrçéy[VôüÜà˜1EÖLg È;²BûÌON;‡¬D”ûµ�sÆNi' Áæ·
&¦¡ÙfÏ®Oq|YÞÜúºOh¹É›¦Nõ¦i»`¥½p?›ÞmefŸ§«ý[åGdí© «e…ŽƒÁf¦�CNÿÎf·m�v‘Ä5| Ø"Å›S·ü¨Ç— |©zsËIx÷–8S€ÅåÐfØÜ»–u‘~P@þƒuž±_N;‹$¶3uõçXÚADDD:%í à�ABðæÖ×½yÚLð�Å2
5ˆRÔQ¦Ú"ý¦€¬ÂÚÚ",þ%fivØ%(û<�íi.™IÄæä?ì—°Ìs¢ìc�‡¨#›Z{Ó´oc~ Æ=eæs6§ããeª-Ò/j ȪFÕO>’v ̘a…™£ÓŽ!""²N"&%¬P$â3À뉳˜'Í"È'´ÜË‹ÝaÞY–"?S+2¥¨ o²Bû~À÷ÓÎ!å`õPºÀæµ5¤�DDD¤?lî;ŸKTĹÌÇO»¸ @¤Ïè�ÝÚäEŸÐ2ç \~o
íZ ©Ó5€€µµ
fTÃyÀ ´³”ðAƒØuøˆªëº¾°ì5ž_º4ÔÕ1{Ð]ÿ]à»�ꉈˆ”_±t2fÉîq÷øÇ DÑOˆã&’M‚EÄñ7�e’ŠåÍÓ~b�+1Ÿ¶rÔü¿°5EúG
éµé�é¸ïžvŒrùæ^ûG?8àƒÉÞ<°‹{(>éÏ,-êö-ëj¿Ô'´Ü´®ˆˆHØÜü†˜›°Ê>qú >~ÊcVÈ_|:a´±Vhÿž7µ”÷yI�7Oí´B~G à5~~¸�{ú¦~ü©/†«)Ò?Ú Øœü¸OI;G¹ÔçrLØã½UóZ_ÒÓÍ1×_Y{ÃU¥×V¬]¾§`mmjþ‰ˆHå+1X?Q
çÇ«þØš¨^¯a¸� ŽT²ÝÓ€»VÌÊ蘀õDújФŒrü„¾òï+ïÙ-Úlè°ªXþÃsOù>óÏ/^üøÃ¡÷�½…í˦
zÓ"""ÍæÏÏ‘|™ý“l²ý«ü?Í-×'¬ÆÉ6wnMn�”^ÞÚZÄíh ;\U;,\-‘þS ã윎#q>œvŽr:i¯ý+þuÞ],òÝ;ÿT:ìªùÅ€{þß™ó›Û6²ì㈈ˆ¬«…Oþ7°}Â*³|̘U¶Ó98Æì„u¶$^üÅ u¤‚yóÔ‡1
K¾ßfΰžH¿Tüƒ‘”�Íš5„8ž‘vŽrúÔ6;Ø^#6©èÙÿ‡/ò^þ«bǽwı—ÿÙ¿ÏFë¿7Pƒ‰ˆˆô›qJÂ
‹ñög¢î€—Ö‡ØNN\C*_u ËUÌÐâ‡Õé7íβ†î&ˆ¶M;F9�2zÿŠ=øÏÝéüÛ½ñ´Ûo*(…=ëo�vÎgûø)�
üà"ýgsÛ†R²-ÎX<’Ȇã>¢�À×ë]Šk>§lwcÑr<î[LäOó,äžñæ)ÿJù·T5ln~CÜv¢ä›c¾9mŽ3÷á˜øP°nz¯Ízâ"n/bþ<æÏƒ=Ljžç}LkðƒM¤6YWû{Á>�°Ê\on}}u?ã�ˬ�?˜œlŸÚ÷ó¦–�ûÄ¥Âxó”Ygûù˜ºù!úpM˜Z"ý£@FÙÌ™Ã<Õ´òì5b¤²Å69ûÿìÒ×ü¸¯.ÝôÜÓ6åÿŸ,GOŽK/ƒÈªŒÎö!�ù®˜í‰ûnÀ¶Ð°æ`}é}ßóÆ_#_å;p0{Ë÷€[߯�±B~9p/Æ_pî¿Ý›Zž�ßh³Î¶õ þƒ`ÁØcwœz?�Ðû¹ïû$üÇçÿ�Ÿxócû>÷‹b+äÿ…ÛSOcü
â‰r÷ó²'½µµŒgŸHÕ‰íD’}_I‘³ßõ#Ìçà6 H6YÐûP˜ð¦©x‘_Œj ˜ï¤ŽÈ:P «†O•vŒrúöÞEf•÷üÙ“�ÆM7_[ZÜð<™uæ_±¹3:¼qú#i'‘ìù÷Ã>ûaÑ~àûûb6üßóeí‘
Þ�óþ7Y¡ãA<¾”(þO8õÁr^IììŽ-¨ãHÜÇ`
óÖ÷áþ"`Ì· Þ÷f§Ã¨†×³ãðû�»©‹o©î—â¬Ðþ‰´S¼©Øs§ŸÔújÚ1Ö–Í=s$ÆW–ù�Ÿ4õÙwû ŸÐòO+ä¯ ¾�h$ç(›{æoüö¢Du¤²½¸âÿÕð°y€j;¨!²NÔ È ›Û6jzÏÚÆّ;ìRQg\¼²¢›Sþ|}éÂǪ Y.ËQdš¹�bçž¾)+£Ï�}
øfõþLŠ‹aVá{ö®:ȵZ!x'Kë.ôÉ“—¦�,4£ëŒÏ@|
9ÁS=h=Ìß¼€RDïj¿žÈ~ÏÊîkªéì³ÀgÓNñ¦\ý�À_ÒŽ±Öââñô6èÔˆfÝF?…8Y ú2·'¬#Ì[[c+ä¯!Ì{¦lÖ¬!>iÒò µDú¥¢�d€ëÇ#ÒŽQN'íµ48W9Ûÿozîißç’Ÿ+ëá¿�ñ%ëüñÖiÇ�Úfóçç¬�ÿ=+sÏ‚ýø"°QÚ¹Ö`o°sVzÖ
Vh¯ô¼kÅÚÚ"ëê8šBþoxü¿8§2ßl†Ù×p~C]ÃBëÌ_ggÏLz"¼T8kk«ÃiJXæFŸ8åžµù@ošr3ðׄã�ÓÔwm¡Ô4»?P¡ˆ†ÒN�j‰ôK%~Á—2êûâtRÚ9ÊiýÁƒ·ëèŠxm÷”z¯÷ûÔU—Ÿ[úz¥Lq¾�
"ZèP‘Õ뻆kÕ¥åÕc8ø°Ç¬�ŸhmmÕø{ À:óïgTÃí¸ÿØ-í<ý0ãP|åjt“²iýáÀ6ÉŠØYýúp·w?+`ílÃKÿø|€:RÑâp[üTÓqK媈‡$@/?qf;¦£œŽÛut4¼¾!í<¼ø%ÿÐÀ_ï·nœqº“V@µß818›Q
wÙœUµÓfÏ®·®üYH;Ï:zÂOš¾0íRfžt’žaäöWõë—,‹.’o1 u@œT®bîlcý`µDúA
€¬‰ýø´#”SÎŒ‰{î›êëÚÝ9ï‘ûã÷]öËâ_-¬ð'ÿ7mÀ°øÈ´CHÍ{<í �ìMÝe]ù/¥dmØœ;S·ìvœoBÂsÕÓugÚ¤¼¬0s4ØG’ág}+ŽÖZïvQ¢q{bsò{¨#•ª'Z¬– ’5 2ÄÎíØ
ø¯´s”ÓvØ9Úný
S{ƒûâò¥~Ä5—›n¾¶´¬¸2ë(—v©qfµÒ Xç7VÈ7í ïÆ
g|„(ú?`Ÿ´³$gw¤�@ÊÌJIgÐWÏ]§_ÛÏŽ
`DLPG*ÕäÉË€~5˜Þ™¯¦ŽHÿ¨�%+âcÁjú€šoŽÞ?µ×ôïž|4Þç’ŸÿðôÕ2ëÿ6v°�3c÷´SH-ójß°:?´ÎŽï¤buzW(Ä×§�%w ¨avÞ¬�q¾–¬Wxã©/¬Ë/í;4ðîDã÷:Úææ7PG*�÷^YóZ˜j‘ ’
5 ²Ä¨ŠåªëêC›omŽÚbÀgÿ_]ÑÃÄ?]WúòuW”^ê®òÛ\âèËiG�V[+ þÍüGÖÙ>)íoeçÌ8çW@}ÚYYIÏà{Ó!e´¢g04Q
÷sýz#Ä*€õ(26@©\aÞkÆqMOÊIåR #ìœüÞ`»¤�£œ&í}À€¿žïxñy?èÒŠ?{ø¾Ê»Þoݨ åc5¹ —ÙVÈ:í 6gÆAÄÑ|`PÚY‚1Ð}Ùµ«÷†"kNXæQšZ®OTae÷¯�¥ s€ÑlÕ}Þ†¼»03÷j%�Hÿ¨�15}ÀÛN·Ã¶ÙaÀ^ÏÅ8æGwßVúèÿñê+Uºäµv°®ö÷¦Bj“7N{¨ÕSÜsÀEVèHµÑjg�5œÈ~CÒ™ÔJãhÿ-[ôäç€íÕ0ŸÛ·<{�ùI¯'ÊÑkg:Û? ŽT›5k½ÿÞ(fºÖTR¡@fxMßM{ÊÞF‘
L³ýïK^ò^þËâÿÜuk\ªôëýÖIôÙ´HM«ÝU 0üÖÖ–Þ×ÖúžXíÝ-ýÿµÍ=éì7%Ÿ"
qb MAêHe©/†;ßÁµ@ÒQ—v )¿ÞÓÿm·´s”ËÆ
Cøê{v+ûnwçü¿?O¾í†ÒÒ•ÕvÂ?¸
øaÚ1¤f=¼?PåÀb û-?î¦wö»ØN¨Ùšµs £š€98&