---
[](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 `httpx`.
* 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`.
* [`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/
[python-multipart]: https://multipart.fastapiexpert.com/
[itsdangerous]: https://itsdangerous.palletsprojects.com/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
[techempower]: https://www.techempower.com/benchmarks/#hw=ph&test=fortune&l=zijzen-sf
starlette-1.6.0/benchmarks/ 0000775 0000000 0000000 00000000000 15235671106 0015650 5 ustar 00root root 0000000 0000000 starlette-1.6.0/benchmarks/README.md 0000664 0000000 0000000 00000005367 15235671106 0017142 0 ustar 00root root 0000000 0000000 # Benchmarks
## 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.6.0/benchmarks/gzip_benchmark.py 0000664 0000000 0000000 00000031147 15235671106 0021213 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 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)
class ASGIRunner:
def __init__(self) -> None:
self.loop = asyncio.new_event_loop()
def run(self, app: ASGIApp, scope: Scope) -> list[Message]:
return self.loop.run_until_complete(run_asgi(app, scope))
def close(self) -> None:
self.loop.close()
async def run_asgi(app: ASGIApp, scope: Scope) -> list[Message]:
messages: list[Message] = []
async def receive() -> Message:
raise AssertionError("The benchmark app must not receive a request body")
async def send(message: Message) -> None:
messages.append(message)
await app(scope, receive, send)
return messages
@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.6.0/benchmarks/routing_benchmark.py 0000664 0000000 0000000 00000006653 15235671106 0021735 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import asyncio
from collections.abc import Iterator
import pytest
from pytest_codspeed.plugin import BenchmarkFixture
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""}
async def run_asgi(app: ASGIApp, scope: Scope) -> list[Message]:
messages: list[Message] = []
async def receive() -> Message:
raise AssertionError("The benchmark app must not receive a request body")
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, method: str, path: str) -> list[Message]:
return self.loop.run_until_complete(run_asgi(app, http_scope(method, path)))
def close(self) -> None:
self.loop.close()
@pytest.fixture(scope="module")
def runner() -> Iterator[ASGIRunner]:
runner = ASGIRunner()
yield runner
runner.close()
def test_routing_static_early(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(LARGE_ROUTER, "GET", "/resources0"))
assert messages[0]["status"] == 200
def test_routing_static_late(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(LARGE_ROUTER, "GET", "/resources29"))
assert messages[0]["status"] == 200
def test_routing_param_late(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(LARGE_ROUTER, "GET", "/resources29/123/items/first"))
assert messages[0]["status"] == 200
def test_routing_miss(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(LARGE_ROUTER, "GET", "/no/such/path"))
assert messages[0]["status"] == 404
def test_routing_method_not_allowed(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(LARGE_ROUTER, "DELETE", "/resources29"))
assert messages[0]["status"] == 405
def test_routing_small_app(runner: ASGIRunner, benchmark: BenchmarkFixture) -> None:
messages = benchmark(lambda: runner.run(SMALL_ROUTER, "GET", "/resources4/7"))
assert messages[0]["status"] == 200
starlette-1.6.0/docs/ 0000775 0000000 0000000 00000000000 15235671106 0014463 5 ustar 00root root 0000000 0000000 starlette-1.6.0/docs/CNAME 0000664 0000000 0000000 00000000021 15235671106 0015222 0 ustar 00root root 0000000 0000000 www.starlette.io
starlette-1.6.0/docs/applications.md 0000664 0000000 0000000 00000003372 15235671106 0017500 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.6.0/docs/authentication.md 0000664 0000000 0000000 00000012550 15235671106 0020027 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 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.6.0/docs/background.md 0000664 0000000 0000000 00000003651 15235671106 0017131 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.6.0/docs/config.md 0000664 0000000 0000000 00000015460 15235671106 0016260 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.6.0/docs/contributing.md 0000664 0000000 0000000 00000012752 15235671106 0017523 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.6.0/docs/css/ 0000775 0000000 0000000 00000000000 15235671106 0015253 5 ustar 00root root 0000000 0000000 starlette-1.6.0/docs/css/custom.css 0000664 0000000 0000000 00000003115 15235671106 0017277 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.6.0/docs/database.md 0000664 0000000 0000000 00000001262 15235671106 0016552 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.6.0/docs/endpoints.md 0000664 0000000 0000000 00000010077 15235671106 0017015 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!")
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