mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
deploy: 1c729c8542
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,8 +30,9 @@ Here's a minimal Dockerfile::
|
||||
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
COPY . .
|
||||
RUN pip install responder
|
||||
RUN uv pip install --system responder
|
||||
ENV PORT=80
|
||||
EXPOSE 80
|
||||
CMD ["python", "api.py"]
|
||||
@@ -42,9 +43,10 @@ Build and run::
|
||||
$ docker run -p 8000:80 myapi
|
||||
|
||||
The ``python:3.13-slim`` image is about 150MB — small enough for fast
|
||||
deploys but includes everything you need. For even smaller images, you
|
||||
can use ``python:3.13-alpine``, though some packages may need extra
|
||||
build dependencies.
|
||||
deploys but includes everything you need. Using ``uv`` for installs
|
||||
is significantly faster than pip. For even smaller images, you can use
|
||||
``python:3.13-alpine``, though some packages may need extra build
|
||||
dependencies.
|
||||
|
||||
|
||||
Cloud Platforms
|
||||
@@ -182,4 +184,4 @@ Before going live:
|
||||
- **Add a health check** — ``/health`` endpoint for monitoring
|
||||
- **Enable HTTPS** — via your proxy, cloud platform, or uvicorn's ``--ssl-*`` flags
|
||||
- **Set up logging** — uvicorn logs requests by default; pipe them to your log aggregator
|
||||
- **Pin your dependencies** — commit ``uv.lock`` for reproducible deploys
|
||||
- **Pin your dependencies** — use a lock file or pinned requirements for reproducible deploys
|
||||
|
||||
@@ -376,3 +376,9 @@ jump into the tutorials:
|
||||
- :doc:`tutorial-rest` — build a full CRUD API with validation
|
||||
- :doc:`tutorial-sqlalchemy` — connect to a database
|
||||
- :doc:`tutorial-auth` — add authentication
|
||||
- :doc:`tutorial-websockets` — real-time communication
|
||||
- :doc:`tutorial-middleware` — hooks and middleware
|
||||
- :doc:`tutorial-flask` — migrating from Flask
|
||||
- :doc:`guide-config` — environment variables and secrets
|
||||
- :doc:`deployment` — Docker, cloud platforms, and production
|
||||
- :doc:`testing` — writing tests with pytest
|
||||
|
||||
+43
-17
@@ -2,35 +2,61 @@
|
||||
# Development Sandbox
|
||||
|
||||
## Setup
|
||||
Set up a development sandbox.
|
||||
|
||||
Acquire sources and create virtualenv.
|
||||
Clone the repo and install all dependencies:
|
||||
```shell
|
||||
git clone https://github.com/kennethreitz/responder.git
|
||||
cd responder
|
||||
uv venv
|
||||
```
|
||||
|
||||
Install project in editable mode, including
|
||||
all development tools.
|
||||
```shell
|
||||
uv venv && source .venv/bin/activate
|
||||
uv pip install --upgrade --editable '.[develop,docs,release,test]'
|
||||
```
|
||||
|
||||
## Operations
|
||||
Run tests.
|
||||
## Running Tests
|
||||
```shell
|
||||
source .venv/bin/activate
|
||||
pytest
|
||||
pytest # full suite with coverage
|
||||
pytest tests/test_responder.py -xvs # single file, stop on first failure
|
||||
pytest -k "test_mount" # run tests matching a pattern
|
||||
```
|
||||
|
||||
Format code.
|
||||
## Code Formatting
|
||||
```shell
|
||||
ruff format .
|
||||
ruff check --fix .
|
||||
ruff format . # auto-format
|
||||
ruff check --fix . # lint and auto-fix
|
||||
```
|
||||
|
||||
Documentation authoring.
|
||||
## Type Checking
|
||||
```shell
|
||||
sphinx-autobuild --open-browser --watch docs/source docs/source docs/build
|
||||
mypy
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Live-reloading doc server (opens in browser):
|
||||
```shell
|
||||
cd docs
|
||||
sphinx-autobuild --open-browser --watch source source build
|
||||
```
|
||||
|
||||
Or build once:
|
||||
```shell
|
||||
cd docs
|
||||
make html
|
||||
# open build/html/index.html
|
||||
```
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
responder/
|
||||
├── responder/ # main package
|
||||
│ ├── api.py # API class — the entry point
|
||||
│ ├── routes.py # Router, Route, WebSocketRoute
|
||||
│ ├── models.py # Request and Response wrappers
|
||||
│ ├── ext/ # extensions (CLI, GraphQL, OpenAPI, rate limiting)
|
||||
│ ├── background.py # background task queue
|
||||
│ └── formats.py # content negotiation (JSON, YAML, msgpack)
|
||||
├── tests/ # pytest test suite
|
||||
├── examples/ # runnable example apps
|
||||
├── docs/source/ # Sphinx documentation
|
||||
└── pyproject.toml # project metadata and tool config
|
||||
```
|
||||
|
||||
@@ -46,14 +46,14 @@ Install PyJWT::
|
||||
Create a helper to encode and decode tokens::
|
||||
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
SECRET = "your-secret-key"
|
||||
|
||||
def create_token(user_id: int) -> str:
|
||||
payload = {
|
||||
"sub": user_id,
|
||||
"exp": datetime.utcnow() + timedelta(hours=24),
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
}
|
||||
return jwt.encode(payload, SECRET, algorithm="HS256")
|
||||
|
||||
@@ -189,3 +189,56 @@ Remember to set a proper secret key::
|
||||
The session data is signed (not encrypted) — users can read it but
|
||||
can't tamper with it. Don't store sensitive data like passwords in
|
||||
sessions.
|
||||
|
||||
|
||||
Role-Based Access Control
|
||||
--------------------------
|
||||
|
||||
For APIs where different users have different permissions, embed the
|
||||
role in the token and check it in route-specific guards::
|
||||
|
||||
def create_token(user_id: int, role: str = "user") -> str:
|
||||
payload = {
|
||||
"sub": user_id,
|
||||
"role": role,
|
||||
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
}
|
||||
return jwt.encode(payload, SECRET, algorithm="HS256")
|
||||
|
||||
Create a helper that checks for a specific role::
|
||||
|
||||
def require_role(*roles):
|
||||
"""Before-request hook factory that restricts by role."""
|
||||
def check(req, resp):
|
||||
user_role = getattr(req.state, "role", None)
|
||||
if user_role not in roles:
|
||||
resp.status_code = 403
|
||||
resp.media = {"error": "Insufficient permissions"}
|
||||
return check
|
||||
|
||||
Use it on specific routes::
|
||||
|
||||
@api.route("/admin/users", before_request=require_role("admin"))
|
||||
def list_all_users(req, resp):
|
||||
resp.media = {"users": [...]}
|
||||
|
||||
And store the role during token verification::
|
||||
|
||||
# In your auth_guard:
|
||||
req.state.user_id = payload["sub"]
|
||||
req.state.role = payload.get("role", "user")
|
||||
|
||||
|
||||
Choosing an Auth Strategy
|
||||
--------------------------
|
||||
|
||||
- **API keys** — simplest. Good for server-to-server, CLI tools, and
|
||||
internal services. No expiration unless you build it.
|
||||
- **JWT tokens** — standard for SPAs and mobile apps. Stateless, so
|
||||
they scale well. Downside: you can't revoke them without a blocklist.
|
||||
- **Sessions** — best for traditional web apps with HTML forms. The
|
||||
browser manages cookies automatically. Stateful — the server controls
|
||||
the session lifecycle.
|
||||
|
||||
Start with API keys for internal tools, JWT for public APIs, and
|
||||
sessions for web apps with login pages.
|
||||
|
||||
@@ -28,8 +28,8 @@ SQLAlchemy models map Python classes to database tables. Each attribute
|
||||
becomes a column::
|
||||
|
||||
# models.py
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
@@ -37,15 +37,16 @@ becomes a column::
|
||||
class Book(Base):
|
||||
__tablename__ = "books"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String, nullable=False)
|
||||
author = Column(String, nullable=False)
|
||||
year = Column(Integer, nullable=False)
|
||||
isbn = Column(String, nullable=True)
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
author: Mapped[str] = mapped_column(String, nullable=False)
|
||||
year: Mapped[int] = mapped_column(nullable=False)
|
||||
isbn: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
|
||||
``DeclarativeBase`` is SQLAlchemy's modern base class (SQLAlchemy 2.0+).
|
||||
Each model class corresponds to a table, and each ``Column`` corresponds
|
||||
to a column in that table.
|
||||
This uses SQLAlchemy 2.0's ``Mapped`` type annotations and
|
||||
``mapped_column()``, which give you type checker support and cleaner
|
||||
syntax than the older ``Column()`` style. Each model class corresponds
|
||||
to a table, and each ``mapped_column()`` corresponds to a column.
|
||||
|
||||
|
||||
Database Setup
|
||||
|
||||
@@ -58,6 +58,8 @@ A chat room needs to broadcast messages to all connected clients. We keep
|
||||
a set of active connections and iterate through them when someone sends
|
||||
a message::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
connected = set()
|
||||
|
||||
@api.route("/chat", websocket=True)
|
||||
@@ -70,13 +72,15 @@ a message::
|
||||
# Broadcast to all connected clients
|
||||
for client in connected:
|
||||
await client.send_text(message)
|
||||
except Exception:
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
connected.discard(ws)
|
||||
|
||||
The ``try/finally`` block ensures we remove disconnected clients from
|
||||
the set, even if the connection drops unexpectedly.
|
||||
the set, even if the connection drops unexpectedly. Catching
|
||||
``WebSocketDisconnect`` specifically (rather than bare ``Exception``)
|
||||
makes the intent clear and avoids swallowing real bugs.
|
||||
|
||||
|
||||
Data Formats
|
||||
@@ -154,6 +158,40 @@ WebSocket before-request hooks receive the ``ws`` object and must call
|
||||
``await ws.accept()`` if they want the connection to proceed.
|
||||
|
||||
|
||||
Connection Lifecycle
|
||||
--------------------
|
||||
|
||||
WebSocket connections go through several states:
|
||||
|
||||
1. **Connecting** — the client sends an upgrade request
|
||||
2. **Open** — after ``await ws.accept()``, both sides can send messages
|
||||
3. **Closing** — either side initiates a close handshake
|
||||
4. **Closed** — the connection is fully terminated
|
||||
|
||||
When a client disconnects (closes the tab, loses network), the next
|
||||
``await ws.receive_text()`` raises ``WebSocketDisconnect``. Always
|
||||
handle this — otherwise your server accumulates dead connections::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
@api.route("/ws", websocket=True)
|
||||
async def handler(ws):
|
||||
await ws.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive_text()
|
||||
await ws.send_text(f"Got: {data}")
|
||||
except WebSocketDisconnect:
|
||||
print("Client disconnected")
|
||||
|
||||
You can also close connections from the server side::
|
||||
|
||||
await ws.close(code=1000) # 1000 = normal closure
|
||||
|
||||
Common close codes: ``1000`` (normal), ``1001`` (going away),
|
||||
``1008`` (policy violation), ``1011`` (server error).
|
||||
|
||||
|
||||
Testing WebSockets
|
||||
------------------
|
||||
|
||||
@@ -169,3 +207,13 @@ Use Starlette's ``TestClient`` for WebSocket tests::
|
||||
|
||||
The ``websocket_connect`` context manager handles the connection
|
||||
lifecycle — it connects on enter and disconnects on exit.
|
||||
|
||||
You can also test that connections are properly rejected::
|
||||
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
def test_websocket_404():
|
||||
client = TestClient(api)
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("/nonexistent"):
|
||||
pass
|
||||
|
||||
+7
-5
@@ -65,8 +65,9 @@ making a reverse proxy like nginx optional for many deployments.</p>
|
||||
Here’s a minimal Dockerfile:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">FROM</span> <span class="n">python</span><span class="p">:</span><span class="mf">3.13</span><span class="o">-</span><span class="n">slim</span>
|
||||
<span class="n">WORKDIR</span> <span class="o">/</span><span class="n">app</span>
|
||||
<span class="n">COPY</span> <span class="o">--</span><span class="n">from</span><span class="o">=</span><span class="n">ghcr</span><span class="o">.</span><span class="n">io</span><span class="o">/</span><span class="n">astral</span><span class="o">-</span><span class="n">sh</span><span class="o">/</span><span class="n">uv</span><span class="p">:</span><span class="n">latest</span> <span class="o">/</span><span class="n">uv</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="nb">bin</span><span class="o">/</span><span class="n">uv</span>
|
||||
<span class="n">COPY</span> <span class="o">.</span> <span class="o">.</span>
|
||||
<span class="n">RUN</span> <span class="n">pip</span> <span class="n">install</span> <span class="n">responder</span>
|
||||
<span class="n">RUN</span> <span class="n">uv</span> <span class="n">pip</span> <span class="n">install</span> <span class="o">--</span><span class="n">system</span> <span class="n">responder</span>
|
||||
<span class="n">ENV</span> <span class="n">PORT</span><span class="o">=</span><span class="mi">80</span>
|
||||
<span class="n">EXPOSE</span> <span class="mi">80</span>
|
||||
<span class="n">CMD</span> <span class="p">[</span><span class="s2">"python"</span><span class="p">,</span> <span class="s2">"api.py"</span><span class="p">]</span>
|
||||
@@ -78,9 +79,10 @@ $ docker run -p 8000:80 myapi
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">python:3.13-slim</span></code> image is about 150MB — small enough for fast
|
||||
deploys but includes everything you need. For even smaller images, you
|
||||
can use <code class="docutils literal notranslate"><span class="pre">python:3.13-alpine</span></code>, though some packages may need extra
|
||||
build dependencies.</p>
|
||||
deploys but includes everything you need. Using <code class="docutils literal notranslate"><span class="pre">uv</span></code> for installs
|
||||
is significantly faster than pip. For even smaller images, you can use
|
||||
<code class="docutils literal notranslate"><span class="pre">python:3.13-alpine</span></code>, though some packages may need extra build
|
||||
dependencies.</p>
|
||||
</section>
|
||||
<section id="cloud-platforms">
|
||||
<h2>Cloud Platforms<a class="headerlink" href="#cloud-platforms" title="Link to this heading">¶</a></h2>
|
||||
@@ -203,7 +205,7 @@ uvicorn directly without a reverse proxy and do just fine.</p>
|
||||
<li><p><strong>Add a health check</strong> — <code class="docutils literal notranslate"><span class="pre">/health</span></code> endpoint for monitoring</p></li>
|
||||
<li><p><strong>Enable HTTPS</strong> — via your proxy, cloud platform, or uvicorn’s <code class="docutils literal notranslate"><span class="pre">--ssl-*</span></code> flags</p></li>
|
||||
<li><p><strong>Set up logging</strong> — uvicorn logs requests by default; pipe them to your log aggregator</p></li>
|
||||
<li><p><strong>Pin your dependencies</strong> — commit <code class="docutils literal notranslate"><span class="pre">uv.lock</span></code> for reproducible deploys</p></li>
|
||||
<li><p><strong>Pin your dependencies</strong> — use a lock file or pinned requirements for reproducible deploys</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -245,6 +245,8 @@ work with — welcome.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-auth.html#skipping-auth-for-public-routes">Skipping Auth for Public Routes</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-auth.html#custom-exception-for-auth-errors">Custom Exception for Auth Errors</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-auth.html#using-sessions-for-web-apps">Using Sessions for Web Apps</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-auth.html#role-based-access-control">Role-Based Access Control</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-auth.html#choosing-an-auth-strategy">Choosing an Auth Strategy</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial-websockets.html">WebSocket Tutorial</a><ul>
|
||||
@@ -254,6 +256,7 @@ work with — welcome.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-websockets.html#data-formats">Data Formats</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-websockets.html#html-client">HTML Client</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-websockets.html#before-request-hooks-for-websockets">Before-Request Hooks for WebSockets</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-websockets.html#connection-lifecycle">Connection Lifecycle</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial-websockets.html#testing-websockets">Testing WebSockets</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -385,6 +385,12 @@ jump into the tutorials:</p>
|
||||
<li><p><a class="reference internal" href="tutorial-rest.html"><span class="doc">Building a REST API</span></a> — build a full CRUD API with validation</p></li>
|
||||
<li><p><a class="reference internal" href="tutorial-sqlalchemy.html"><span class="doc">Using SQLAlchemy</span></a> — connect to a database</p></li>
|
||||
<li><p><a class="reference internal" href="tutorial-auth.html"><span class="doc">Authentication</span></a> — add authentication</p></li>
|
||||
<li><p><a class="reference internal" href="tutorial-websockets.html"><span class="doc">WebSocket Tutorial</span></a> — real-time communication</p></li>
|
||||
<li><p><a class="reference internal" href="tutorial-middleware.html"><span class="doc">Writing Middleware</span></a> — hooks and middleware</p></li>
|
||||
<li><p><a class="reference internal" href="tutorial-flask.html"><span class="doc">Migrating from Flask</span></a> — migrating from Flask</p></li>
|
||||
<li><p><a class="reference internal" href="guide-config.html"><span class="doc">Configuration</span></a> — environment variables and secrets</p></li>
|
||||
<li><p><a class="reference internal" href="deployment.html"><span class="doc">Deployment</span></a> — Docker, cloud platforms, and production</p></li>
|
||||
<li><p><a class="reference internal" href="testing.html"><span class="doc">Testing</span></a> — writing tests with pytest</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
+53
-19
@@ -44,33 +44,63 @@
|
||||
<span id="sandbox"></span><h1>Development Sandbox<a class="headerlink" href="#development-sandbox" title="Link to this heading">¶</a></h1>
|
||||
<section id="setup">
|
||||
<h2>Setup<a class="headerlink" href="#setup" title="Link to this heading">¶</a></h2>
|
||||
<p>Set up a development sandbox.</p>
|
||||
<p>Acquire sources and create virtualenv.</p>
|
||||
<p>Clone the repo and install all dependencies:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>git<span class="w"> </span>clone<span class="w"> </span>https://github.com/kennethreitz/responder.git
|
||||
<span class="nb">cd</span><span class="w"> </span>responder
|
||||
uv<span class="w"> </span>venv
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Install project in editable mode, including
|
||||
all development tools.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>uv<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>--upgrade<span class="w"> </span>--editable<span class="w"> </span><span class="s1">'.[develop,docs,release,test]'</span>
|
||||
uv<span class="w"> </span>venv<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nb">source</span><span class="w"> </span>.venv/bin/activate
|
||||
uv<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>--upgrade<span class="w"> </span>--editable<span class="w"> </span><span class="s1">'.[develop,docs,release,test]'</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="operations">
|
||||
<h2>Operations<a class="headerlink" href="#operations" title="Link to this heading">¶</a></h2>
|
||||
<p>Run tests.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="nb">source</span><span class="w"> </span>.venv/bin/activate
|
||||
pytest
|
||||
<section id="running-tests">
|
||||
<h2>Running Tests<a class="headerlink" href="#running-tests" title="Link to this heading">¶</a></h2>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pytest<span class="w"> </span><span class="c1"># full suite with coverage</span>
|
||||
pytest<span class="w"> </span>tests/test_responder.py<span class="w"> </span>-xvs<span class="w"> </span><span class="c1"># single file, stop on first failure</span>
|
||||
pytest<span class="w"> </span>-k<span class="w"> </span><span class="s2">"test_mount"</span><span class="w"> </span><span class="c1"># run tests matching a pattern</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Format code.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>ruff<span class="w"> </span>format<span class="w"> </span>.
|
||||
ruff<span class="w"> </span>check<span class="w"> </span>--fix<span class="w"> </span>.
|
||||
</section>
|
||||
<section id="code-formatting">
|
||||
<h2>Code Formatting<a class="headerlink" href="#code-formatting" title="Link to this heading">¶</a></h2>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>ruff<span class="w"> </span>format<span class="w"> </span>.<span class="w"> </span><span class="c1"># auto-format</span>
|
||||
ruff<span class="w"> </span>check<span class="w"> </span>--fix<span class="w"> </span>.<span class="w"> </span><span class="c1"># lint and auto-fix</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Documentation authoring.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>sphinx-autobuild<span class="w"> </span>--open-browser<span class="w"> </span>--watch<span class="w"> </span>docs/source<span class="w"> </span>docs/source<span class="w"> </span>docs/build
|
||||
</section>
|
||||
<section id="type-checking">
|
||||
<h2>Type Checking<a class="headerlink" href="#type-checking" title="Link to this heading">¶</a></h2>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>mypy
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="documentation">
|
||||
<h2>Documentation<a class="headerlink" href="#documentation" title="Link to this heading">¶</a></h2>
|
||||
<p>Live-reloading doc server (opens in browser):</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="nb">cd</span><span class="w"> </span>docs
|
||||
sphinx-autobuild<span class="w"> </span>--open-browser<span class="w"> </span>--watch<span class="w"> </span><span class="nb">source</span><span class="w"> </span><span class="nb">source</span><span class="w"> </span>build
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Or build once:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="nb">cd</span><span class="w"> </span>docs
|
||||
make<span class="w"> </span>html
|
||||
<span class="c1"># open build/html/index.html</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="project-layout">
|
||||
<h2>Project Layout<a class="headerlink" href="#project-layout" title="Link to this heading">¶</a></h2>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>responder/
|
||||
├── responder/ # main package
|
||||
│ ├── api.py # API class — the entry point
|
||||
│ ├── routes.py # Router, Route, WebSocketRoute
|
||||
│ ├── models.py # Request and Response wrappers
|
||||
│ ├── ext/ # extensions (CLI, GraphQL, OpenAPI, rate limiting)
|
||||
│ ├── background.py # background task queue
|
||||
│ └── formats.py # content negotiation (JSON, YAML, msgpack)
|
||||
├── tests/ # pytest test suite
|
||||
├── examples/ # runnable example apps
|
||||
├── docs/source/ # Sphinx documentation
|
||||
└── pyproject.toml # project metadata and tool config
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -103,7 +133,11 @@ ruff<span class="w"> </span>check<span class="w"> </span>--fix<span class="w"> <
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Development Sandbox</a><ul>
|
||||
<li><a class="reference internal" href="#setup">Setup</a></li>
|
||||
<li><a class="reference internal" href="#operations">Operations</a></li>
|
||||
<li><a class="reference internal" href="#running-tests">Running Tests</a></li>
|
||||
<li><a class="reference internal" href="#code-formatting">Code Formatting</a></li>
|
||||
<li><a class="reference internal" href="#type-checking">Type Checking</a></li>
|
||||
<li><a class="reference internal" href="#documentation">Documentation</a></li>
|
||||
<li><a class="reference internal" href="#project-layout">Project Layout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+55
-2
@@ -80,14 +80,14 @@ common format is <a class="reference external" href="https://jwt.io/">JWT</a> (J
|
||||
</div>
|
||||
<p>Create a helper to encode and decode tokens:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">jwt</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||||
|
||||
<span class="n">SECRET</span> <span class="o">=</span> <span class="s2">"your-secret-key"</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">create_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="s2">"sub"</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
|
||||
<span class="s2">"exp"</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">()</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
|
||||
<span class="s2">"exp"</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
|
||||
<span class="p">}</span>
|
||||
<span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s2">"HS256"</span><span class="p">)</span>
|
||||
|
||||
@@ -222,6 +222,57 @@ sessions are simpler than tokens. The browser handles cookies automatically
|
||||
can’t tamper with it. Don’t store sensitive data like passwords in
|
||||
sessions.</p>
|
||||
</section>
|
||||
<section id="role-based-access-control">
|
||||
<h2>Role-Based Access Control<a class="headerlink" href="#role-based-access-control" title="Link to this heading">¶</a></h2>
|
||||
<p>For APIs where different users have different permissions, embed the
|
||||
role in the token and check it in route-specific guards:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">create_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">role</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"user"</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="s2">"sub"</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
|
||||
<span class="s2">"role"</span><span class="p">:</span> <span class="n">role</span><span class="p">,</span>
|
||||
<span class="s2">"exp"</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
|
||||
<span class="p">}</span>
|
||||
<span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s2">"HS256"</span><span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Create a helper that checks for a specific role:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">require_role</span><span class="p">(</span><span class="o">*</span><span class="n">roles</span><span class="p">):</span>
|
||||
<span class="w"> </span><span class="sd">"""Before-request hook factory that restricts by role."""</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">check</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="n">user_role</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="p">,</span> <span class="s2">"role"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">user_role</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">roles</span><span class="p">:</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">403</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Insufficient permissions"</span><span class="p">}</span>
|
||||
<span class="k">return</span> <span class="n">check</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Use it on specific routes:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/admin/users"</span><span class="p">,</span> <span class="n">before_request</span><span class="o">=</span><span class="n">require_role</span><span class="p">(</span><span class="s2">"admin"</span><span class="p">))</span>
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">list_all_users</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"users"</span><span class="p">:</span> <span class="p">[</span><span class="o">...</span><span class="p">]}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>And store the role during token verification:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># In your auth_guard:</span>
|
||||
<span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="n">payload</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">]</span>
|
||||
<span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">role</span> <span class="o">=</span> <span class="n">payload</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"role"</span><span class="p">,</span> <span class="s2">"user"</span><span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="choosing-an-auth-strategy">
|
||||
<h2>Choosing an Auth Strategy<a class="headerlink" href="#choosing-an-auth-strategy" title="Link to this heading">¶</a></h2>
|
||||
<ul class="simple">
|
||||
<li><p><strong>API keys</strong> — simplest. Good for server-to-server, CLI tools, and
|
||||
internal services. No expiration unless you build it.</p></li>
|
||||
<li><p><strong>JWT tokens</strong> — standard for SPAs and mobile apps. Stateless, so
|
||||
they scale well. Downside: you can’t revoke them without a blocklist.</p></li>
|
||||
<li><p><strong>Sessions</strong> — best for traditional web apps with HTML forms. The
|
||||
browser manages cookies automatically. Stateful — the server controls
|
||||
the session lifecycle.</p></li>
|
||||
</ul>
|
||||
<p>Start with API keys for internal tools, JWT for public APIs, and
|
||||
sessions for web apps with login pages.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -255,6 +306,8 @@ sessions.</p>
|
||||
<li><a class="reference internal" href="#skipping-auth-for-public-routes">Skipping Auth for Public Routes</a></li>
|
||||
<li><a class="reference internal" href="#custom-exception-for-auth-errors">Custom Exception for Auth Errors</a></li>
|
||||
<li><a class="reference internal" href="#using-sessions-for-web-apps">Using Sessions for Web Apps</a></li>
|
||||
<li><a class="reference internal" href="#role-based-access-control">Role-Based Access Control</a></li>
|
||||
<li><a class="reference internal" href="#choosing-an-auth-strategy">Choosing an Auth Strategy</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
+11
-10
@@ -63,8 +63,8 @@ MySQL, and any other database SQLAlchemy supports:</p>
|
||||
<p>SQLAlchemy models map Python classes to database tables. Each attribute
|
||||
becomes a column:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># models.py</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">sqlalchemy</span><span class="w"> </span><span class="kn">import</span> <span class="n">Column</span><span class="p">,</span> <span class="n">Integer</span><span class="p">,</span> <span class="n">String</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">sqlalchemy.orm</span><span class="w"> </span><span class="kn">import</span> <span class="n">DeclarativeBase</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">sqlalchemy</span><span class="w"> </span><span class="kn">import</span> <span class="n">String</span>
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">sqlalchemy.orm</span><span class="w"> </span><span class="kn">import</span> <span class="n">DeclarativeBase</span><span class="p">,</span> <span class="n">Mapped</span><span class="p">,</span> <span class="n">mapped_column</span>
|
||||
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">Base</span><span class="p">(</span><span class="n">DeclarativeBase</span><span class="p">):</span>
|
||||
<span class="k">pass</span>
|
||||
@@ -72,16 +72,17 @@ becomes a column:</p>
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">Book</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
|
||||
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">"books"</span>
|
||||
|
||||
<span class="nb">id</span> <span class="o">=</span> <span class="n">Column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="n">title</span> <span class="o">=</span> <span class="n">Column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">author</span> <span class="o">=</span> <span class="n">Column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">year</span> <span class="o">=</span> <span class="n">Column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">isbn</span> <span class="o">=</span> <span class="n">Column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="n">title</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">author</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">year</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||||
<span class="n">isbn</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">String</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">DeclarativeBase</span></code> is SQLAlchemy’s modern base class (SQLAlchemy 2.0+).
|
||||
Each model class corresponds to a table, and each <code class="docutils literal notranslate"><span class="pre">Column</span></code> corresponds
|
||||
to a column in that table.</p>
|
||||
<p>This uses SQLAlchemy 2.0’s <code class="docutils literal notranslate"><span class="pre">Mapped</span></code> type annotations and
|
||||
<code class="docutils literal notranslate"><span class="pre">mapped_column()</span></code>, which give you type checker support and cleaner
|
||||
syntax than the older <code class="docutils literal notranslate"><span class="pre">Column()</span></code> style. Each model class corresponds
|
||||
to a table, and each <code class="docutils literal notranslate"><span class="pre">mapped_column()</span></code> corresponds to a column.</p>
|
||||
</section>
|
||||
<section id="database-setup">
|
||||
<h2>Database Setup<a class="headerlink" href="#database-setup" title="Link to this heading">¶</a></h2>
|
||||
|
||||
@@ -91,7 +91,9 @@ ws.close()
|
||||
<p>A chat room needs to broadcast messages to all connected clients. We keep
|
||||
a set of active connections and iterate through them when someone sends
|
||||
a message:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">connected</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">starlette.websockets</span><span class="w"> </span><span class="kn">import</span> <span class="n">WebSocketDisconnect</span>
|
||||
|
||||
<span class="n">connected</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
|
||||
|
||||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/chat"</span><span class="p">,</span> <span class="n">websocket</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">chat</span><span class="p">(</span><span class="n">ws</span><span class="p">):</span>
|
||||
@@ -103,14 +105,16 @@ a message:</p>
|
||||
<span class="c1"># Broadcast to all connected clients</span>
|
||||
<span class="k">for</span> <span class="n">client</span> <span class="ow">in</span> <span class="n">connected</span><span class="p">:</span>
|
||||
<span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">send_text</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
|
||||
<span class="k">except</span> <span class="n">WebSocketDisconnect</span><span class="p">:</span>
|
||||
<span class="k">pass</span>
|
||||
<span class="k">finally</span><span class="p">:</span>
|
||||
<span class="n">connected</span><span class="o">.</span><span class="n">discard</span><span class="p">(</span><span class="n">ws</span><span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">try/finally</span></code> block ensures we remove disconnected clients from
|
||||
the set, even if the connection drops unexpectedly.</p>
|
||||
the set, even if the connection drops unexpectedly. Catching
|
||||
<code class="docutils literal notranslate"><span class="pre">WebSocketDisconnect</span></code> specifically (rather than bare <code class="docutils literal notranslate"><span class="pre">Exception</span></code>)
|
||||
makes the intent clear and avoids swallowing real bugs.</p>
|
||||
</section>
|
||||
<section id="data-formats">
|
||||
<h2>Data Formats<a class="headerlink" href="#data-formats" title="Link to this heading">¶</a></h2>
|
||||
@@ -179,6 +183,38 @@ HTTP before-request hooks. This is useful for authentication:</p>
|
||||
<p>WebSocket before-request hooks receive the <code class="docutils literal notranslate"><span class="pre">ws</span></code> object and must call
|
||||
<code class="docutils literal notranslate"><span class="pre">await</span> <span class="pre">ws.accept()</span></code> if they want the connection to proceed.</p>
|
||||
</section>
|
||||
<section id="connection-lifecycle">
|
||||
<h2>Connection Lifecycle<a class="headerlink" href="#connection-lifecycle" title="Link to this heading">¶</a></h2>
|
||||
<p>WebSocket connections go through several states:</p>
|
||||
<ol class="arabic simple">
|
||||
<li><p><strong>Connecting</strong> — the client sends an upgrade request</p></li>
|
||||
<li><p><strong>Open</strong> — after <code class="docutils literal notranslate"><span class="pre">await</span> <span class="pre">ws.accept()</span></code>, both sides can send messages</p></li>
|
||||
<li><p><strong>Closing</strong> — either side initiates a close handshake</p></li>
|
||||
<li><p><strong>Closed</strong> — the connection is fully terminated</p></li>
|
||||
</ol>
|
||||
<p>When a client disconnects (closes the tab, loses network), the next
|
||||
<code class="docutils literal notranslate"><span class="pre">await</span> <span class="pre">ws.receive_text()</span></code> raises <code class="docutils literal notranslate"><span class="pre">WebSocketDisconnect</span></code>. Always
|
||||
handle this — otherwise your server accumulates dead connections:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">starlette.websockets</span><span class="w"> </span><span class="kn">import</span> <span class="n">WebSocketDisconnect</span>
|
||||
|
||||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/ws"</span><span class="p">,</span> <span class="n">websocket</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">handler</span><span class="p">(</span><span class="n">ws</span><span class="p">):</span>
|
||||
<span class="k">await</span> <span class="n">ws</span><span class="o">.</span><span class="n">accept</span><span class="p">()</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
|
||||
<span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">ws</span><span class="o">.</span><span class="n">receive_text</span><span class="p">()</span>
|
||||
<span class="k">await</span> <span class="n">ws</span><span class="o">.</span><span class="n">send_text</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Got: </span><span class="si">{</span><span class="n">data</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="n">WebSocketDisconnect</span><span class="p">:</span>
|
||||
<span class="nb">print</span><span class="p">(</span><span class="s2">"Client disconnected"</span><span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>You can also close connections from the server side:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">await</span> <span class="n">ws</span><span class="o">.</span><span class="n">close</span><span class="p">(</span><span class="n">code</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span> <span class="c1"># 1000 = normal closure</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Common close codes: <code class="docutils literal notranslate"><span class="pre">1000</span></code> (normal), <code class="docutils literal notranslate"><span class="pre">1001</span></code> (going away),
|
||||
<code class="docutils literal notranslate"><span class="pre">1008</span></code> (policy violation), <code class="docutils literal notranslate"><span class="pre">1011</span></code> (server error).</p>
|
||||
</section>
|
||||
<section id="testing-websockets">
|
||||
<h2>Testing WebSockets<a class="headerlink" href="#testing-websockets" title="Link to this heading">¶</a></h2>
|
||||
<p>Use Starlette’s <code class="docutils literal notranslate"><span class="pre">TestClient</span></code> for WebSocket tests:</p>
|
||||
@@ -193,6 +229,16 @@ HTTP before-request hooks. This is useful for authentication:</p>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">websocket_connect</span></code> context manager handles the connection
|
||||
lifecycle — it connects on enter and disconnects on exit.</p>
|
||||
<p>You can also test that connections are properly rejected:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">starlette.websockets</span><span class="w"> </span><span class="kn">import</span> <span class="n">WebSocketDisconnect</span>
|
||||
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">test_websocket_404</span><span class="p">():</span>
|
||||
<span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">api</span><span class="p">)</span>
|
||||
<span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="n">WebSocketDisconnect</span><span class="p">):</span>
|
||||
<span class="k">with</span> <span class="n">client</span><span class="o">.</span><span class="n">websocket_connect</span><span class="p">(</span><span class="s2">"/nonexistent"</span><span class="p">):</span>
|
||||
<span class="k">pass</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -228,6 +274,7 @@ lifecycle — it connects on enter and disconnects on exit.</p>
|
||||
<li><a class="reference internal" href="#data-formats">Data Formats</a></li>
|
||||
<li><a class="reference internal" href="#html-client">HTML Client</a></li>
|
||||
<li><a class="reference internal" href="#before-request-hooks-for-websockets">Before-Request Hooks for WebSockets</a></li>
|
||||
<li><a class="reference internal" href="#connection-lifecycle">Connection Lifecycle</a></li>
|
||||
<li><a class="reference internal" href="#testing-websockets">Testing WebSockets</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user